ADR-MOB-009 — Distribution: F-Droid-shaped by default, GitHub APK first, Play later¶
Status: Accepted (2026-08-07) · design only, no code yet Deciders: Mohsen Seyedkazemi Ardebili Context links: [[adr-mob-005]] (engines), [[adr-mob-006]] (models), [[adr-mob-007]] (privacy gate), [[adr-011]], desktop analogue: adr-008 (packaging), design/distribution-status.md
Context¶
The desktop line learned that distribution is not a last step: the APT/PPA/Snap channels each imposed constraints that had to be designed for, and the strictly-confined snap still cannot do hold-to-talk. The Android channels impose their own, and they conflict:
- F-Droid builds everything from source, does not accept prebuilt binaries (including convenience AARs and prebuilt
.sofiles), rejects proprietary dependencies outright (no Play Services, no Firebase), and rewards reproducible builds. It is also where the privacy-focused Android audience actually looks, which is exactly YazSes' audience. - Google Play reaches everyone else, costs a one-off developer fee, requires an accurate Data Safety declaration ([[adr-mob-007]] F), applies extra scrutiny to keyboards and to
RECORD_AUDIO, and forces an annual target-SDK treadmill. - Accrescent and direct GitHub Releases APKs are fast, unmediated channels; the closest FOSS neighbour (Transcribro) ships via Accrescent and GitHub.
The engine choice interacts directly: whisper.cpp is a small CMake project that an F-Droid recipe can realistically build from source, whereas sherpa-onnx's practical Android path is a prebuilt AAR over ONNX Runtime — convenient everywhere except the one channel that matters most ([[adr-mob-005]]).
Decision¶
- F-Droid compatibility is a design constraint from M0, not a later port. Every choice in the dictation path must be buildable from source with no proprietary dependency. This is why whisper.cpp is the default engine and why no Google library may enter the dependency allow-list.
- Two product flavours, one codebase:
foss— the default and the F-Droid/Accrescent/GitHub build. Dictation path only from source-built native code. sherpa-onnx-dependent capabilities (diarization, Silero VAD, Parakeet) are either built from source in the recipe if that proves feasible, or are absent and honestly reported as absent by the feature registry (the desktop'ssystem/backends.pydistinction between "install the extra" and "this build cannot supply it" ports directly).full— the GitHub/Play build, which may use upstream-published binary artefacts for the optional engines. Never differs in privacy posture: same permission table, same CI privacy gate, no analytics in either.- Release order: M1 → signed GitHub Releases APK (
android-v0.1.0). M2 → F-Droid submission + Accrescent. M3+ → Play, only once Meeting Mode and file import are stable enough to survive Play review and the keyboard-policy questions. - Independent versioning and tags. Android releases use
android-vX.Y.Ztags and anandroid-release.ymlworkflow; desktopv*tags must not build or publish an APK, and the Android workflow must not fire on desktop tags. The Android About screen shows both its own version and the contract version it satisfies ([[adr-mob-008]] §6). - Signing. A dedicated Android release keystore, held only by the maintainer, backed up offline, never in the repo and never in a workflow that a fork can trigger; the CI secret is scoped to tag-triggered release runs.
apksignerv2+v3 signing. Reproducible-build settings are enabled from the first release so an F-Droid reproducible listing stays possible later. Key rotation on Android is effectively impossible for sideloaded users, so this is treated with the same seriousness as the APT signing key (whose loss already cost this project a re-key — see the APT repo history). - CI. A path-filtered
android-test.yml(Gradle: assemble, unit tests including the contract vectors, lint, the manifest/dependency privacy gates) runs onandroid/**andcontract/**changes; an instrumented job runs the airplane-mode round-trip on an emulator. Desktop CI is untouched. - Store listing text is bound by the honesty rules. No feature is listed as working until it is wired and tested on a device; the "offline" claim is stated with its one exception (the user-initiated model download) in the listing itself, not only in the FAQ.
Consequences¶
- The default build is the most restricted one, so the project cannot accidentally take a dependency that locks it out of F-Droid — the failure mode is caught at M0 rather than at submission.
- Feature availability differs by flavour, which is a documentation burden and a support burden; the feature screen must show it plainly, per-build, at runtime.
- Play arrives late, which costs reach early. Accepted: the early audience is the privacy-motivated one, and a rushed Play listing for a keyboard with
RECORD_AUDIOand a half-built Meeting Mode is a rejection risk with reputational cost. - An annual target-SDK obligation now exists for the Play build, and target-SDK bumps change FGS/permission behaviour ([[adr-mob-007]]) — so the bump is a scheduled task with a test pass, not a one-line diff.
Rejected¶
- Play-first. Reaches the wrong audience first, imposes the heaviest process on the least-mature build, and would tempt Play-only conveniences into the codebase.
- Prebuilt
.so/AAR in the default build. Blocks F-Droid's main repo and weakens the supply-chain story for an app whose selling point is trust. - A separate "YazSes Pro" paid build. Out of scope; the project is Apache-2.0 and community-built, and a paid tier would poison the contribution model.
- Publishing the APK from the same tag as the desktop release. Couples two release cadences that have nothing to do with each other, and would make every desktop patch release look like a mobile release.