From 5f31e55d7e676225903273a03f9b9d986c2946e4 Mon Sep 17 00:00:00 2001 From: DepengWang <2818245+DepengWang@users.noreply.github.com> Date: Sun, 4 Oct 2026 12:57:01 +0800 Subject: [PATCH 1/2] fix(android): resolve the credential path from the cached data dir android_credentials_path() asked the JVM for Context.getFilesDir() on every call, through Tao's Activity registry. That registry only holds an Activity while it is in the foreground, so the lookup fails with "Tao Android context not yet initialized" whenever the Tauri Activity is backgrounded or finished, which is nearly all the time the IME is in use. The in-memory credential cache hid this until something forced a reload. Seen on a OnePlus (CPH2573) with encrypted sync signed in: - A sync restore started right after a dictation while no Activity was in the foreground. Four seconds later the vault logged credential read failed: resolve Android credential directory: Tao Android context not yet initialized - The restore stayed pending (pendingRestore in generation.json, the restore-*.bin left behind), and the sync write gate rejects every write while a restore is pending. - From then on the splash marker could not be saved, so the startup animation replayed on every settings open ("failed to persist splash marker: recovery_required"), and tapping the mic did nothing: starting a dictation failed at the credential read, and that warning is de-duplicated, so nothing was logged or shown. - The process stayed alive in this state for 25 hours without a crash. Use android_storage::android_data_dir(), which is resolved once at startup and cached; it is the same {filesDir}/OpenLess directory. With this change the pending restore on that phone completed by itself on the next process start and dictation worked again. Co-Authored-By: Claude Opus 5.5 --- .../app/src-tauri/src/persistence/credentials.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openless-all/app/src-tauri/src/persistence/credentials.rs b/openless-all/app/src-tauri/src/persistence/credentials.rs index 5ef7ef794..35babe4f8 100644 --- a/openless-all/app/src-tauri/src/persistence/credentials.rs +++ b/openless-all/app/src-tauri/src/persistence/credentials.rs @@ -1470,11 +1470,14 @@ fn keyring_entry_for(account: &str) -> Result { #[cfg(target_os = "android")] fn android_credentials_path() -> Result { - let files_dir = crate::android::jni::android::app_files_dir() - .map_err(|error| anyhow::anyhow!("resolve Android credential directory: {error}"))?; - Ok(PathBuf::from(files_dir) - .join("OpenLess") - .join(ANDROID_CREDENTIALS_FILE)) + // Use the directory cached at startup. A live lookup goes through Tao's + // Activity registry, which is empty whenever the Tauri Activity is not in the + // foreground — i.e. for nearly all IME use. A sync restore that touched the + // vault in that state failed, stayed pending, and left the write gate + // rejecting every later write with `recovery_required`. + let data_dir = super::android_storage::android_data_dir() + .context("resolve Android credential directory")?; + Ok(data_dir.join(ANDROID_CREDENTIALS_FILE)) } #[cfg(target_os = "android")] From 0806e16ed138c86ccc1a72df55bf7561b2627e33 Mon Sep 17 00:00:00 2001 From: DepengWang <2818245+DepengWang@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:39:11 +0800 Subject: [PATCH 2/2] fix(android): cache the files dir in the JNI helper instead The previous commit made android_credentials_path() use android_storage::android_data_dir(). That cache may have been filled from TAURI_ANDROID_APP_DATA_DIR when the JNI lookup failed, which the credential-keystore contract test rightly rejects: the vault must only ever live under the JNI-resolved Context.getFilesDir(), never under an environment or temporary path. Restore android_credentials_path() and fix the lookup itself instead. app_files_dir() now: - caches the directory once resolved (it is fixed for the life of the process), and - falls back to the Context registered by the Application / runtime service when Tao has no foreground Activity. That Context returns the same getFilesDir() and stays valid during IME use. The vault path is still derived only from getFilesDir(), and every other caller of app_files_dir() gets the same robustness. Co-Authored-By: Claude Opus 5.5 --- openless-all/app/src-tauri/src/android/jni.rs | 28 ++++++++++++++++--- .../src-tauri/src/persistence/credentials.rs | 13 ++++----- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/openless-all/app/src-tauri/src/android/jni.rs b/openless-all/app/src-tauri/src/android/jni.rs index cd8b2712a..52afe475e 100644 --- a/openless-all/app/src-tauri/src/android/jni.rs +++ b/openless-all/app/src-tauri/src/android/jni.rs @@ -192,9 +192,21 @@ pub mod android { /// Returns the app-private files directory supplied by Android's Context. pub(crate) fn app_files_dir() -> Result { - // Persistence initializes before mobile_runtime::setup initializes - // ndk-context, so use Tao's non-panicking activity registry here. - with_tao_android_env(|env, context| { + // The directory is fixed for the life of the process, so resolve it once. + // Tao only keeps an Activity registered while it is in the foreground: + // without this cache and the fallback below, every lookup failed while + // the IME ran with no Activity visible. That made the credential vault + // unreadable in the middle of a sync restore, which then stayed pending + // and left the write gate rejecting every later write. + static FILES_DIR: std::sync::OnceLock = std::sync::OnceLock::new(); + if let Some(path) = FILES_DIR.get() { + return Ok(path.clone()); + } + + fn files_dir<'local>( + env: &mut JNIEnv<'local>, + context: &JObject<'local>, + ) -> Result { let directory = env .call_method(context, "getFilesDir", "()Ljava/io/File;", &[]) .and_then(|value| value.l()) @@ -218,7 +230,15 @@ pub mod android { return Err("Context files directory is empty".to_string()); } Ok(path) - }) + } + + // Persistence initializes before mobile_runtime::setup initializes + // ndk-context, so use Tao's non-panicking activity registry first. The + // Context registered by the Application / runtime service returns the + // same directory and stays valid with no Activity in the foreground. + let path = with_tao_android_env(files_dir) + .or_else(|tao_error| with_android_env(files_dir).map_err(|_| tao_error))?; + Ok(FILES_DIR.get_or_init(|| path).clone()) } /// Returns the app-private cache directory supplied by Android's Context. diff --git a/openless-all/app/src-tauri/src/persistence/credentials.rs b/openless-all/app/src-tauri/src/persistence/credentials.rs index 35babe4f8..5ef7ef794 100644 --- a/openless-all/app/src-tauri/src/persistence/credentials.rs +++ b/openless-all/app/src-tauri/src/persistence/credentials.rs @@ -1470,14 +1470,11 @@ fn keyring_entry_for(account: &str) -> Result { #[cfg(target_os = "android")] fn android_credentials_path() -> Result { - // Use the directory cached at startup. A live lookup goes through Tao's - // Activity registry, which is empty whenever the Tauri Activity is not in the - // foreground — i.e. for nearly all IME use. A sync restore that touched the - // vault in that state failed, stayed pending, and left the write gate - // rejecting every later write with `recovery_required`. - let data_dir = super::android_storage::android_data_dir() - .context("resolve Android credential directory")?; - Ok(data_dir.join(ANDROID_CREDENTIALS_FILE)) + let files_dir = crate::android::jni::android::app_files_dir() + .map_err(|error| anyhow::anyhow!("resolve Android credential directory: {error}"))?; + Ok(PathBuf::from(files_dir) + .join("OpenLess") + .join(ANDROID_CREDENTIALS_FILE)) } #[cfg(target_os = "android")]