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;