1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
//! 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<Option<_>>` anyone could hold across a JNI call.
static VM: AtomicPtr<jni::sys::JavaVM> = 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<jni::sys::_jobject> = 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<JavaVM> {
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<jni::sys::jobject> {
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<JClass<'a>, 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::<JClass>(loaded)
.map_err(|e| format!("cast {binary_name}: {e}"))
}
|