//! The JavaVM and the Activity, handed across from `libvidya.so`. //! //! On a desktop this library needs nothing from the host: it opens V4L2 nodes //! and PipeWire streams by itself. On the phone the camera is Java, and every //! call into it needs the process's `JavaVM` and the running `Activity`. //! //! Neither is reachable from here. `android-activity`'s glue receives them, and //! the glue runs in `libvidya.so` — a different shared object, because whoever //! holds the glue owns the event loop and that is the UI's job, not this one's. //! The usual way across, `ndk_context`, does not answer: its handles live in a //! `static` inside the `ndk-context` crate, and a `static` is per-object. Two //! cdylibs that both link it get two of them, and the one this library would //! read is the one nobody ever set. //! //! So the handles are pushed rather than pulled. `libjoltapp.so` links both //! objects, reads them out of libvidya's C ABI, and passes them to //! [`crate::joltmoq_android_init`] before any call starts. Everything here is //! `None` until that has happened, and a camera call fails with a reason rather //! than dereferencing a null. use std::ptr; use std::sync::atomic::{AtomicPtr, Ordering}; use jni::objects::{JClass, JObject, JValue}; use jni::{jni_sig, jni_str, JavaVM}; /// Set once, before the application starts. Read from every thread that talks /// to Camera2 — the tokio workers among them, which is why this is not a /// `Mutex>` anyone could hold across a JNI call. static VM: AtomicPtr = AtomicPtr::new(ptr::null_mut()); /// The Activity, as the glue's own global reference. Not a local ref that would /// die with the frame that made it: libvidya holds this one for the lifetime of /// the process, so it is as good here as it is there. static ACTIVITY: AtomicPtr = AtomicPtr::new(ptr::null_mut()); /// Store the handles. Called once, from the glue, before any session opens. /// /// # Safety /// /// `vm` must be the process's `JavaVM` and `activity` a global reference to the /// running Activity, both still live — which is what libvidya's accessors /// return for as long as the process runs. pub unsafe fn set(vm: *mut jni::sys::JavaVM, activity: jni::sys::jobject) { VM.store(vm, Ordering::Release); ACTIVITY.store(activity, Ordering::Release); // And the same pair again, into this object's `ndk_context`. // // Not for anything here: cpal's AAudio host asks `ndk_context` for the VM // when it opens a stream, and the copy it asks is the one linked into // *this* library, which nothing has ever filled in. libvidya's glue // initialised libvidya's copy, and that is a different static — the same // reason this module exists at all, arriving a second time from underneath. // // Unset, it is not a fallback but a panic: `android context was not // initialized`, on the audio thread, the moment a call starts. // // SAFETY: the caller's contract, and the same values stored above. unsafe { ndk_context::initialize_android_context(vm.cast(), activity.cast()) }; } /// The `JavaVM`, or `None` before the glue has handed it over. pub fn vm() -> Option { let ptr = VM.load(Ordering::Acquire); if ptr.is_null() { return None; } // SAFETY: non-null only after `set`, whose contract is that this is the // process's own VM. A JavaVM outlives every thread that could read it. Some(unsafe { JavaVM::from_raw(ptr) }) } /// The Activity's `jobject`, or `None` before the glue has handed it over. pub fn activity() -> Option { let ptr = ACTIVITY.load(Ordering::Acquire); if ptr.is_null() { None } else { Some(ptr) } } /// Load an **application** class (APK `classes.dex`) by binary name. /// /// `Env::find_class` on a thread this library made — a tokio worker, the media /// pump — resolves against the *system* class loader, which knows nothing of /// the APK. `uk.nandi.frq.CameraCapture` is not there and never will be. The /// Activity's own loader is the one that can see it. /// /// `binary_name` is the Java binary name: dots, not slashes. pub fn load_app_class<'a>( env: &mut jni::Env<'a>, activity: &JObject<'_>, binary_name: &str, ) -> Result, String> { let loader = env .call_method( activity, jni_str!("getClassLoader"), jni_sig!(() -> java.lang.ClassLoader), &[], ) .map_err(|e| format!("getClassLoader: {e}"))? .l() .map_err(|e| format!("getClassLoader: {e}"))?; if loader.is_null() { return Err("Activity.getClassLoader returned null".into()); } let class_name = env .new_string(binary_name) .map_err(|e| format!("new_string({binary_name}): {e}"))?; let loaded = env .call_method( &loader, jni_str!("loadClass"), jni_sig!((java.lang.String) -> java.lang.Class), &[JValue::Object(class_name.as_ref())], ) .map_err(|e| format!("loadClass({binary_name}): {e}"))? .l() .map_err(|e| format!("loadClass({binary_name}): {e}"))?; if loaded.is_null() { return Err(format!("loadClass({binary_name}) returned null")); } env.cast_local::(loaded) .map_err(|e| format!("cast {binary_name}: {e}")) }