Skip to content

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

Merged
H-Chris233 merged 3 commits into
Open-Less:betafrom
DepengWang:fix/android-credential-dir
Oct 12, 2026
Merged

H-Chris233 merged 3 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
Keep both the Tao lookup failure and registered Context fallback failure so background credential-path failures remain diagnosable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@HKLHaoBin HKLHaoBin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the JNI context fallback, cache behavior, and credential-path constraints. The implementation correctly handles the no-foreground-Activity case without introducing environment or temporary-path fallbacks.

I also pushed 258e150 to preserve both the Tao lookup error and the registered Context fallback error for field diagnostics. Rust formatting and the Android credential contract test pass.

@DepengWang

Copy link
Copy Markdown
Contributor Author

@H-Chris233 想请你有空时看一下这个 PR 🙏 #1133 合并后 Android 同步进了主线,这个问题会更容易被碰到。

  • 问题仍在当前 beta 上:app_files_dir() 只通过 Tao 的 Activity 注册表获取 Context,而 Tao 只在 Activity 处于前台时保留注册。只有输入法在工作、没有界面在前台时,这个调用会失败,凭据文件路径解析不出来。
  • 后果:如果恰好发生在同步恢复过程中,恢复会一直挂起,写入门之后一直返回 recovery_required,表现为开屏动画反复播放、麦克风键失效(我在一加上实际遇到过)。
  • 修复内容:只改 jni.rs 里这一个函数,进程内缓存一次目录,并在 Tao 路径失败时回退到已注册的 Android Context;失败时两条路径的错误都会保留。
  • 当前状态:与最新 beta 无冲突,CI 全部通过,@HKLHaoBin 已于 10-06 批准;这个修复也一直在我的两台 Android 设备上运行。

如果有需要调整的地方请直接告诉我。

@H-Chris233
H-Chris233 merged commit d292924 into Open-Less:beta Oct 12, 2026
7 checks passed
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.

3 participants