fix(security): harden app-lock recovery restart
The key-invalidation recovery restart was unreliable in two ways, both in AppLockViewModel: 1. Same-process self-restart race: restartProcess() did context.startActivity(...) immediately followed by Runtime.exit(0) in the same process, so ActivityManager could schedule the relaunch into the process being killed and drop it — the app just closed, recovering only on the next manual launch. Fixed with a ProcessPhoenix-style separate-process trampoline (RestartActivity in a distinct ":restart" process, driven by ProcessRestarter): it kills the original process by PID and only then relaunches, so the relaunch is issued from a process that survives the kill. No new dependency; LibreMailApplication early-returns in the ":restart" process so it runs no normal startup work. 2. Lost syncNow() enqueue: clearCacheAndRestart() enqueued the post-wipe re-sync fire-and-forget, but WorkManager persists the WorkSpec asynchronously on its serial task executor, so exiting raced that insert and could drop the re-sync (now user-visible after #118: an empty mailbox until the next periodic sync). syncNow() now returns its enqueue Operation, and clearCacheAndRestart awaits it (bounded by a 5s timeout) before restarting, so the WorkSpec is durably persisted first. CLEAR_PENDING recovery-flag semantics are preserved; the cache wipe still happens at cold start in DatabaseModule (unchanged). Tests: JVM unit tests assert the enqueue Operation is awaited before the restart is triggered (order) and that a timed-out enqueue still restarts; SyncSchedulerTest pins syncNow() returning the enqueue Operation. The separate-process kill/relaunch is device-only and noted for on-device wipe+resync verification. Closes #99 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,19 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<!-- Separate-process trampoline for the app-lock key-invalidation recovery restart. Runs in
|
||||
its own ":restart" process (android:process) so it survives the main process being killed
|
||||
and can reliably relaunch the app from the outside — a same-process "startActivity then
|
||||
exit(0)" restart races ActivityManager and can be dropped (the ProcessPhoenix pattern).
|
||||
Not exported; only ProcessRestarter starts it. Translucent + excluded from recents so it
|
||||
never flashes UI or lingers in the switcher before it finishes and exits its own process. -->
|
||||
<activity
|
||||
android:name=".restart.RestartActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:process=":restart"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<!-- Shares downloaded attachments with viewer apps via a content:// URI. -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
||||
Reference in New Issue
Block a user