| Let the media plane cross to the phone, camera and all fd0e21a nandi 18d ago | 1 | //! The JavaVM and the Activity, handed across from `libvidya.so`. |
| 2 | //! |
| 3 | //! On a desktop this library needs nothing from the host: it opens V4L2 nodes |
| 4 | //! and PipeWire streams by itself. On the phone the camera is Java, and every |
| 5 | //! call into it needs the process's `JavaVM` and the running `Activity`. |
| 6 | //! |
| 7 | //! Neither is reachable from here. `android-activity`'s glue receives them, and |
| 8 | //! the glue runs in `libvidya.so` — a different shared object, because whoever |
| 9 | //! holds the glue owns the event loop and that is the UI's job, not this one's. |
| 10 | //! The usual way across, `ndk_context`, does not answer: its handles live in a |
| 11 | //! `static` inside the `ndk-context` crate, and a `static` is per-object. Two |
| 12 | //! cdylibs that both link it get two of them, and the one this library would |
| 13 | //! read is the one nobody ever set. |
| 14 | //! |
| 15 | //! So the handles are pushed rather than pulled. `libjoltapp.so` links both |
| 16 | //! objects, reads them out of libvidya's C ABI, and passes them to |
| 17 | //! [`crate::joltmoq_android_init`] before any call starts. Everything here is |
| 18 | //! `None` until that has happened, and a camera call fails with a reason rather |
| 19 | //! than dereferencing a null. |
| 20 | |
| 21 | use std::ptr; |
| 22 | use std::sync::atomic::{AtomicPtr, Ordering}; |
| 23 | |
| 24 | use jni::objects::{JClass, JObject, JValue}; |
| 25 | use jni::{jni_sig, jni_str, JavaVM}; |
| 26 | |
| 27 | /// Set once, before the application starts. Read from every thread that talks |
| 28 | /// to Camera2 — the tokio workers among them, which is why this is not a |
| 29 | /// `Mutex<Option<_>>` anyone could hold across a JNI call. |
| 30 | static VM: AtomicPtr<jni::sys::JavaVM> = AtomicPtr::new(ptr::null_mut()); |
| 31 | |
| 32 | /// The Activity, as the glue's own global reference. Not a local ref that would |
| 33 | /// die with the frame that made it: libvidya holds this one for the lifetime of |
| 34 | /// the process, so it is as good here as it is there. |
| 35 | static ACTIVITY: AtomicPtr<jni::sys::_jobject> = AtomicPtr::new(ptr::null_mut()); |
| 36 | |
| 37 | /// Store the handles. Called once, from the glue, before any session opens. |
| 38 | /// |
| 39 | /// # Safety |
| 40 | /// |
| 41 | /// `vm` must be the process's `JavaVM` and `activity` a global reference to the |
| 42 | /// running Activity, both still live — which is what libvidya's accessors |
| 43 | /// return for as long as the process runs. |
| 44 | pub unsafe fn set(vm: *mut jni::sys::JavaVM, activity: jni::sys::jobject) { |
| 45 | VM.store(vm, Ordering::Release); |
| 46 | ACTIVITY.store(activity, Ordering::Release); |
| 47 | |
| 48 | // And the same pair again, into this object's `ndk_context`. |
| 49 | // |
| 50 | // Not for anything here: cpal's AAudio host asks `ndk_context` for the VM |
| 51 | // when it opens a stream, and the copy it asks is the one linked into |
| 52 | // *this* library, which nothing has ever filled in. libvidya's glue |
| 53 | // initialised libvidya's copy, and that is a different static — the same |
| 54 | // reason this module exists at all, arriving a second time from underneath. |
| 55 | // |
| 56 | // Unset, it is not a fallback but a panic: `android context was not |
| 57 | // initialized`, on the audio thread, the moment a call starts. |
| 58 | // |
| 59 | // SAFETY: the caller's contract, and the same values stored above. |
| 60 | unsafe { ndk_context::initialize_android_context(vm.cast(), activity.cast()) }; |
| 61 | } |
| 62 | |
| 63 | /// The `JavaVM`, or `None` before the glue has handed it over. |
| 64 | pub fn vm() -> Option<JavaVM> { |
| 65 | let ptr = VM.load(Ordering::Acquire); |
| 66 | if ptr.is_null() { |
| 67 | return None; |
| 68 | } |
| 69 | // SAFETY: non-null only after `set`, whose contract is that this is the |
| 70 | // process's own VM. A JavaVM outlives every thread that could read it. |
| 71 | Some(unsafe { JavaVM::from_raw(ptr) }) |
| 72 | } |
| 73 | |
| 74 | /// The Activity's `jobject`, or `None` before the glue has handed it over. |
| 75 | pub fn activity() -> Option<jni::sys::jobject> { |
| 76 | let ptr = ACTIVITY.load(Ordering::Acquire); |
| 77 | if ptr.is_null() { |
| 78 | None |
| 79 | } else { |
| 80 | Some(ptr) |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | /// Load an **application** class (APK `classes.dex`) by binary name. |
| 85 | /// |
| 86 | /// `Env::find_class` on a thread this library made — a tokio worker, the media |
| 87 | /// pump — resolves against the *system* class loader, which knows nothing of |
| 88 | /// the APK. `uk.nandi.frq.CameraCapture` is not there and never will be. The |
| 89 | /// Activity's own loader is the one that can see it. |
| 90 | /// |
| 91 | /// `binary_name` is the Java binary name: dots, not slashes. |
| 92 | pub fn load_app_class<'a>( |
| 93 | env: &mut jni::Env<'a>, |
| 94 | activity: &JObject<'_>, |
| 95 | binary_name: &str, |
| 96 | ) -> Result<JClass<'a>, String> { |
| 97 | let loader = env |
| 98 | .call_method( |
| 99 | activity, |
| 100 | jni_str!("getClassLoader"), |
| 101 | jni_sig!(() -> java.lang.ClassLoader), |
| 102 | &[], |
| 103 | ) |
| 104 | .map_err(|e| format!("getClassLoader: {e}"))? |
| 105 | .l() |
| 106 | .map_err(|e| format!("getClassLoader: {e}"))?; |
| 107 | if loader.is_null() { |
| 108 | return Err("Activity.getClassLoader returned null".into()); |
| 109 | } |
| 110 | |
| 111 | let class_name = env |
| 112 | .new_string(binary_name) |
| 113 | .map_err(|e| format!("new_string({binary_name}): {e}"))?; |
| 114 | let loaded = env |
| 115 | .call_method( |
| 116 | &loader, |
| 117 | jni_str!("loadClass"), |
| 118 | jni_sig!((java.lang.String) -> java.lang.Class), |
| 119 | &[JValue::Object(class_name.as_ref())], |
| 120 | ) |
| 121 | .map_err(|e| format!("loadClass({binary_name}): {e}"))? |
| 122 | .l() |
| 123 | .map_err(|e| format!("loadClass({binary_name}): {e}"))?; |
| 124 | if loaded.is_null() { |
| 125 | return Err(format!("loadClass({binary_name}) returned null")); |
| 126 | } |
| 127 | env.cast_local::<JClass>(loaded) |
| 128 | .map_err(|e| format!("cast {binary_name}: {e}")) |
| 129 | } |