Every scan on the phone failed with "bluetooth error: JNI call failed",
which is the entire symptom: jni's Display for JniCall drops the source
that says what actually went wrong. It was ThreadDetached.
droidplug reaches the JVM through JavaVM::get_env(), which does not
attach — it fails outright on any thread the JVM has never seen. Every
BLE call in this app is made from a Tauri task, and Tauri's default
runtime spawns plain Rust worker threads, so on Android no BLE call could
ever have worked. This was invisible until it ran on hardware: the
desktop build shares the code and does not care.
Attaching inside the tasks would not have fixed it. A Tokio task can move
to another worker at any .await, so the thread that starts a scan is not
necessarily the one that polls it next — the attachment has to belong to
the threads, not the work. on_thread_start is the hook that gets that
right, and it covers the blocking pool too. Permanent rather than scoped,
because a scoped attachment detaches at the end of the guard, which for a
worker thread means after the first task it runs.
Ordering is load-bearing at both ends. The JVM is stashed in
initBtleplug, which runs before the super chain that starts us, so it is
there when the runtime is built; and the runtime is installed before
tauri::Builder, because async_runtime::set only affects later spawns.
The scan log now carries the Debug form as well as Display. The chain
read `Bluetooth(Other(JniCall(ThreadDetached)))` all along and would have
named this in the first minute rather than the last.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>