Skip to content

fix(android): keep the app files dir resolvable without a foreground Activity - #1145

Open
DepengWang wants to merge 2 commits into
Open-Less:betafrom
DepengWang:fix/android-credential-dir
Open

DepengWang wants to merge 2 commits into
Open-Less:betafrom
DepengWang:fix/android-credential-dir

Conversation

@DepengWang

@DepengWang DepengWang commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On Android, the credential vault resolved its file path with a live JNI lookup that only works while the Tauri Activity is in the foreground. When encrypted sync ran a restore while the IME was in use and no Activity was visible, the restore failed half-way and stayed pending. The sync write gate then rejected every later write, and the IME stopped working with no visible error until the process was restarted.

This PR makes the JNI helper app_files_dir() cache its result and fall back to the always-registered Context when no Activity is in the foreground.

What happened (OnePlus CPH2573, encrypted sync signed in)

Taken from the on-device openless.log and app state; the process stayed alive for the whole period, no crash.

  • 11:15:58 sync connects right after a dictation and starts a restore (pendingRestore written to encrypted-sync/generation.json, restore-*.bin created).
  • 11:16:02 [vault] credential read failed: resolve Android credential directory: Tao Android context not yet initialized
  • The restore never completes. SyncWriteGate::begin_mutation returns RecoveryRequired while pending_restore is set.
  • For the next 25 hours:
    • every time settings is opened: [splash] failed to persist splash marker: recovery_required, so the startup animation replays each time;
    • tapping the mic in the IME does nothing. The Kotlin readiness checks pass (no mictap / heartbeat counters), the start command reaches Rust, and it fails at the credential read. That warning is de-duplicated against the previous one, so nothing is logged at the tap and the user sees no error.
  • Provider tests run from the settings screen during that time succeed, because the Activity is in the foreground then.

Cause

android_credentials_path() calls jni::app_files_dir() on every use. That helper went only through tao::platform::android::prelude::main_android_context(), and the comment at the top of android/jni.rs already notes that Tao only keeps an Activity in that map while it is resumed.

Introduced with the Keystore-backed vault in #838; it becomes reachable once something reloads credentials while no Activity is in the foreground, which a sync restore does.

Change

app_files_dir() (in android/jni.rs) now:

  • caches the directory in a OnceLock once resolved; it cannot change during the life of the process;
  • still tries Tao's registry first (needed during early startup), and falls back to the Context registered through nativeRegisterActivityContext (the Application at startup, then the runtime service). That Context returns the same getFilesDir() and stays valid while only the IME is running.

android_credentials_path() is unchanged: the vault path is still derived only from the JNI getFilesDir(), never from TAURI_ANDROID_APP_DATA_DIR or a temp directory.

Note on the two commits

The first commit pointed the vault at android_storage::android_data_dir(). That cache can be filled from the environment variable when JNI fails, so it would have weakened the "no environment or temporary fallback" rule that android-credential-keystore-contract.test.mjs enforces; CI caught it. The second commit reverts that and fixes the lookup itself. Happy to squash.

Testing

  • android-credential-keystore-contract.test.mjs passes; all of scripts/*.test.mjs: 53 of 56 pass, and the 3 failures (android-apk-workflow-contract, ci-cache-usage, ci-changed-areas) fail identically on an untouched upstream/beta checkout on this machine.
  • cargo check --locked --target aarch64-linux-android on this branch; host cargo check --locked; rustfmt --check on the touched file.
  • The diagnosis and the effect of removing the failing lookup were verified on the OnePlus with the first commit's approach: on the next process start the pending restore completed by itself within about two minutes (pendingRestore gone, restore-*.bin removed, generation 267 -> 270), three dictations ran normally, and later settings opens no longer logged the splash-marker failure.

Not tested:

  • The final version (second commit) has been compiled for Android but not yet run on a device.
  • No automated regression test: reproducing needs a sync restore with the Activity backgrounded.

Not addressed here

  • A restore that fails half-way for any other reason still leaves the gate blocked with nothing shown to the user; only a process restart retries the recovery. On the same phone the Keystore reported temporarily_unavailable several times during a cold start, which could hit the same window.
  • The de-duplicated vault warning makes a repeating failure look like a single old one in the log.

🤖 Generated with Claude Code

DepengWang and others added 2 commits October 4, 2026 12:57
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@DepengWang DepengWang changed the title fix(android): resolve the credential path from the cached data dir fix(android): keep the app files dir resolvable without a foreground Activity Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant