ADR-0078: Opt-in screenshot and Recents protection
Context
The app is defended at rest (SQLCipher + Keystore, ADR-0010) and behind a
PIN/biometric gate (ADR-0012), but nothing stopped an OS screenshot, a screen
recording, a cast/AirPlay mirror, or the system Recents thumbnail from showing a
transaction, a balance or the review queue. There was no FLAG_SECURE anywhere
in the main sources. The repository also screenshots and records its own UI for
the documentation (scripts/generate_screenshots.sh,
scripts/generate_recordings.sh, docs/assets/screens/**), which needs the
screen to be capturable.
The window flag is the only lever the OS exposes: once set, screenshots, recordings, casting and the Recents snapshot are blanked by the system. It cannot stop a photo of the screen taken with another device, and it does not protect the notification shade or other apps' windows.
Decision
- Default off. A new encrypted preference
SettingsRepository.KEY_SCREENSHOT_PROTECTION(string"true"/"false", in the existing encryptedsettingstable) is absent until the user opts in, so the repository's own screenshot/recording tooling keeps working untouched and no released behaviour changes. - Whole-window scope, applied from cold start. When on,
MainActivitysetsWindowManager.LayoutParams.FLAG_SECUREon the activity window. Every destination — not just the lock overlay — is covered, because any screen can be the Recents preview. The stored value is read on the same cold-start pass as the onboarding flag and applied beforesetContent, so no frame (not even the fail-closed gate's background surface from ADR-0012) composes unprotected — strictly earlier than theresolvedgate. AcollectAsStateWithLifecycleobserve of the pref re-applies or clears the flag immediately on a change, and re-reads on rotation and process restart. - Honest copy (limits stated). The row help says the feature blocks screenshots, screen recordings and casting on most devices, and that photos taken with another device are not prevented. It is presented as defence-in-depth, never as absolute protection.
- One shared control. The toggle is a
SettingsRowinside the existing Security section (SettingsSectionCard), soscripts/ui-guard.shstays green.
Consequences
- Opted in, the OS blanks screenshots, recordings, casting mirrors and the Recents thumbnail for the whole app window; the flag survives restart and rotation because the preference is persisted.
- Off (the default), the documentation capture flow and
docs/assets/screens/**are unchanged. - JVM tests cover the preference policy (default off, encrypted persistence,
observe-on-change); the on-device contract is covered by
ScreenshotProtectionActivityTest(flag set/cleared, default off, live toggle, Activity recreation) and a rawadb shell screencapcomparison. - No Room schema change (version stays 9), no new dependency, no new architecture layer.
Rejected alternatives
- Default on — would blank the repository's own screenshots/recordings and silently change device behaviour for users who never asked.
- Protect only the lock overlay — the Recents preview and every normal screen would remain exposed.
- Gate on the lock
resolvedflag instead of applying beforesetContent— equivalent in the common case, but applying before content composes removes the race entirely and does not couple the two concerns. - Claim absolute protection — the platform cannot stop an external camera; the copy states that limit (AC4).