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.
|
// operations will fail gracefully with TokenError::KeychainUnavailable.
|
||||||
//
|
//
|
||||||
// Android: `keyring` isn't compiled in (its `rpassword` transitive
|
// Android: `keyring` isn't compiled in (its `rpassword` transitive
|
||||||
// pulls a libc symbol Android's bionic doesn't expose). `auth_tokens`
|
// pulls a libc symbol Android's bionic doesn't expose). The Android
|
||||||
// ships an Android stub that returns KeychainUnavailable for every
|
// auth-token path uses Android Keystore via JNI; `android_main` passes
|
||||||
// call — the runtime behaviour is "session login required each launch"
|
// the process JavaVM pointer into `solitaire_data` before `run()`.
|
||||||
// until we wire Android Keystore via JNI in the Phase-Android round.
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
if let Err(e) = keyring::use_native_store(true) {
|
if let Err(e) = keyring::use_native_store(true) {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@@ -366,6 +365,10 @@ fn set_window_icon(
|
|||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
fn android_main(android_app: bevy::android::android_activity::AndroidApp) {
|
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);
|
let _ = bevy::android::ANDROID_APP.set(android_app);
|
||||||
run();
|
run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,14 @@ use jni::{
|
|||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::c_void;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use crate::auth_tokens::TokenError;
|
use crate::auth_tokens::TokenError;
|
||||||
|
|
||||||
const KEY_ALIAS: &str = "ferrous_solitaire_token_key";
|
const KEY_ALIAS: &str = "ferrous_solitaire_token_key";
|
||||||
|
static ANDROID_JVM: OnceLock<JavaVM> = OnceLock::new();
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct TokenBlob {
|
struct TokenBlob {
|
||||||
@@ -36,17 +39,37 @@ struct TokenBlob {
|
|||||||
// JVM helper
|
// 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>
|
fn with_jvm<F, R>(f: F) -> Result<R, TokenError>
|
||||||
where
|
where
|
||||||
F: for<'env> FnOnce(&mut JNIEnv<'env>) -> Result<R, jni::errors::Error>,
|
F: for<'env> FnOnce(&mut JNIEnv<'env>) -> Result<R, jni::errors::Error>,
|
||||||
{
|
{
|
||||||
let app = bevy::android::ANDROID_APP
|
let vm = ANDROID_JVM
|
||||||
.get()
|
.get()
|
||||||
.ok_or_else(|| TokenError::KeychainUnavailable("ANDROID_APP not initialised".into()))?;
|
.ok_or_else(|| TokenError::KeychainUnavailable("Android JavaVM 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}")))?;
|
|
||||||
|
|
||||||
let mut env = vm
|
let mut env = vm
|
||||||
.attach_current_thread_permanently()
|
.attach_current_thread_permanently()
|
||||||
|
|||||||
@@ -14,15 +14,13 @@
|
|||||||
//! the Bevy `App`). If no default store is set, all operations in this module
|
//! the Bevy `App`). If no default store is set, all operations in this module
|
||||||
//! will return [`TokenError::KeychainUnavailable`].
|
//! will return [`TokenError::KeychainUnavailable`].
|
||||||
//!
|
//!
|
||||||
//! # Android stub
|
//! # Android
|
||||||
//!
|
//!
|
||||||
//! `keyring-core` cannot compile for the android target (its `rpassword`
|
//! `keyring-core` cannot compile for the android target (its `rpassword`
|
||||||
//! transitive dep uses `libc::__errno_location`, which Android's bionic
|
//! transitive dep uses `libc::__errno_location`, which Android's bionic
|
||||||
//! doesn't expose). On Android every function in this module returns
|
//! doesn't expose). On Android this module delegates to an Android Keystore
|
||||||
//! [`TokenError::KeychainUnavailable`] so callers can detect the fallback
|
//! JNI backend. `solitaire_app` must call `solitaire_data::init_android_jvm`
|
||||||
//! the same way they handle a Linux box without Secret Service. The
|
//! from Android startup before token operations can succeed.
|
||||||
//! real Android backend will arrive in the Phase-Android round when we
|
|
||||||
//! wire Android Keystore via JNI.
|
|
||||||
//!
|
//!
|
||||||
//! # Note: no unit tests — requires live OS keychain.
|
//! # Note: no unit tests — requires live OS keychain.
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ pub use settings::{
|
|||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
mod android_keystore;
|
mod android_keystore;
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub use android_keystore::init_android_jvm;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod auth_tokens;
|
pub mod auth_tokens;
|
||||||
|
|||||||
Reference in New Issue
Block a user