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<JavaVM> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<JavaVM> = 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, R>(f: F) -> Result<R, TokenError>
|
||||
where
|
||||
F: for<'env> FnOnce(&mut JNIEnv<'env>) -> Result<R, jni::errors::Error>,
|
||||
{
|
||||
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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user