# ADR-MOB-007 — Android privacy budget: permission minimalism, one networked module, CI-enforced

**Status:** Accepted (2026-08-07) · design only, no code yet
**Deciders:** Mohsen Seyedkazemi Ardebili
**Context links:** [[adr-011]] (the parent policy), [[adr-mob-003]] (no accessibility grant),
[[adr-mob-004]] (no ambient capture), [[adr-mob-006]] (the only network use),
[[adr-mob-009]] (store declarations)

---

## Context

[[adr-011]] is the project's constitutional document: zero telemetry, offline by default,
no cloud fallback, no ambient capture, local-only logs, CI-enforced. It was written for a
desktop daemon. Android changes the enforcement surface in ways that need to be made
explicit before contributors start adding dependencies:

- An Android app's privacy posture is largely determined by its **manifest** and by the
  **transitive dependencies** of its modules — an SDK three levels down can add `INTERNET`
  and phone-home behaviour that no reviewer notices in a diff.
- A keyboard is the single most sensitive app class on the platform. The system itself
  warns the user that a keyboard "may be able to collect all the text you type", and users
  are right to be suspicious. Trust here must be *demonstrable*, not asserted.
- Android 14+ requires typed foreground services, and `RECORD_AUDIO` is a while-in-use
  permission: a `microphone` FGS cannot be started from the background, and Android 15+
  applies time limits to several FGS types. Long-running capture (Meeting Mode) must be
  designed against these rules, and the rules move with target SDK.
- Both stores require a data-safety / privacy declaration even for apps that collect
  nothing, and those declarations must match the binary.

## Decision

**A. Permission budget (the manifest is a reviewed artefact).**

| Permission | Where | Why |
|---|---|---|
| `RECORD_AUDIO` | `:app` | the product |
| `INTERNET` | **`:model` only** | model download ([[adr-mob-006]]) |
| `POST_NOTIFICATIONS` | `:app` | Meeting Mode ongoing notification, download status |
| `FOREGROUND_SERVICE` + `FOREGROUND_SERVICE_MICROPHONE` | `:feature:meeting` | long capture |
| `SYSTEM_ALERT_WINDOW` | `:feature:bubble` (opt-in variant) | floating mic ([[adr-mob-003]] §5) |

Everything else is **forbidden without a superseding ADR** — explicitly including
`READ_EXTERNAL_STORAGE`/`READ_MEDIA_*` (use SAF), `ACCESS_NETWORK_STATE` outside `:model`,
`BIND_ACCESSIBILITY_SERVICE`, contacts, phone state, location, and `QUERY_ALL_PACKAGES`.
A PR that adds a manifest permission must change this table in the same PR.

**B. Network containment.** `INTERNET` is declared only in `:model`'s manifest, and only
`:model` may depend on an HTTP client. A Gradle dependency-analysis check fails the build
if any other module (transitively) pulls in OkHttp/Retrofit/Ktor-client or declares a
network permission. The dictation path — `:core:audio` → `:core:vad` → `:core:stt` →
`:core:postprocess` → `:feature:ime` — has no reachable network code, and that is a
build-enforced property rather than a promise.

**C. CI privacy gate** (the Android analogue of [[adr-011]] §10, and of the desktop's
`privacy-gate` job):
1. **Manifest diff gate** — the merged manifest is dumped and compared against a checked-in
   golden file; any new permission, exported component, or `usesCleartextTraffic` change
   fails until the golden file and the table above are updated in the same PR.
2. **Dependency allow-list** — the full transitive dependency graph is compared against a
   checked-in allow-list; new artefacts require review. No analytics/crash SDK may ever
   enter it.
3. **Airplane-mode instrumentation test** — a full dictation round trip (audio fixture →
   text delivered to a test `InputConnection`) runs with networking disabled on the
   emulator and must pass.
4. **No-logging-of-content check** — a lint rule forbidding transcript text or raw audio in
   any log statement. Diagnostics are metadata only (durations, sizes, states), matching
   the desktop's `yazses logs`.

**D. Lifecycle and capture rules.**
- Hold-to-talk needs no foreground service: the IME window is visible, so the app is
  in-use ([[adr-mob-004]]).
- Meeting Mode runs a `microphone`-typed foreground service **started from a visible,
  user-initiated action** (Quick Settings tile, shortcut, or in-app button), with a
  permanent notification carrying elapsed time and a stop action. Android 15+ FGS
  time-limit behaviour for this type is an **open risk with a dedicated spike issue**: the
  design requirement is that reaching a system limit *finalises and saves the meeting*
  (the desktop's `max_minutes` auto-stop behaviour) and never loses recorded audio.
- Capture stops immediately on: mic permission revoked, another app taking the mic
  (`AudioManager` focus/recording callbacks), a password field gaining focus, or the
  session cap being reached.
- **The microphone indicator is never suppressed** and no attempt is made to work around
  privacy indicators, hidden-API restrictions, or battery-optimisation prompts beyond
  documented, user-visible settings.

**E. Data at rest.** Transcripts are not persisted at all in wave 1 unless the user is in
Meeting Mode or file-import, both of which write to app-private storage the user can see
and delete. If and when the desktop's learning corpus (`adr-012`) is ported, it arrives
**off by default with the same encryption-at-rest requirement**, as its own ADR. Nothing
is written to shared storage without SAF and an explicit user pick. Backups exclude models,
transcripts and any future corpus.

**F. Store declarations must match the binary** ([[adr-mob-009]]): "no data collected, no
data shared", with the model download described accurately as a user-initiated file
download to a third-party host.

## Consequences

- A contributor cannot accidentally weaken the privacy posture: the manifest gate and the
  dependency allow-list turn [[adr-011]] from a document into a build failure.
- Meeting Mode is the only genuinely hard lifecycle problem in the app, and it is scoped
  and risk-flagged rather than assumed to work.
- Some conveniences are permanently unavailable (crash reporting, remote config, A/B
  tests). Bug reports follow the desktop model: a local, user-reviewed report the user
  attaches by hand (`yazses report` is the desktop analogue).
- The app can make an unusually strong, checkable statement — *revoke network access and it
  still works* — which is the single best answer to "why not just use Gboard's offline
  voice typing".

## Rejected

- **`INTERNET` in `:app` "for convenience".** Destroys the containment property, which is
  the whole point.
- **Crash reporting, even self-hosted (e.g. an ACRA endpoint), even opt-in, in wave 1.**
  [[adr-011]] §1 is unambiguous; a local, user-reviewed crash log is the substitute. An
  opt-in local-file crash log is acceptable; anything that transmits is not.
- **Google Play Services / Firebase for anything** (including push, config or App Check) —
  also disqualifying for F-Droid.
- **Requesting all permissions up front at first launch.** Each is requested at the moment
  it is first needed, with an in-context explanation.
