From 8b1a374bd129337a76e8745e2dbf671bb0d8c013 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Thu, 24 Sep 2026 10:36:30 +0200 Subject: [PATCH] Keep ndk_context valid across Android activity recreation --- Cargo.lock | 1 + .../gen/app/src/main/AndroidManifest.xml.hbs | 2 +- packages/desktop/Cargo.toml | 1 + packages/desktop/src/mobile.rs | 37 +++++++------ .../manganis/manganis/src/android/activity.rs | 52 +++++++++++-------- 5 files changed, 53 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1dffb9b6a..93e82454d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3501,6 +3501,7 @@ dependencies = [ "jni 0.21.1", "lazy-js-bundle", "libc", + "manganis", "muda", "ndk", "ndk-context", diff --git a/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs b/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs index 469f4b758b..c10f391672 100644 --- a/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs +++ b/packages/cli/assets/android/gen/app/src/main/AndroidManifest.xml.hbs @@ -25,7 +25,7 @@ {{#if uses_cleartext_traffic}}android:usesCleartextTraffic="{{ uses_cleartext_traffic }}"{{/if}} {{#if large_heap}}android:largeHeap="{{ large_heap }}"{{/if}} android:networkSecurityConfig="@xml/network_security_config"> - diff --git a/packages/desktop/Cargo.toml b/packages/desktop/Cargo.toml index 729c0f91ef..213e710ddc 100644 --- a/packages/desktop/Cargo.toml +++ b/packages/desktop/Cargo.toml @@ -82,6 +82,7 @@ jni = "0.21.1" ndk = { version = "0.9.0" } ndk-sys = { version = "0.6.0" } ndk-context = { version = "0.1.1" } +manganis = { workspace = true } # use native tls on other platforms [target.'cfg(not(target_os = "android"))'.dependencies] diff --git a/packages/desktop/src/mobile.rs b/packages/desktop/src/mobile.rs index 2bea03e6eb..499384c9bd 100644 --- a/packages/desktop/src/mobile.rs +++ b/packages/desktop/src/mobile.rs @@ -18,34 +18,39 @@ pub extern "C" fn start_app() { use std::any::Any; // tao 0.35 dropped its automatic `ndk_context::initialize_android_context` call - // (see https://github.com/tauri-apps/tao/issues/1220). Many android-aware crates — - // including parts of wry itself — call `ndk_context::android_context()` and panic if - // it's uninitialized, which then poisons wry's static mutexes and turns the original - // panic into a confusing `PoisonError` at the next JNI callback. Initialize it here - // before handing off to wry's own setup. - // - // Guarded by `Once` because `WryActivity.onCreate` (and therefore this setup) runs - // again on activity re-creation — rotation, theme changes, back/foreground cycles — - // and `ndk_context::initialize_android_context` asserts `previous.is_none()`, which - // would abort the process on every re-entry. The global only needs the JavaVM + an - // activity-like Context pointer for consumers to attach a JNI thread; we don't need - // to refresh it per-activity. + // (see https://github.com/tauri-apps/tao/issues/1220), and many android-aware crates panic + // without it. It gets the Application context because activities are recreated while the + // process lives. unsafe fn android_setup( package: &str, - env: ::wry::prelude::JNIEnv<'_>, + mut env: ::wry::prelude::JNIEnv<'_>, looper: &::ndk::looper::ThreadLooper, activity: ::wry::prelude::GlobalRef, ) { - static NDK_CONTEXT_INIT: std::sync::Once = std::sync::Once::new(); - NDK_CONTEXT_INIT.call_once(|| { + static APPLICATION: std::sync::OnceLock<::wry::prelude::GlobalRef> = + std::sync::OnceLock::new(); + APPLICATION.get_or_init(|| { + let application = env + .call_method( + activity.as_obj(), + "getApplicationContext", + "()Landroid/content/Context;", + &[], + ) + .and_then(|context| context.l()) + .and_then(|context| env.new_global_ref(context)) + .expect("an activity has an Application context"); let vm = env.get_java_vm().unwrap(); + // SAFETY: `APPLICATION` keeps this global reference alive for the rest of the process. unsafe { ::ndk_context::initialize_android_context( vm.get_java_vm_pointer() as *mut _, - activity.as_obj().as_raw() as *mut _, + application.as_obj().as_raw() as *mut _, ); } + application }); + ::manganis::android::set_current_activity(activity.clone()); unsafe { wry::android_setup(package, env, looper, activity); } diff --git a/packages/manganis/manganis/src/android/activity.rs b/packages/manganis/manganis/src/android/activity.rs index 8216065e19..598a5b0f7a 100644 --- a/packages/manganis/manganis/src/android/activity.rs +++ b/packages/manganis/manganis/src/android/activity.rs @@ -1,15 +1,24 @@ -use jni::{JNIEnv, JavaVM, objects::JObject}; -use std::sync::OnceLock; +use jni::{ + JNIEnv, JavaVM, + objects::{GlobalRef, JObject}, +}; +use std::sync::{OnceLock, RwLock}; -/// Cached reference to the Android activity. -static ACTIVITY: OnceLock = OnceLock::new(); +/// The live Android activity, replaced whenever the system recreates it. +static CURRENT_ACTIVITY: RwLock> = RwLock::new(None); static JAVA_VM: OnceLock = OnceLock::new(); -/// Execute a JNI operation with a cached activity reference. +/// Make `activity` the one [`with_activity`] hands out, called by the renderer for every new activity. +pub fn set_current_activity(activity: GlobalRef) { + *CURRENT_ACTIVITY + .write() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(activity); +} + +/// Execute a JNI operation with the current activity. /// -/// This function handles the boilerplate of getting the JavaVM and Activity -/// references, caching them for subsequent calls. It's the foundation for -/// most Android mobile API operations. +/// Without an activity registered through [`set_current_activity`], the closure receives the +/// `ndk_context` context. /// /// # Arguments /// @@ -44,20 +53,17 @@ where }; let mut env = vm.attach_current_thread().ok()?; - let activity = if let Some(activity) = ACTIVITY.get() { - activity - } else { - let raw_activity = unsafe { JObject::from_raw(ctx.context() as jni::sys::jobject) }; - let global = env.new_global_ref(&raw_activity).ok()?; - match ACTIVITY.set(global) { - Ok(()) => ACTIVITY.get().unwrap(), - Err(global) => { - drop(global); - ACTIVITY.get()? - } + // The clone keeps the reference alive while `f` runs, even if the activity is replaced meanwhile. + let activity = CURRENT_ACTIVITY + .read() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .clone(); + match activity { + Some(activity) => f(&mut env, activity.as_obj()), + None => { + // SAFETY: `ndk_context` holds this global reference for as long as it is initialised. + let context = unsafe { JObject::from_raw(ctx.context() as jni::sys::jobject) }; + f(&mut env, &context) } - }; - - let activity_obj = activity.as_obj(); - f(&mut env, &activity_obj) + } }