Bluetooth: wire existing AudioRouteManager SCO support through both app
variants. Replace binary speaker toggle with 3-way route cycling
(Earpiece → Speaker → Bluetooth). Tauri side adds JNI bridge functions
(start/stop/query SCO, device availability) and Oboe stream restart.
Network awareness: integrate Android ConnectivityManager to detect
WiFi/cellular transitions and feed them to AdaptiveQualityController
via lock-free AtomicU8 signaling. Enables proactive quality downgrade
and FEC boost on network handoffs.
Build: add --arch flag to build-tauri-android.sh supporting arm64,
armv7, or all (separate per-arch APKs for smaller tester binaries).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add caller_build_version / callee_build_version (git short hash)
to DirectCallOffer and DirectCallAnswer so peers can identify each
other's build in debug logs. Also log own build at register time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous commit d269600 added the libc++_shared.so copy step but the
comment block included "Android's dynamic linker" — the apostrophe
closed the enclosing `bash -c '...'` single-quoted string prematurely.
Everything after "Android" was interpreted as wrapper-script bash
instead of docker-container bash, so JNI_ABI_DIR (set inside the
docker context) was unbound when the wrapper tried to use it.
Build failed with:
/tmp/wzp-tauri-build.sh: line 149: JNI_ABI_DIR: unbound variable
Note the pre-existing script uses backticks in its comments ("cargo-
tauri`s linker wiring") exactly to avoid this trap. Matched that style
and added an explicit NOTE to the comment explaining the quoting
hazard for future editors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause of "wzp-native not loaded" at runtime on opus-DRED-v2 APK:
libwzp_native.so has a NEEDED entry for libc++_shared.so (because
crates/wzp-native/build.rs uses cpp_link_stdlib(Some("c++_shared"))),
but the APK only contained:
lib/arm64-v8a/libwzp_desktop_lib.so (192 MB)
lib/arm64-v8a/libwzp_native.so (683 KB)
No libc++_shared.so → Android's dynamic linker fails the dlopen of
libwzp_native.so at runtime with "library libc++_shared.so not found",
and every audio path that routes through wzp_native (capture, playout,
register, direct call) refuses to start.
Diagnosis:
- readelf -d libwzp_native.so shows NEEDED libc++_shared.so
- python zipfile listing of the APK confirms libc++_shared.so is
absent from lib/arm64-v8a/
- scripts/build-and-notify.sh (the legacy wzp-android build path)
already had this fix at lines 126-134 with an explicit comment:
"cargo-ndk may not copy libc++_shared.so — grab it from the NDK if
missing". That fix was never ported to build-tauri-android.sh when
the Tauri mobile pipeline was set up.
Fix: after `cargo ndk build -p wzp-native --release` produces
libwzp_native.so into jniLibs, copy libc++_shared.so from the NDK
sysroot (same find pattern as build-and-notify.sh) into the same
jniLibs dir. Abort with a clear error if the NDK doesn't have the file.
Also noting the 191 MB vs 359 MB size discrepancy the user saw: that's
almost entirely libwzp_desktop_lib.so being a 192 MB debug build. The
old working APK was probably a release build (smaller main lib) or
included multiple arches (doubling/tripling the .so count). The size
is cosmetic — the crash is the real issue, and libc++_shared.so is
~2 MB so this fix doesn't close the size gap. Can investigate the
size difference separately after register + direct call work again.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 of the big refactor. Escape the Tauri Android
__init_tcb+4 symbol leak (rust-lang/rust#104707) by making
wzp-desktop's Android .so pure Rust — ZERO cc::Build, no cpp/ files,
no C++ in the rustc link step. All future C++ (Oboe audio bridge)
lives in a new standalone cdylib crate `wzp-native` which is built
with cargo-ndk (the same path the legacy wzp-android crate uses
successfully on the same phone + same NDK), copied into Tauri's
gen/android/app/src/main/jniLibs at build time, and dlopened by
wzp-desktop at runtime via libloading.
Changes in this commit:
- NEW crate crates/wzp-native/ with crate-type = ["cdylib"] only
(no staticlib, no rlib — rust#104707 shows mixing staticlib with
cdylib leaks non-exported symbols, which is the original bug
source). Phase 1 scaffold has TWO extern "C" functions:
wzp_native_version() -> i32 (returns 42)
wzp_native_hello(buf, cap) -> usize (writes a string)
So we can verify dlopen + dlsym + cross-.so FFI end-to-end
before adding any real C++.
- desktop/src-tauri/cpp/ directory DELETED (7 files gone).
- desktop/src-tauri/build.rs reduced to just the git hash capture
+ tauri_build::build(). No more cc::Build of any kind.
- desktop/src-tauri/Cargo.toml: drop cc from build-dependencies,
add libloading = "0.8" as an Android-only runtime dep.
- desktop/src-tauri/src/lib.rs Builder::setup() now (on Android only)
dlopens libwzp_native.so, calls wzp_native_version() and
wzp_native_hello(), and logs the result:
"wzp-native dlopen OK: version=42 msg=\"hello from wzp-native\""
If this log appears in logcat when the app launches and the home
screen still renders, the split-cdylib pipeline is validated and
Phase 2 (port the Oboe bridge into wzp-native) can proceed.
- scripts/build-tauri-android.sh: insert a `cargo ndk -t arm64-v8a
build --release -p wzp-native` step before `cargo tauri android
build`, with `-o desktop/src-tauri/gen/android/app/src/main/jniLibs`
so the resulting libwzp_native.so lands in the place gradle will
package into the final APK.
- Workspace Cargo.toml: add crates/wzp-native to [workspace] members.
Phase 2 (separate commit, only if Phase 1 works):
- Copy cpp/oboe_bridge.{h,cpp} + getauxval_fix.c from the legacy
wzp-android crate into crates/wzp-native/cpp/.
- Add cc = "1" as a build-dependency on wzp-native (safe: it's a
single-cdylib crate with no staticlib, so no symbol leak).
- Add build.rs that compiles the Oboe C++ and the wzp-native Rust
FFI exposes the audio start/stop/read/write functions.
- wzp-desktop::engine.rs dlopens wzp-native at CallEngine::start,
uses its audio functions instead of CPAL on Android.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Spent 10+ builds chasing a __init_tcb+4 / pthread_create SIGSEGV after
adding the oboe audio backend. Every "fix" made things worse. Reverting
all Android-specific files to the state at 35642d1 (build #6), which
was the last commit where the Tauri Android app actually launched,
rendered the home screen, and successfully registered on a relay.
Reverted files (all back to their 35642d1 content):
- desktop/src-tauri/Cargo.toml (no build-dep cc, no tracing-android)
- desktop/src-tauri/build.rs (git hash only, no Oboe / cc build)
- desktop/src-tauri/src/lib.rs (engine cfg-gated on non-android)
- desktop/src-tauri/src/main.rs (two-line desktop entry)
- desktop/src-tauri/src/engine.rs (desktop-only audio setup)
- scripts/Dockerfile.android-builder (no android24→26 clang shim)
- scripts/build-tauri-android.sh (no linker env vars / manifest patch)
Deleted (were added between b314138 and e2e023d):
- desktop/src-tauri/cpp/getauxval_fix.c
- desktop/src-tauri/cpp/oboe_bridge.{h,cpp}
- desktop/src-tauri/cpp/oboe_stub.cpp
- desktop/src-tauri/src/oboe_audio.rs
Next: rebuild image on remote (to drop the baked-in clang shim), build
an APK, install on Pixel 6, verify the UI renders the same way build #6
did. From there we add features back ONE at a time so we can actually
bisect which one triggers the tao::ndk_glue crash. User's rule:
"if you want to change stack, change incrementally, so we can debug".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build #13's PATH wrapper trick failed because tauri-cli invokes the linker
with an absolute path (/opt/android-sdk/ndk/.../bin/aarch64-linux-android24-
clang), which bypasses \$PATH entirely. The pthread_shim logs confirmed the
broken API-24 stubs were still being linked:
WZP_pthread_shim: dlsym(RTLD_DEFAULT, pthread_create) returned NULL:
libdl.a is a stub --- use libdl.so instead
Move the fix up a level — into the Dockerfile itself. On image build, for
each of the four android ABIs × {clang, clang++}, rename
`${abi}24-${suffix}` to `${abi}24-${suffix}.orig` and replace it with a
shell wrapper that exec()s `${abi}26-${suffix}`. Any call to the API-24
wrapper — via PATH, absolute path, or otherwise — now transparently runs
the API-26 wrapper, which uses the real libc.so/libdl.so bindings.
The old bash-c /tmp/wrappers workaround in build-tauri-android.sh is
removed now that the image handles it at the right layer.
Also add `--shell` to build-tauri-android.sh: opens an interactive docker
container on the remote with the same mounts/env as the build, so I can
iterate on cargo tauri android build / manually patch files / etc.
without the full git push → ssh → rebuild → install loop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build #12's instrumented pthread_shim gave us the definitive diagnosis:
WZP_pthread_shim: dlsym(RTLD_DEFAULT, pthread_create) returned NULL:
libdl.a is a stub --- use libdl.so instead
Tauri-cli invokes `aarch64-linux-android24-clang` as the linker and the
API-24 NDK sysroot ships *stub* libdl.a / libc.a: they compile fine but
every symbol crashes if called, because they're meant to coexist with a
separate dynamic .so that the dynamic linker provides at runtime. Rust's
pre-built libstd.rlib has static calls into those stubs baked in, so no
matter what we do at link time the broken code lands in the .so.
Env-var overrides of CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER don't
stick — tauri-cli resets them before invoking cargo. So instead of
fighting the env, we put a wrapper on $PATH, literally named
`aarch64-linux-android24-clang`, that exec()s the android26 version.
When tauri-cli looks up android24-clang via PATH, it gets our wrapper,
our wrapper runs android26-clang, and suddenly the whole build is using
the API-26 NDK sysroot with real dynamic bindings to libc.so / libdl.so.
Wrappers are installed for all four ABIs (aarch64, armv7, x86_64, i686)
× both suffixes (clang, clang++) directly inside the docker bash -c
preamble before any cargo invocation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous commit bumped minSdk from 24 to 26 in build.gradle.kts
hoping tauri-cli would pick it up and use the android26-clang linker,
but the crash recurred at exactly the same frame (__init_tcb via
pthread_create via std::thread::spawn). That means tauri-cli is
ignoring the gradle minSdk value and sticking with its hardcoded
aarch64-linux-android24-clang.
The android24 linker resolves __init_tcb against the broken static
stub in libc.a (API 24 does NOT export __init_tcb as a dynamic symbol
from libc.so — it only exists in the static archive, and the stub
expects the TCB to be initialised by a running static init path,
which never happens in a dlopen-loaded .so).
Override the linker env vars directly in the docker run invocation
for all four ABIs. These take precedence over anything tauri-cli or
.cargo/config.toml might set.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Build #7 crashed at launch on the Pixel 6 with SIGSEGV in
__init_tcb / pthread_create called from tao::ndk_glue::create in
WryActivity.onCreate:
#00 __init_tcb(bionic_tcb*, pthread_internal_t*)+4
#01 pthread_create+360
#02 std::sys::thread::unix::Thread::new
#04 tao::platform_impl::platform::ndk_glue::create
#05 Java_com_wzp_desktop_WryActivity_create
Tauri scaffolds build.gradle.kts with `minSdk = 24`, which makes the
tauri-cli invoke `aarch64-linux-android24-clang` as the Rust linker. That
linker transitively pulls broken static stubs from libc.a for getauxval,
__init_tcb and pthread_create — these stubs only work in statically-
linked executables because they read bionic state (__libc_auxv, TCB) that
only the libc init path sets up. In a .so loaded via dlopen they SIGSEGV
the moment anything spawns a thread.
API 26+ has the real runtime symbols and the NDK-26 linker resolves them
against libc.so instead of the static fallback. This is also the minimum
Oboe supports. Patch the generated build.gradle.kts post-init to swap
`minSdk = 24` for `minSdk = 26` — the legacy wzp-android crate solved
the same issue with a .cargo/config.toml linker override plus a
getauxval_fix.c shim.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This is the big one — the Tauri Android app now has a real audio stack
capable of full-duplex VoIP, reusing the proven C++ Oboe bridge from the
legacy wzp-android crate.
Architecture:
- desktop/src-tauri/cpp/ — copies of oboe_bridge.{h,cpp}, oboe_stub.cpp,
and getauxval_fix.c from crates/wzp-android/cpp/. build.rs clones
google/oboe@1.8.1 into OUT_DIR and compiles the bridge + all Oboe
sources as "oboe_bridge" static lib, linking against shared libc++
(static would pull broken libc stubs that SIGSEGV in .so libraries).
- src/oboe_audio.rs — Rust side: an SPSC ring buffer matching the C++
bridge's AtomicI32 layout, plus OboeHandle::start() which returns
(capture_ring, playout_ring, owning_handle). The ring exposes the same
(available / read / write) methods as wzp_client::audio_ring::AudioRing
so CallEngine treats both backends interchangeably.
- src/engine.rs — compiled on every platform now. A cfg-switched type
alias picks wzp_client::audio_ring::AudioRing on desktop and
crate::oboe_audio::AudioRing on Android. The audio setup block has
three branches: VPIO/CPAL on macOS, CPAL on Linux/Windows, Oboe on
Android. Send/recv tasks are identical across platforms.
- src/lib.rs — removes all the "step 3 not done" Android stubs. The
engine module is no longer cfg-gated; connect / disconnect / toggle_mic
/ toggle_speaker / get_status are single implementations used by both
desktop and Android. Identity path resolves via app.path().app_data_dir()
from the Tauri setup() callback (already wired in step 1).
Runtime mic permission:
- scripts/build-tauri-android.sh now injects RECORD_AUDIO + MODIFY_AUDIO_
SETTINGS into gen/android/app/src/main/AndroidManifest.xml after init,
and overwrites MainActivity.kt with a version that calls
ActivityCompat.requestPermissions in onCreate. This is idempotent:
every build re-applies the patches so tauri re-init can't regress them.
Cargo.toml:
- cc is now an unconditional build-dep (build.rs runs on the host, so
target-gating build-deps doesn't work).
- wzp-client is now a dep on every platform. On Android it gets default
features only (no "audio"/"vpio") so CPAL isn't dragged in — oboe_audio
provides the capture/playout rings instead.
- tracing-android is added on Android so tracing events flow into logcat.
build.rs also gained embedded git hash (WZP_GIT_HASH) capture, which is
shown under the fingerprint on the home screen — already committed in
7639aaf, reinstated here alongside the Oboe build logic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two related Android-only papercuts found while testing build #4 on a Pixel 6:
1. Frontend was crashing in the WebView with:
Tauri/Console: Uncaught (in promise) event.listen not allowed.
Permissions associated with this command: core:event:allow-listen,
core:event:default
The desktop build worked fine because Tauri's default capability set
covers the desktop side. On Android (and iOS) Tauri 2.x is much stricter
about ACL — without an explicit capabilities/default.json that lists
"android" in its platforms, the WebView gets zero permissions. Add a
default capability granting core:default + the event listener perms
across all five platforms (linux/macOS/windows/android/iOS).
2. Every fresh docker run produced a new ~/.android/debug.keystore, so
`adb install -r` of a freshly built APK over an already-installed one
failed with INSTALL_FAILED_UPDATE_INCOMPATIBLE. Mount a persistent host
volume at /home/builder/.android in build-tauri-android.sh so the same
debug keystore is reused across builds and `install -r` keeps working.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dockerfile.android-builder: install Android API 36 platform + build-tools
35.0.0 alongside the existing API 34 set. Tauri 2.x mobile defaults to
compileSdk 36 / build-tools 35; without these the gradle build fails with
"SDK directory is not writable" because the read-only /opt/android-sdk
volume can't grow at build time. Adding Node.js 20, all four Rust android
targets, and tauri-cli 2.x was already in place.
scripts/build-tauri-android.sh: new build wrapper for the desktop/ Tauri
project (parallel to scripts/build-and-notify.sh which targets the legacy
android/ Kotlin app). Pulls the branch on remote, runs cargo tauri android
build inside the docker image, and sends three ntfy.sh/wzp notifications
that all carry the short git hash:
- STARTED [hash] — <commit subject>
- OK [hash] (size) — <rustypaste apk url>
- FAILED [hash] (line N) — <rustypaste log url>
On failure the full /tmp/wzp-tauri-build.log is uploaded to rustypaste so
the URL in the failure ntfy is directly downloadable, same place as the
APK.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>