Two lifecycle-consistency items from PR #45's review.
1. Grace period now survives leaving via Back (activity recreation)
AppLockGate (the pure lock/grace state machine) was a field of the Activity-scoped AppLockViewModel. On API 29/30, Back on the task root finishes the Activity and clears its ViewModelStore, destroying the ViewModel and its gate — so re-entry within the 30s grace re-armed a fresh LOCKED gate and demanded full re-auth, while leaving via Home and returning stayed unlocked. This contradicted the documented 30s grace rule the JVM tests pin.
Fix: hoist the gate to an application-scoped @Singleton (provided in SecurityModule) and inject it into AppLockViewModel instead of constructing it inline. The same instance is now reused across Activity recreation, so Back-then-return behaves identically to Home-then-return. A genuine cold start (process death) constructs a fresh gate that correctly starts LOCKED — so this does not weaken the cold-start rule, and (deliberately) does not persist backgroundedAt to disk, which would wrongly let a killed process skip re-auth (and would strand PassphraseSession, which is empty after process death).
The KDoc promised the passphrase is "cleared on lock, timeout," but no path re-locked it on grace expiry, and true eviction is not achievable here: provideDatabase runs once per process and SQLCipher keeps the key in the already-open Room handle, so the secret lives for the process lifetime regardless. That DB-handle lifecycle is owned by #93 (provideDatabase) and #111 (DB re-architecture), so per the ticket I did not touch DatabaseModule/provideDatabase.
Corrected the PassphraseSession KDoc to state the process-lifetime limitation explicitly.
Added a code comment at the timeout re-lock in AppLockViewModel deferring full eviction to #93/#111.
Did not call session.lock() on timeout: it is the only separately-held copy, but it also drives EncryptedCacheGuard, so clearing it while the app is merely locked (not exited) would stall background sync/push even though the DB stays open — i.e. it is not a correct side-effect-free partial eviction in the current architecture. The ticket's conditional "if there is a cheap correct partial eviction" is therefore not satisfied; documented why.
Tests (JVM unit)
AppLockGateTest: grace survives a reused-instance recreation within the window; still expires beyond it; a fresh gate (process death / the pre-fix Activity-scoped bug) starts LOCKED.
AppLockViewModelTest (new): the gate is an injected dependency the ViewModel delegates to (onBackground records on the injected gate; onAuthError re-locks through it) — a regression guard against re-inlining the gate. Broader ViewModel coverage remains issue #100.
Verification
Fast gate green locally (JDK 21): :app:assembleDebug :app:testDebugUnitTest :app:lintDebug :app:ktlintCheck :app:detekt and :app:compileDebugAndroidTestKotlin.
Closes #101
Two lifecycle-consistency items from PR #45's review.
## 1. Grace period now survives leaving via Back (activity recreation)
`AppLockGate` (the pure lock/grace state machine) was a field of the Activity-scoped `AppLockViewModel`. On API 29/30, Back on the task root finishes the Activity and clears its `ViewModelStore`, destroying the ViewModel and its gate — so re-entry within the 30s grace re-armed a fresh `LOCKED` gate and demanded full re-auth, while leaving via Home and returning stayed unlocked. This contradicted the documented 30s grace rule the JVM tests pin.
**Fix:** hoist the gate to an application-scoped `@Singleton` (provided in `SecurityModule`) and inject it into `AppLockViewModel` instead of constructing it inline. The same instance is now reused across Activity recreation, so Back-then-return behaves identically to Home-then-return. A genuine cold start (process death) constructs a fresh gate that correctly starts `LOCKED` — so this does **not** weaken the cold-start rule, and (deliberately) does not persist `backgroundedAt` to disk, which would wrongly let a killed process skip re-auth (and would strand `PassphraseSession`, which is empty after process death).
## 2. `PassphraseSession` eviction — KDoc corrected + limitation documented
The KDoc promised the passphrase is "cleared on lock, timeout," but no path re-locked it on grace expiry, and true eviction is not achievable here: `provideDatabase` runs once per process and SQLCipher keeps the key in the already-open Room handle, so the secret lives for the process lifetime regardless. That DB-handle lifecycle is owned by **#93** (provideDatabase) and **#111** (DB re-architecture), so per the ticket I did not touch `DatabaseModule`/`provideDatabase`.
- Corrected the `PassphraseSession` KDoc to state the process-lifetime limitation explicitly.
- Added a code comment at the timeout re-lock in `AppLockViewModel` deferring full eviction to #93/#111.
- **Did not** call `session.lock()` on timeout: it is the only separately-held copy, but it also drives `EncryptedCacheGuard`, so clearing it while the app is merely locked (not exited) would stall background sync/push even though the DB stays open — i.e. it is not a *correct* side-effect-free partial eviction in the current architecture. The ticket's conditional "if there is a cheap correct partial eviction" is therefore not satisfied; documented why.
## Tests (JVM unit)
- `AppLockGateTest`: grace survives a reused-instance recreation within the window; still expires beyond it; a fresh gate (process death / the pre-fix Activity-scoped bug) starts `LOCKED`.
- `AppLockViewModelTest` (new): the gate is an injected dependency the ViewModel delegates to (`onBackground` records on the injected gate; `onAuthError` re-locks through it) — a regression guard against re-inlining the gate. Broader ViewModel coverage remains issue #100.
## Verification
Fast gate green locally (JDK 21): `:app:assembleDebug :app:testDebugUnitTest :app:lintDebug :app:ktlintCheck :app:detekt` and `:app:compileDebugAndroidTestKotlin`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #101
Two lifecycle-consistency items from PR #45's review.
1. Grace period now survives leaving via Back (activity recreation)
AppLockGate(the pure lock/grace state machine) was a field of the Activity-scopedAppLockViewModel. On API 29/30, Back on the task root finishes the Activity and clears itsViewModelStore, destroying the ViewModel and its gate — so re-entry within the 30s grace re-armed a freshLOCKEDgate and demanded full re-auth, while leaving via Home and returning stayed unlocked. This contradicted the documented 30s grace rule the JVM tests pin.Fix: hoist the gate to an application-scoped
@Singleton(provided inSecurityModule) and inject it intoAppLockViewModelinstead of constructing it inline. The same instance is now reused across Activity recreation, so Back-then-return behaves identically to Home-then-return. A genuine cold start (process death) constructs a fresh gate that correctly startsLOCKED— so this does not weaken the cold-start rule, and (deliberately) does not persistbackgroundedAtto disk, which would wrongly let a killed process skip re-auth (and would strandPassphraseSession, which is empty after process death).2.
PassphraseSessioneviction — KDoc corrected + limitation documentedThe KDoc promised the passphrase is "cleared on lock, timeout," but no path re-locked it on grace expiry, and true eviction is not achievable here:
provideDatabaseruns once per process and SQLCipher keeps the key in the already-open Room handle, so the secret lives for the process lifetime regardless. That DB-handle lifecycle is owned by #93 (provideDatabase) and #111 (DB re-architecture), so per the ticket I did not touchDatabaseModule/provideDatabase.PassphraseSessionKDoc to state the process-lifetime limitation explicitly.AppLockViewModeldeferring full eviction to #93/#111.session.lock()on timeout: it is the only separately-held copy, but it also drivesEncryptedCacheGuard, so clearing it while the app is merely locked (not exited) would stall background sync/push even though the DB stays open — i.e. it is not a correct side-effect-free partial eviction in the current architecture. The ticket's conditional "if there is a cheap correct partial eviction" is therefore not satisfied; documented why.Tests (JVM unit)
AppLockGateTest: grace survives a reused-instance recreation within the window; still expires beyond it; a fresh gate (process death / the pre-fix Activity-scoped bug) startsLOCKED.AppLockViewModelTest(new): the gate is an injected dependency the ViewModel delegates to (onBackgroundrecords on the injected gate;onAuthErrorre-locks through it) — a regression guard against re-inlining the gate. Broader ViewModel coverage remains issue #100.Verification
Fast gate green locally (JDK 21):
:app:assembleDebug :app:testDebugUnitTest :app:lintDebug :app:ktlintCheck :app:detektand:app:compileDebugAndroidTestKotlin.🤖 Generated with Claude Code