The encryptCache setting is written immediately but the on-disk
conversion is deferred to the next cold start, so a transitional window
exists (setting off, DB still encrypted, SEALED_AUTH present) in which
three components answered from the setting and got it wrong (#479):
* AppLockViewModel fed the raw setting into KeyInvalidationPolicy, so
removing the device lock inside the window returned DISABLE_APP_LOCK:
app-lock silently off, no wipe, SEALED_AUTH orphaned - and the next
cold start hung forever in DatabaseKeyStore.resolvePassphrase
(session.await() nothing could complete), bricking the app behind the
CacheEncryptionGate until a data clear. onForeground now derives the
policy input as `encryptCache || hasAuthSealedPassphrase()` (the same
gate-on-the-seal fix SettingsViewModel.setAppLock already carries), so
the window routes to CLEAR_AND_DISABLE (wipe scheduled, restart) and
DISABLE_APP_LOCK is only reachable seal-free. The policy parameter is
renamed to `encryptedCacheProtected` to make the contract explicit.
* EncryptedCacheGuard.isCacheLocked() derived "locked" from the settings
pair, wrong in both transitional states: workers parked forever inside
provideDatabase when the setting was off but the DB still auth-sealed
(case A), and sync/push/send stalled needlessly while the seal was
still MASTER (case B). It now mirrors resolvePassphrase's blocking
branches - keyed off DatabaseKeyStore.sealState() plus, for the
AUTH-seal-with-setting-off window, a raw header read of the cache file
(still never touching Room).
* DatabaseProvisioner now releases the orphaned auth seal after the
decrypt-on-disable conversion (reseals under the master key,
best-effort), closing the window at its source instead of leaving
SEALED_AUTH to linger indefinitely.
All decision/fallback paths breadcrumb through AppLog (PII-free enums
and booleans only).
Closes#479