docs(android): M8 step 1 built — API 36, the bundle and the Play upload

Records what Android-app's feat/m8-google-play builds and how it was
checked, raises target/compile SDK to 36 in §2 and §13, describes the
Play step in §12, and lists the console and Cloud setup the org lead
does before CI's first upload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-23 07:04:22 -05:00
parent ebab704228
commit bda3c2e8ee

View File

@@ -494,7 +494,7 @@ Native, Android-only:
| Live feed | OkHttp SSE (`EventSource`) for `/public/shard/stream` | | Live feed | OkHttp SSE (`EventSource`) for `/public/shard/stream` |
| Images | Coil | | Images | Coil |
| Min SDK | **Android 10 (API 29)** — ~95% device reach with a modern baseline (biometric, storage, TLS) and no compat shims | | Min SDK | **Android 10 (API 29)** — ~95% device reach with a modern baseline (biometric, storage, TLS) and no compat shims |
| Target/compile SDK | Latest stable (35) | | Target/compile SDK | Latest stable (36, Android 16: Google Play's floor since 2026-08-31; M8) |
| Telemetry | **None in v1** — no crash/analytics SDK (privacy-first). Revisit self-hosted crash reporting later. | | Telemetry | **None in v1** — no crash/analytics SDK (privacy-first). Revisit self-hosted crash reporting later. |
| Localization | **Strings externalized from day one** (`res/values/strings.xml`); English is the only bundled locale, but the structure invites community translations. No hardcoded UI strings. | | Localization | **Strings externalized from day one** (`res/values/strings.xml`); English is the only bundled locale, but the structure invites community translations. No hardcoded UI strings. |
| Web hand-off | Chrome Custom Tabs — opens the website for registration / invite / password reset / SSO (§4.2) | | Web hand-off | Chrome Custom Tabs — opens the website for registration / invite / password reset / SSO (§4.2) |
@@ -966,6 +966,30 @@ push, and Play (M6–M8) follow the designed app.
**Order of work:** **Order of work:**
1. **App + CI** (one PR into `edge`): API 36 + AGP bump, AAB, Play upload step. Walked on the 1. **App + CI** (one PR into `edge`): API 36 + AGP bump, AAB, Play upload step. Walked on the
emulator and the S22 before the PR. emulator and the S22 before the PR.
**Built 2026-10-23.** AGP 8.13.2 (the last 8.x; 9.x changes the DSL) with the Gradle 8.14.3
wrapper; Kotlin, KSP and Hilt unchanged. AGP 8.13 still defaults to build-tools 35.0.0, so
the workflows only add `platforms;android-36`. The code had none of Android 16's hotspots:
no edge-to-edge opt-out, no `onBackPressed` override (Navigation Compose takes predictive
back), no orientation lock. The bundle goes on the Gitea release as well, which is where
step 2's hand upload takes it from. Checks: the debug gate (unit tests, lint, debug build)
passes; a release APK and bundle built with a throwaway keystore carry the same
certificate, `targetSdkVersion 36`, versionCode 601; the Play step ran against a mock of
the token and Play endpoints in `ubuntu:latest` (JWT signature checked against the key's
public half; success, a rejected token, a refused upload, a refused commit, a mismatched
versionCode, and an unset secret). Walked on an Android 16 emulator (API 36.1): connect,
sign in as admin, notification settings saved, edge-swipe back, Rust servers, Dashboard,
no crash; the minified release build refuses `http` and survives a TLS round trip. On the
`s22_ultra` emulator (API 35) the build updates in place, connects and opens the menu. Not
walked: the push foreground service (the walk site has no ntfy), and a signed-in release
build (no local HTTPS site); the internal-track install in step 2 covers both.
**Before the first CI upload (org lead, by hand):** in Google Cloud, a service account
with a JSON key, and the Google Play Android Developer API enabled; in Play Console →
Users and permissions, invite the service account's address with release rights on this
app only; then the whole JSON file as the `PLAY_SERVICE_ACCOUNT_JSON` secret on
`RunicGateway/Android-app`. Until a release has been rolled out by hand (step 2), Play
refuses CI's upload with "Only releases with status draft may be created on draft app".
2. **First bundle**, uploaded by hand from the next tag, with the signing key enrolled. 2. **First bundle**, uploaded by hand from the next tag, with the signing key enrolled.
3. **Console declarations**: target audience 18+ (runicgateway.com D31), advertising ID none, 3. **Console declarations**: target audience 18+ (runicgateway.com D31), advertising ID none,
data safety from runicgateway.com's generated `PLAY_DATA_SAFETY.md`, and sign-in details data safety from runicgateway.com's generated `PLAY_DATA_SAFETY.md`, and sign-in details
@@ -2051,6 +2075,14 @@ repos use), on a bare `ubuntu:latest` container.
Gitea release (mirrors `link/`'s release job). The **keystore is a base64 Gitea Actions secret** Gitea release (mirrors `link/`'s release job). The **keystore is a base64 Gitea Actions secret**
decoded in CI; store/key passwords are secrets. The keystore never lives in the repo. Keep the decoded in CI; store/key passwords are secrets. The keystore never lives in the repo. Keep the
signing identity stable from the first release (Play later requires consistency). signing identity stable from the first release (Play later requires consistency).
- **Google Play** (M8, same workflow): the keystore also signs an app bundle (`bundleRelease`),
attached to the Gitea release beside the APK. After the Gitea release exists, the bundle goes to
Play's **internal** track through the Play Developer API v3 edit flow (open an edit → upload the
bundle → set the internal track → commit), with an OAuth token from a service-account JWT signed
by `openssl`. The key is the `PLAY_SERVICE_ACCOUNT_JSON` secret; while it is unset, the step
warns and skips. The step runs last, so a Play failure fails the run without holding back the
APK. Play rejects a `completed` release on an app that has never rolled one out ("Only releases
with status draft may be created on draft app"); the first bundle is therefore uploaded by hand.
- Semantic `versionName` + monotonic `versionCode`; tag releases. - Semantic `versionName` + monotonic `versionCode`; tag releases.
### 12.1 Code quality — SonarQube (`Runic-Gateway-Android-app`) ### 12.1 Code quality — SonarQube (`Runic-Gateway-Android-app`)
@@ -2084,7 +2116,7 @@ Compose/nav cognitive-complexity + hoisted-callback param-count smells (idiomati
**Decided (recorded here for context):** single shard per install (§3); native auth is **Decided (recorded here for context):** single shard per install (§3); native auth is
password+TOTP only, with registration/invite/reset/SSO **handled by the website** (§4); **password password+TOTP only, with registration/invite/reset/SSO **handled by the website** (§4); **password
reset built on backend + web first**, before app work (§8); minSdk 29, compile/target 35 (§2); no reset built on backend + web first**, before app work (§8); minSdk 29, compile/target 36 (§2, raised from 35 at M8); no
telemetry in v1 (§2); strings externalized from day one, English-only bundled (§2); **text-only** game telemetry in v1 (§2); strings externalized from day one, English-only bundled (§2); **text-only** game
data in v1, pretty paperdoll is future (§6.3); **no offline cache in v1** (§7); push via self-hosted data in v1, pretty paperdoll is future (§6.3); **no offline cache in v1** (§7); push via self-hosted
ntfy / UnifiedPush (§11) with the **distributor embedded in the app — no second app to install** ntfy / UnifiedPush (§11) with the **distributor embedded in the app — no second app to install**