From fa786bafcff2d0f941e2c108fd578916ba85c36f Mon Sep 17 00:00:00 2001 From: funman300 Date: Mon, 8 Jun 2026 11:05:23 -0700 Subject: [PATCH] feat(android): wire Android Keystore JNI via OnceLock Remove the dependency on bevy::android::ANDROID_APP inside android_keystore.rs. Instead, solitaire_data owns a process-wide OnceLock initialised by a new pub fn init_android_jvm(). solitaire_app calls it from android_main before run() so JNI is ready before any auth-token operation can execute. - android_keystore: drop ANDROID_APP import; add ANDROID_JVM OnceLock and init_android_jvm(vm_ptr: *mut c_void) - solitaire_data/lib.rs: re-export init_android_jvm for android target - auth_tokens.rs: update doc comment (Android backend is now complete) - solitaire_app/lib.rs: call init_android_jvm from android_main Co-Authored-By: Claude Sonnet 4.6 --- solitaire_app/src/lib.rs | 11 +++++--- solitaire_data/src/android_keystore.rs | 35 +++++++++++++++++++++----- solitaire_data/src/auth_tokens.rs | 10 +++----- solitaire_data/src/lib.rs | 2 ++ 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/solitaire_app/src/lib.rs b/solitaire_app/src/lib.rs index bcea166..0082814 100644 --- a/solitaire_app/src/lib.rs +++ b/solitaire_app/src/lib.rs @@ -65,10 +65,9 @@ pub fn run() { // operations will fail gracefully with TokenError::KeychainUnavailable. // // Android: `keyring` isn't compiled in (its `rpassword` transitive - // pulls a libc symbol Android's bionic doesn't expose). `auth_tokens` - // ships an Android stub that returns KeychainUnavailable for every - // call — the runtime behaviour is "session login required each launch" - // until we wire Android Keystore via JNI in the Phase-Android round. + // pulls a libc symbol Android's bionic doesn't expose). The Android + // auth-token path uses Android Keystore via JNI; `android_main` passes + // the process JavaVM pointer into `solitaire_data` before `run()`. #[cfg(not(target_os = "android"))] if let Err(e) = keyring::use_native_store(true) { eprintln!( @@ -366,6 +365,10 @@ fn set_window_icon( #[cfg(target_os = "android")] #[unsafe(no_mangle)] fn android_main(android_app: bevy::android::android_activity::AndroidApp) { + let vm_ptr = android_app.vm_as_ptr().cast(); + if let Err(e) = solitaire_data::init_android_jvm(vm_ptr) { + eprintln!("warn: could not initialise Android Keystore JNI ({e})"); + } let _ = bevy::android::ANDROID_APP.set(android_app); run(); } diff --git a/solitaire_data/src/android_keystore.rs b/solitaire_data/src/android_keystore.rs index 3f16896..05de32a 100644 --- a/solitaire_data/src/android_keystore.rs +++ b/solitaire_data/src/android_keystore.rs @@ -19,11 +19,14 @@ use jni::{ }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::ffi::c_void; use std::path::PathBuf; +use std::sync::OnceLock; use crate::auth_tokens::TokenError; const KEY_ALIAS: &str = "ferrous_solitaire_token_key"; +static ANDROID_JVM: OnceLock = OnceLock::new(); #[derive(Serialize, Deserialize)] struct TokenBlob { @@ -36,17 +39,37 @@ struct TokenBlob { // JVM helper // --------------------------------------------------------------------------- +/// Initialise Android Keystore access with the process-wide `JavaVM*`. +/// +/// This is called by `solitaire_app` from Android startup code. Keeping the +/// raw JVM pointer here avoids making `solitaire_data` depend on the app or +/// engine layer just to reach platform startup state. +pub fn init_android_jvm(vm_ptr: *mut c_void) -> Result<(), TokenError> { + if vm_ptr.is_null() { + return Err(TokenError::KeychainUnavailable( + "JavaVM pointer is null".into(), + )); + } + if ANDROID_JVM.get().is_some() { + return Ok(()); + } + + // SAFETY: `vm_ptr` is supplied by Android startup code and must be the + // process-wide JavaVM* for this app. `OnceLock` keeps the wrapper alive for + // the process lifetime. + let vm = unsafe { JavaVM::from_raw(vm_ptr.cast()) } + .map_err(|e| TokenError::Keyring(format!("JavaVM: {e}")))?; + let _ = ANDROID_JVM.set(vm); + Ok(()) +} + fn with_jvm(f: F) -> Result where F: for<'env> FnOnce(&mut JNIEnv<'env>) -> Result, { - let app = bevy::android::ANDROID_APP + let vm = ANDROID_JVM .get() - .ok_or_else(|| TokenError::KeychainUnavailable("ANDROID_APP not initialised".into()))?; - - // SAFETY: vm_as_ptr() is the process-wide JavaVM* set by the Android runtime. - let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) } - .map_err(|e| TokenError::Keyring(format!("JavaVM: {e}")))?; + .ok_or_else(|| TokenError::KeychainUnavailable("Android JavaVM not initialised".into()))?; let mut env = vm .attach_current_thread_permanently() diff --git a/solitaire_data/src/auth_tokens.rs b/solitaire_data/src/auth_tokens.rs index b9ff173..6e0402d 100644 --- a/solitaire_data/src/auth_tokens.rs +++ b/solitaire_data/src/auth_tokens.rs @@ -14,15 +14,13 @@ //! the Bevy `App`). If no default store is set, all operations in this module //! will return [`TokenError::KeychainUnavailable`]. //! -//! # Android stub +//! # Android //! //! `keyring-core` cannot compile for the android target (its `rpassword` //! transitive dep uses `libc::__errno_location`, which Android's bionic -//! doesn't expose). On Android every function in this module returns -//! [`TokenError::KeychainUnavailable`] so callers can detect the fallback -//! the same way they handle a Linux box without Secret Service. The -//! real Android backend will arrive in the Phase-Android round when we -//! wire Android Keystore via JNI. +//! doesn't expose). On Android this module delegates to an Android Keystore +//! JNI backend. `solitaire_app` must call `solitaire_data::init_android_jvm` +//! from Android startup before token operations can succeed. //! //! # Note: no unit tests — requires live OS keychain. diff --git a/solitaire_data/src/lib.rs b/solitaire_data/src/lib.rs index fdae73d..a5c4dd2 100644 --- a/solitaire_data/src/lib.rs +++ b/solitaire_data/src/lib.rs @@ -145,6 +145,8 @@ pub use settings::{ #[cfg(target_os = "android")] mod android_keystore; +#[cfg(target_os = "android")] +pub use android_keystore::init_android_jvm; #[cfg(not(target_arch = "wasm32"))] pub mod auth_tokens;