From 0173a5febf5b8487c787b4cd06629a48cb31f2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20DEM=C4=B0RC=C4=B0?= Date: Thu, 10 Sep 2026 13:47:32 -0700 Subject: [PATCH] fix(android): cache UsbNative class in nativeInit fd_bridge resolved UsbNative with FindClass on the first call, which runs on a Rust thread attached via attach_current_thread. Such threads use the system class loader, so the lookup throws ClassNotFoundException; the pending exception survives the early return and ART kills the app when the thread detaches (e.g. on the first available_ports call). nativeInit is a static native method on UsbNative, so its jclass argument is the class itself, resolved by the app class loader. Keep a global ref to it there and drop the thread-dependent FindClass. Co-Authored-By: Claude Opus 5 --- src/android/fd_bridge.rs | 23 ++++++++++++----------- src/android/jni.rs | 7 +++++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/android/fd_bridge.rs b/src/android/fd_bridge.rs index 8d3ed99c..8a854730 100644 --- a/src/android/fd_bridge.rs +++ b/src/android/fd_bridge.rs @@ -77,20 +77,21 @@ fn map_exception(env: &mut JNIEnv, fallback: &str) -> Result<(), Error> { } #[cfg(target_os = "android")] -fn cache(env: &mut JNIEnv) -> Result<&'static FdJniCache, Error> { - if let Some(c) = CACHE.get() { - return Ok(c); - } - let class = env - .find_class("app/tauri/serialplugin/UsbNative") - .map_err(|e| Error::new(e.to_string()))?; - let global = env - .new_global_ref(class) - .map_err(|e| Error::new(e.to_string()))?; - let _ = CACHE.set(FdJniCache { class: global }); +fn cache(_env: &mut JNIEnv) -> Result<&'static FdJniCache, Error> { CACHE.get().ok_or_else(not_init) } +/// Called from `UsbNative.nativeInit` on a Java thread, where the app class loader is in scope. +#[cfg(target_os = "android")] +pub fn init_class(env: &mut JNIEnv, class: &JObject) { + if CACHE.get().is_some() { + return; + } + if let Ok(global) = env.new_global_ref(class) { + let _ = CACHE.set(FdJniCache { class: global }); + } +} + #[cfg(target_os = "android")] pub fn init_java_vm(vm: JavaVM) { let _ = JVM.set(vm); diff --git a/src/android/jni.rs b/src/android/jni.rs index 383c7610..3dcc3694 100644 --- a/src/android/jni.rs +++ b/src/android/jni.rs @@ -32,9 +32,12 @@ pub extern "system" fn Java_app_tauri_serialplugin_MobileBridge_onDeviceDetached #[cfg(target_os = "android")] #[no_mangle] pub extern "system" fn Java_app_tauri_serialplugin_UsbNative_nativeInit( - env: JNIEnv, - _class: JClass, + mut env: JNIEnv, + class: JClass, ) { + // `class` is UsbNative itself. Cache it here: FindClass on a Rust-attached + // thread uses the system class loader and cannot see app classes. + crate::android::fd_bridge::init_class(&mut env, &class); if let Ok(vm) = env.get_java_vm() { crate::android::fd_bridge::init_java_vm(vm); }