//! Android-specific JNI lifecycle helpers. //! //! On Android the Flutter engine starts our cdylib but does not push //! anything into `ndk_context` (the global the Android Oboe backend //! reads to find Android audio services). Without that, the first //! audio call hangs / fails. //! //! We solve this two ways: //! //! 1. `JNI_OnLoad` captures the `JavaVM*` as soon as the library is //! loaded. //! 2. We expose a `Java_app_chanora_chanora_1flutter_MainActivity_initChanoraContext` //! JNI function the Kotlin `MainActivity.onCreate` calls with its //! application Context. That JNI function pushes both the //! `JavaVM*` and a `Context` global ref into `ndk_context`. //! //! After that, the Android Oboe backend can open the default //! input/output devices. #![cfg(target_os = "android")] use std::sync::Once; use jni::objects::{JClass, JObject}; use jni::sys::{jint, JNI_VERSION_1_6}; use jni::JNIEnv; use log::{error, info}; /// Stash the JavaVM pointer between JNI_OnLoad and initContext. static mut JAVA_VM: *mut std::ffi::c_void = std::ptr::null_mut(); static INIT_ONCE: Once = Once::new(); /// JNI entry point invoked by the Android runtime when the cdylib is /// loaded via `System.loadLibrary` (which `flutter_rust_bridge` does /// at `RustLib.init`). #[no_mangle] pub extern "system" fn JNI_OnLoad( vm: *mut std::ffi::c_void, _reserved: *mut std::ffi::c_void, ) -> jint { // Safety: Android guarantees `vm` is a valid JavaVM* for the // lifetime of the library. unsafe { JAVA_VM = vm; } JNI_VERSION_1_6 } /// Called by `MainActivity.onCreate` with the application Context. /// Initialises the `ndk_context` so the Oboe backend can locate /// the Android audio services. /// /// Symbol mangling note: Kotlin / JNI mangles the underscore in /// `chanora_flutter` to `chanora_1flutter` because the literal `_` /// in a JNI symbol means package separator. #[no_mangle] pub extern "system" fn Java_app_chanora_chanora_1flutter_MainActivity_initChanoraContext<'local>( env: JNIEnv<'local>, _class: JClass<'local>, context: JObject<'local>, ) { INIT_ONCE.call_once(|| { let global = match env.new_global_ref(&context) { Ok(g) => g, Err(e) => { error!("initChanoraContext: new_global_ref failed: {e}"); return; } }; let raw_ctx = global.as_obj().as_raw() as *mut std::ffi::c_void; // Leak the global ref so it survives for the process lifetime // (ndk_context borrows the pointer). std::mem::forget(global); unsafe { ndk_context::initialize_android_context(JAVA_VM, raw_ctx); } info!( "initChanoraContext: ndk_context initialised (vm={:p}, ctx={:p})", unsafe { JAVA_VM }, raw_ctx ); }); }