Skip to content

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 encrypted settings table) 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, MainActivity sets WindowManager.LayoutParams.FLAG_SECURE on 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 before setContent, so no frame (not even the fail-closed gate's background surface from ADR-0012) composes unprotected — strictly earlier than the resolved gate. A collectAsStateWithLifecycle observe 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 SettingsRow inside the existing Security section (SettingsSectionCard), so scripts/ui-guard.sh stays 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 raw adb shell screencap comparison.
  • 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 resolved flag instead of applying before setContent — 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).