//! Safe JNI bridge for Android platform integration. //! //! Reconstructing the raw `JavaVM` / `NativeActivity` handles handed over by //! the Android runtime requires `unsafe` FFI. That single reconstruction lives //! in `solitaire_app`'s entry point ([`solitaire_app::android_main`]); this //! module only ever stores the resulting *safe* [`JavaVM`] and activity //! [`GlobalRef`] and hands callers an attached [`JNIEnv`]. //! //! As a result every consumer of Android JNI — the keystore, clipboard, and //! safe-area subsystems — goes through the safe functions here and stays //! `forbid(unsafe_code)`. The only crate carrying `unsafe` is `solitaire_app`. //! //! Only compiled and linked on `target_os = "android"`. use jni::objects::{GlobalRef, JObject}; use jni::{JNIEnv, JavaVM}; use std::sync::OnceLock; static ANDROID_JVM: OnceLock = OnceLock::new(); static ANDROID_ACTIVITY: OnceLock = OnceLock::new(); /// Store the process-wide [`JavaVM`]. Called once from Android startup /// (`solitaire_app::android_main`); subsequent calls are ignored. pub fn set_jvm(vm: JavaVM) { let _ = ANDROID_JVM.set(vm); } /// Store a global reference to the `NativeActivity`. Called once from Android /// startup; subsequent calls are ignored. pub fn set_activity(activity: GlobalRef) { let _ = ANDROID_ACTIVITY.set(activity); } /// Run `f` with a [`JNIEnv`] attached to the current thread. /// /// Returns an error string if the bridge has not been initialised yet or the /// thread cannot be attached. The closure's JNI errors are surfaced through the /// same `String` channel so callers have a single error type to map. pub fn with_env(f: F) -> Result where F: for<'local> FnOnce(&mut JNIEnv<'local>) -> jni::errors::Result, { let vm = ANDROID_JVM .get() .ok_or_else(|| "Android JavaVM not initialised".to_string())?; let mut env = vm .attach_current_thread_permanently() .map_err(|e| format!("attach_current_thread: {e}"))?; f(&mut env).map_err(|e| format!("JNI: {e}")) } /// Run `f` with an attached [`JNIEnv`] and the `NativeActivity` object. /// /// Like [`with_env`] but also resolves the cached activity global reference, so /// callers that need to invoke instance methods on the activity (clipboard, /// window insets) never touch a raw handle. pub fn with_activity_env(f: F) -> Result where F: for<'local> FnOnce(&mut JNIEnv<'local>, &JObject<'local>) -> jni::errors::Result, { let activity = ANDROID_ACTIVITY .get() .ok_or_else(|| "Android activity not initialised".to_string())?; with_env(|env| f(env, activity.as_obj())) }