From bda3c2e8ee0f3140b685ee445687a473a3dffcd7 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 23 Oct 2026 07:04:22 -0500 Subject: [PATCH] =?UTF-8?q?docs(android):=20M8=20step=201=20built=20?= =?UTF-8?q?=E2=80=94=20API=2036,=20the=20bundle=20and=20the=20Play=20uploa?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- android/PLAN.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/android/PLAN.md b/android/PLAN.md index e66dc84..ead4bab 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -494,7 +494,7 @@ Native, Android-only: | Live feed | OkHttp SSE (`EventSource`) for `/public/shard/stream` | | Images | Coil | | 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. | | 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) | @@ -966,6 +966,30 @@ push, and Play (M6–M8) follow the designed app. **Order of work:** 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. + + **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. 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 @@ -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** 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). +- **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. ### 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 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 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**