//! Publishes the Android JavaVM and application Context into `ndk_context`. //! //! TRACES: UR-012 | DR-223 //! //! # Why this exists //! //! Seven places in this crate (five in `credentials.rs`, two in `lib.rs`) reach //! the JNI environment through [`ndk_context::android_context`], which reads a //! process-global pair of pointers: the `JavaVM` and a `Context` jobject. //! //! Nothing here ever set that global. `tao` did — the windowing layer beneath //! `wry`, several dependencies below anything this project names. tao 0.34.5 //! called `ndk_context::initialize_android_context(...)` while starting the //! Android activity, and our code simply read what it had left behind. //! //! **tao 0.35.3 stopped.** It keeps the same two pointers in a private //! `AndroidContext` struct of its own and no longer publishes them. The moment //! that landed (via the Tauri 2.9.5 → 2.11.5 upgrade), the first credential //! read on Android aborted the process: //! //! ```text //! PANIC at ndk-context/src/lib.rs:72: android context was not initialized //! 8: ndk_context::android_context //! 9: jellytau_lib::run::{{closure}} //! ``` //! //! Not a crash in our code, and not a change to our code: an undocumented side //! effect of a transitive dependency disappeared. The lesson worth keeping is //! that relying on *someone else* to populate a global is a dependency you //! cannot see in `Cargo.toml` and will not be told about when it breaks. //! //! # Why restore the global rather than rewrite the call sites //! //! Threading a VM and Context handle through seven call sites — including the //! credential path — is a larger and riskier change than owning the invariant //! those call sites already depend on. This module makes the assumption true //! instead of removing it, and the seven callers are untouched. //! //! # How //! //! `JNI_OnLoad` gives us the `JavaVM` the instant the shared library loads, //! which is the earliest and most reliable moment available — nothing in the app //! can run before it. It does *not* give us a Context, so that is resolved //! lazily on first use via `ActivityThread.currentApplication()`, by which point //! the Application object certainly exists. //! //! The Context published is the **Application**, not the Activity. That is what //! the consumers want anyway (`SecureStorage.initialize()` immediately calls //! `context.applicationContext`), and it cannot outlive its own lifetime the way //! a retained Activity reference would. use std::ffi::c_void; use std::sync::atomic::{AtomicPtr, Ordering}; use std::sync::OnceLock; use jni::objects::GlobalRef; use jni::sys::{jint, JNI_VERSION_1_6}; use jni::JavaVM; /// The `JavaVM`, captured at library load. static JAVA_VM: AtomicPtr = AtomicPtr::new(std::ptr::null_mut()); /// A global reference to the Application, kept alive for the process lifetime. /// /// `ndk_context` stores a bare pointer and does not own the reference, so the /// `GlobalRef` must outlive every read. A local reference would be freed the /// moment the frame that created it returned, leaving a dangling jobject that /// only misbehaves later. static APP_CONTEXT: OnceLock = OnceLock::new(); /// Whether the `ndk_context` global has been populated. static PUBLISHED: OnceLock = OnceLock::new(); /// Called by the Android runtime when `libjellytau_lib.so` is loaded. /// /// Verified that neither tao, wry nor tauri defines `JNI_OnLoad` in this /// library, so there is nothing to collide with. Returning the JNI version is /// mandatory — returning 0 makes `System.loadLibrary` fail. /// /// TRACES: UR-012 | DR-223 #[no_mangle] pub extern "system" fn JNI_OnLoad(vm: JavaVM, _reserved: *mut c_void) -> jint { JAVA_VM.store(vm.get_java_vm_pointer().cast(), Ordering::SeqCst); // Deliberately no logging here: the logger is not installed this early. JNI_VERSION_1_6 } /// Make [`ndk_context::android_context`] safe to call. /// /// Idempotent and cheap after the first success. Returns an error rather than /// panicking: a failure here means credentials fall back to the encrypted-file /// path, which is a degraded mode the app already supports — far better than /// aborting the process, which is what the missing global did. /// /// TRACES: UR-012 | DR-223 pub fn ensure_initialized() -> Result<(), String> { if PUBLISHED.get().is_some() { return Ok(()); } let vm_ptr = JAVA_VM.load(Ordering::SeqCst); if vm_ptr.is_null() { return Err( "JNI_OnLoad has not run: no JavaVM captured. The library was loaded in an \ unexpected way, or JNI_OnLoad was stripped from the shared object." .to_string(), ); } let vm = unsafe { JavaVM::from_raw(vm_ptr.cast()) } .map_err(|e| format!("failed to adopt the JavaVM pointer: {e}"))?; let mut env = vm .attach_current_thread() .map_err(|e| format!("failed to attach the current thread to the JVM: {e}"))?; // ActivityThread.currentApplication() is the standard way to reach the // Application from native code without being handed a Context. It is a // hidden-but-stable API; it returns null only before the Application is // constructed, which cannot be the case by the time anything here runs. let activity_thread = env .find_class("android/app/ActivityThread") .map_err(|e| format!("android.app.ActivityThread not found: {e}"))?; let application = env .call_static_method( activity_thread, "currentApplication", "()Landroid/app/Application;", &[], ) .map_err(|e| format!("ActivityThread.currentApplication() failed: {e}"))? .l() .map_err(|e| format!("currentApplication() did not return an object: {e}"))?; if application.is_null() { return Err( "ActivityThread.currentApplication() returned null — the Application has not \ been created yet." .to_string(), ); } let global = env .new_global_ref(&application) .map_err(|e| format!("failed to pin the Application as a global reference: {e}"))?; // Store first, publish second: `ndk_context` will hold a bare pointer into // this reference, so it must already be owned somewhere permanent. let stored = APP_CONTEXT.get_or_init(|| global); let context_ptr = stored.as_obj().as_raw().cast::(); unsafe { ndk_context::initialize_android_context(vm_ptr, context_ptr); } let _ = PUBLISHED.set(true); log::info!("[INIT] Android JavaVM and Application published to ndk_context"); Ok(()) } /// Whether the global has been published, for callers that want to degrade /// rather than attempt a JNI call. #[allow(dead_code)] pub fn is_initialized() -> bool { PUBLISHED.get().is_some() }