solo-android-release Β· v1.0.0 Β· 2026-07-29 Β· sha256 893574879905fe6e
solo-android-release v1.0.0A
Immutable. This exact content is served forever at /api/v1/blob/893574879905fe6e.
---
name: android-release
description: Ship an Android app to testers or to Google Play. Use when the user says "make the Android app available to testers", "internal/open testing link", "publish to Play", "release the APK/AAB", or "Play Console". Leads with the MINIMAL tester-link workflow (fastest), then the FULL production listing. Carries the exact Play Console browser steps, signing/keystore setup, the testing-track comparison, required assets, and the gotchas (lintVitalRelease crash, 16 KB alignment) so you don't re-learn them.
license: MIT
metadata:
author: fortunto2
version: "1.0.0"
openclaw:
emoji: "π€"
---
# android-release β ship Android to testers, then to Play
**Start with MINIMAL** (a working tester link in minutes). Do FULL only when the user wants the
public Play listing. Sibling skill: **`ios-release`** for iPhone/TestFlight.
Golden rules:
- **Never hit final publish/"Send for review" without the user's OK** β it's outward-facing. Filling
drafts is fine; an internal-testing rollout is low-risk + reversible, so a prior "set it all up"
covers it β but state what you did.
- **Back up the keystore + password** β losing it = can't update the same Play listing.
- Browser work: **the user logs in** (never touch their password/2FA); you drive the forms after.
---
## Which track? (who can use the link)
| Track | Who can install via link | Setup needed |
|-------|--------------------------|--------------|
| **Internal testing** | only Google accounts on your list (β€100) | almost none β no review |
| **Closed testing** | accounts on a list or a Google Group | + content rating, data safety |
| **Open testing** | **anyone with the link** | + full store listing, content rating, target audience, data safety, **review** |
| **GitHub APK (sideload)** | **literally anyone**, no Google account | none β but "unknown sources" prompt |
Most "available to testers" asks β **Internal testing** (fast) or the **GitHub APK link** (instant,
zero-restriction). "Anyone with a link, via Play" β **Open testing** (heavier: listing + review).
---
## A. MINIMAL β get a tester link
**A1. Fastest: sideload APK via a GitHub Release** (no Play account, ~2 min, works for anyone)
```bash
JAVA_HOME=<jdk17> ./gradlew :composeApp:assembleRelease # universal signed APK
cp composeApp/build/outputs/apk/release/*-release.apk app-<ver>.apk
gh release create v<ver> app-<ver>.apk --repo <owner>/<repo> \
--title "<App> <ver> β Android tester build" --notes-file notes.md
# testers download the .apk from the release page β tap β allow "install from this source"
```
**A2. Play Internal Testing** (opt-in link, installs via Play, no review). Signed **AAB** required
(`:composeApp:bundleRelease`). Browser steps (Playwright β user logs in first):
1. `play.google.com/console` β **Create app**: name (β€30), package `com.β¦`, **Check availability**,
App, Free, tick both declarations (Program Policies + US export laws) β **Create app**.
2. **Testing β Internal testing** β **Create new release**.
3. Leave Play App Signing on ("Releases are signed by Google Play").
4. **Upload** the `.aab` (click Upload β `browser_file_upload` with the absolute path). Wait for
"optimized for distribution". Release name auto-fills; add notes inside `<en-US>β¦</en-US>`.
5. **Next β Save and publish** (confirm dialog). Benign warnings: "no deobfuscation file", "no
native debug symbols", "no testers yet".
6. **Testers** tab β tick an email list (or **Create email list** + paste emails) β **Save** β
track flips to **Active**. Copy the link: `https://play.google.com/apps/internaltest/<trackId>`.
Testers open it signed into a listed Google account, tap "Become a tester", install from Play.
**A3. Upgrade internal β open** later: **Testing β Open testing β Promote release** (or create a new
open release). Requires the FULL section below (listing + content rating) + review before it goes live.
---
## B. FULL β production / open-testing listing
Complete before "Send for review":
- **Store listing**: app name, short desc (β€80), full desc (β€4000), **app icon 512Γ512**,
**feature graphic 1024Γ500**, β₯2 phone screenshots (1080Γ1920..2160; longer side β€ 2Γ shorter).
- **App content** (all required): Privacy policy URL, Ads, App access, Content rating
(questionnaire), Target audience & children, Data safety, plus Government/Financial/Health if relevant.
- **Countries/regions** + Free/Paid.
- Production (or Open testing) track β create release β upload AAB β **Send for review** (confirm first).
- New personal accounts: Google may require **β₯12 testers for 14 days** on closed testing before
production access β plan for that.
### Order that actually unblocks "Send app for review" (Publishing overview)
The **Send app for review** button stays locked until the **Dashboard "Set up your app" checklist is
100%**. Complete, in this order:
1. **Store listing** (Grow users β Store presence β Store listings): name, short/full desc, upload
icon 512 + feature 1024Γ500 + β₯2 phone screenshots. (Tablet-screenshot slots show `*` but are NOT
required for a phone app.)
2. **Store settings** (Store presence β **Store settings**) β EASY TO MISS, it's a separate checklist
item "Select an app category and provide contact details" that needs THREE things, all required to
mark it complete: **App category** + **β₯1 Tag** (Manage tags β NOT optional; a missing tag silently
keeps the whole task incomplete and "Send app for review" locked, with no obvious error) + a
**contact Email**. Verify each value persists after save (a filled-looking field can save blank).
3. **App content** (Policy and programs β **App content β overview**) β do every "Start declaration":
Privacy policy URL Β· App access (Sign-in details) Β· Ads Β· **Content ratings** (IARC questionnaire) Β·
**Target audience** Β· **Data safety** Β· Advertising ID Β· Government apps Β· Financial features Β· Health.
Yes/No order is NOT consistent across pages β read each before clicking.
4. **Open testing track**: Countries/regions (select all β Save) β **Create release** β Add bundle
**from library** (reuse the internal AAB) β notes β Next β **Publishing overview β Send app for review**.
### Content rating (IARC) β for a utility/"All Other App Types" app
Category "All Other App Types"; email for the certificate. Answer content questions **No** (violence/
sex/language/drugs/gambling). If the app has user-posted content (photos/notes shared with others),
"User Content Sharing" = **Yes** β then the sub-questions (nudity/violence/block/report/moderation)
are all **No** for a benign app; rating stays Everyone/PEGI 3.
### Data safety β the long one
Q1 collects data? **Yes** β encrypted in transit **Yes** β account creation method (anonymous app =
"My app does not allow users to create an account") β external login **No** β data-deletion request:
**Yes** needs a "Delete data URL" (use the privacy-policy URL) or answer **No** (it's Optional).
Pick **data types** (LocationβPrecise, Photos, Personal infoβName/Email/User IDs, App activityβOther
UGC), then per type open its modal: **Collected** (your backend is not "Shared" with third parties) β
not ephemeral β **optional** ("users can choose") β purpose **App functionality** β Save.
### Browser-automation gotchas (Playwright on Play Console)
- **Angular Material radios/checkboxes ignore JS `.click()`** β must use a real `browser_click`. Target
them by the accessibility ref, or `question:has-text("<unique question text>") >> role=radio[name="No"]`.
Radio labels ("Yes"/"No") are sibling text, so `role=radio[name=...]` often has an EMPTY name β click
by the visible label text or the ref instead.
- **Sticky footer / overlay panes intercept clicks** on library-Upload / "Add" / "Create release"
buttons β click via `page.evaluate(() => document.querySelector('button[debug-id="..."]').click())`
(debug-ids: `upload-button`, `add-to-content-button`, `create-android-release-button`).
- **File upload**: side-panel Upload button β `browser_file_upload` with the absolute path β the asset
lands in the library β select it β click **Add** (footer-intercepted; JS-click it).
- **Text fields**: type with `slowly`/pressSequentially and VERIFY `input.value` before Save β a filled-
looking field can be empty (wrong element), leaving "Save" disabled or saving blank (bit me on the
contact email β took two tries).
- **Direct URLs to `/app-content/*` sub-pages redirect to Home** on hard-navigation; reach them by
clicking the in-app "App content β overview" link (SPA nav), or navigate to `/app-content/overview`.
- **"Send app for review" locked with everything seemingly filled?** It's almost never a UI lag β
a required sub-field is silently blank. The dashboard task name understates its requirements (e.g.
"Select an app categoryβ¦" also needs **Tags**). When a checklist item won't turn green, open it and
fill EVERY field, including the ones that look optional, before assuming it's a glitch.
---
## Assets β generate what's missing
- **Play icon 512** β resize the app's 1024 icon (e.g. the iOS `AppIcon` `icon_1024.png`) β 512.
- **Feature graphic 1024Γ500** β brand gradient + icon + wordmark (PIL). Keep emoji OUT of Arial
captions (they render as tofu boxes).
- **Framed screenshots** β compose raw device captures onto a caption band; template:
`docs/store/compose_play.py` (caretta-friends). Play phone size 1080Γ2160 (longer β€ 2Γ shorter).
## Signing (once per app)
```bash
keytool -genkeypair -v -keystore keystore/upload.jks -alias <a> -keyalg RSA -keysize 2048 \
-validity 10000 -storepass β¦ -keypass β¦ -dname "CN=β¦, O=β¦, C=β¦"
```
`build.gradle.kts`: `signingConfigs { create("release") { β¦ read keystore.properties β¦ } }` +
`buildTypes.release.signingConfig`. **Gitignore** `*.jks`, `keystore/`, `keystore.properties`, `*.aab`, `*.apk`.
Play App Signing (on by default) re-signs with Google's key; your keystore is the **upload** key.
## Gotchas
- **`lintVitalRelease` crashes** on KMP/Compose ("Unexpected failure during lint analysis of
MainActivity.kt") and fails `assembleRelease` though the APK/AAB packaged fine β
`android { lint { checkReleaseBuilds = false; abortOnError = false } }`.
- **16 KB page alignment** β Play requirement for `targetSdk 35+`. Native `.so` LOAD segments must
have `p_align = 0x4000`. For MapLibre use **11.13.x** + force `androidx.graphics:graphics-path:1.0.1`.
Verify by parsing the AAB's ELF PT_LOAD headers for `p_align >= 16384`.
- **AAB β installable** β testers need an **APK** (`assembleRelease`); AAB is Play-only.
- **Package name** locks at Create-app (Check availability); it's the applicationId forever.