55dc2293b2
A window at or above Material expanded breakpoint (840dp) now splits into a master pane and a detail pane: tapping a row highlights it and the second pane shows a folder children, or a preview plus the metadata for a file. Opening moves to a double tap on wide windows only; phones (~411dp) and the 7-inch in portrait (~600dp) keep their layout and their tap behaviour. Found while taking the tablet screenshots: the app had no large-screen layout at all, so on a 2560x1600 canvas every row was 2560px wide with its content stranded in the left ~240px. The tablet sets are re-shot on this build. That also caught a blank capture (7-inch 01-roots) that had already been committed: a headless AVD draws its system bars before Compose composes its first frame, so the file was not obviously empty. make-store-screenshots.sh now measures each capture app area and refuses to normalise one that is blank.
144 lines
7.4 KiB
Markdown
144 lines
7.4 KiB
Markdown
# Google Play release checklist
|
||
|
||
Work top to bottom. Items marked **[code]** are already handled in this repository; the
|
||
rest are things you must do in the Play Console or with your own accounts.
|
||
|
||
## 1. Identity and signing
|
||
|
||
- [x] **Application id** chosen: `de.softwareapp_hb.filemanager`
|
||
(`applicationId` in `app/build.gradle.kts`). It can **never be changed after the first
|
||
release**, so treat it as locked. `namespace` and the Kotlin package of the same name
|
||
are aligned with it; debug builds append `.debug`.
|
||
- [x] **[code] Upload keystore generated** — `filemanager-upload.jks` in the project root
|
||
(git-ignored, `alias upload`, 4096-bit RSA, 10 000 days, `CN=Hadrian Burkhardt,
|
||
L=Potsdam, C=DE`). Certificate SHA-256 fingerprint, to confirm the right key is
|
||
registered when Play App Signing is enabled:
|
||
`49:A9:FC:13:F5:13:24:48:AE:74:5A:AE:7E:05:D9:2F:67:55:1F:EE:71:A5:64:A1:10:13:57:4D:0E:AA:0E:4E`
|
||
- [x] **[code] `keystore.properties` written** (git-ignored, mode `600`). The Gradle build
|
||
picks it up automatically and signs release builds; without it the release APK stays
|
||
unsigned.
|
||
- [ ] **Store the keystore and its password in a password manager and back it up.** Losing
|
||
the upload key means you cannot ship updates.
|
||
- [ ] Enable **Play App Signing** in the Play Console with the first upload. Google then
|
||
holds the app signing key and this one only signs uploads:
|
||
```bash
|
||
keytool -exportcert -alias upload -keystore filemanager-upload.jks -rfc > upload_cert.pem
|
||
```
|
||
|
||
## 2. Versioning
|
||
|
||
- [x] `versionName` **[code]** — `1.0.0` in `app/build.gradle.kts`.
|
||
- [x] `versionCode` **[code]** — `1`. Play orders uploads by it, so every later upload must
|
||
be higher.
|
||
|
||
## 3. Build the release artefacts
|
||
|
||
```bash
|
||
./gradlew :app:bundleRelease # -> app/build/outputs/bundle/release/app-release.aab
|
||
./gradlew :app:assembleRelease # -> APK, for local checks
|
||
./gradlew :app:testDebugUnitTest # JVM unit tests
|
||
./gradlew :app:lintDebug # Android Lint
|
||
```
|
||
|
||
- [x] **[code] verified 2026-10-14.** `bundleRelease` and `assembleRelease` both succeed
|
||
with R8 minification and resource shrinking on.
|
||
- `app-release.aab` is **4 682 814 bytes**
|
||
(`sha256 9014627a8596397623404cb9d986af5019c5e33cf331c5d363427c140f121788`).
|
||
- `app-release.apk` is **2 081 955 bytes**, signed by the upload key above.
|
||
- Both report `versionCode=1 versionName=1.0.0 targetSdk=36` and exactly the
|
||
permission set in step 4 — **no `INTERNET`**, so "nothing leaves the device" holds
|
||
in the shipped artefact and not just in the source manifest.
|
||
- [ ] Upload the **`.aab`** (not the APK) to the Play Console.
|
||
⚠️ **Expect a native-debug-symbols warning and ignore it.** Play finds
|
||
`base/lib/<abi>/libandroidx.graphics.path.so`, which arrives transitively through
|
||
`androidx.compose.ui:ui-graphics` → `androidx.graphics:graphics-path`. It is a
|
||
recommendation, not an error: it does not block upload, review or rollout. This
|
||
project has **no** `ndk`/`externalNativeBuild`/`jniLibs` configuration and no native
|
||
sources of its own.
|
||
- [ ] Keep `app/build/outputs/mapping/release/mapping.txt` for every release so Play can
|
||
de-obfuscate crash reports.
|
||
|
||
## 4. Sensitive permissions — the Permissions Declaration Form
|
||
|
||
This is the step that can block a file manager, and the one to get right.
|
||
|
||
- [ ] In the Play Console open **App content → Sensitive app permissions** (older UI:
|
||
*Permissions Declaration Form*) and declare **All files access**
|
||
(`MANAGE_EXTERNAL_STORAGE`).
|
||
- [ ] Select the **file manager** use case. Google's policy — *Use of All files access
|
||
(MANAGE_EXTERNAL_STORAGE) permission* — names file managers explicitly:
|
||
*"intended and permitted use includes file managers, backup and restore apps,
|
||
anti-virus apps, and document management apps."*
|
||
- [ ] Make sure the **store description states the core purpose** (browsing, organising and
|
||
managing files on the device). The policy requires the core functionality to be
|
||
"prominently documented and promoted in the app's description" — the listing copy in
|
||
`docs/STORE_LISTING.md` (M10) is written to satisfy that.
|
||
- [ ] **Do not** add `QUERY_ALL_PACKAGES`, `REQUEST_INSTALL_PACKAGES` or `INTERNET` without
|
||
declaring them. The app currently requests none of them.
|
||
- [ ] Provide the **privacy policy URL** ([PRIVACY.md](../PRIVACY.md) — it must be a public
|
||
URL, not a file in the repo).
|
||
- [ ] Keep the permission set honest: declaring a permission the app does not use is itself
|
||
a policy violation.
|
||
|
||
**Verify the shipped permission set** after every release build:
|
||
|
||
```bash
|
||
$ANDROID_HOME/build-tools/36.0.0/aapt2 dump permissions \
|
||
app/build/outputs/apk/release/app-release.apk
|
||
```
|
||
|
||
Expected, and nothing else:
|
||
|
||
```
|
||
MANAGE_EXTERNAL_STORAGE
|
||
READ_EXTERNAL_STORAGE (maxSdkVersion=32)
|
||
READ_MEDIA_IMAGES
|
||
READ_MEDIA_VIDEO
|
||
READ_MEDIA_AUDIO
|
||
READ_MEDIA_VISUAL_USER_SELECTED
|
||
WRITE_EXTERNAL_STORAGE (maxSdkVersion=29)
|
||
FOREGROUND_SERVICE
|
||
FOREGROUND_SERVICE_DATA_SYNC
|
||
POST_NOTIFICATIONS
|
||
<applicationId>.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION (added by AndroidX)
|
||
```
|
||
|
||
## 5. Data safety, content rating, audience
|
||
|
||
- [ ] **Data safety** form: the app collects **nothing** and shares **nothing**. Answer "No"
|
||
to collection, "No" to sharing, and state that data is not transmitted off the device.
|
||
- [ ] Confirm the answers match [PRIVACY.md](../PRIVACY.md).
|
||
- [ ] Content rating questionnaire: utility, no user-generated content, no ads.
|
||
- [ ] Target audience: not directed at children.
|
||
|
||
## 6. Store listing
|
||
|
||
Full copy and the generated graphics land in **M10** (`docs/STORE_LISTING.md`). Console
|
||
fields needed: app name, short description, full description, app icon 512×512, feature
|
||
graphic 1024×500, and the screenshots.
|
||
|
||
- [ ] Upload the screenshots from `store-assets/screenshots/`: **phone** 1080×2160 (5),
|
||
**7-inch tablet** 1200×1920 (4), **10-inch tablet** 2560×1600 (4). Play wants 4+ per
|
||
large-screen form factor. All three sets clear its rules — 24-bit PNG with no alpha,
|
||
shorter side ≥ 320, longer side ≤ 3840, and longer side ≤ 2× the shorter side.
|
||
`tools/make-store-screenshots.sh` enforces the aspect ratio and rejects a capture
|
||
whose app area is blank.
|
||
- [ ] Expect the 7-inch set to be single-pane and the 10-inch set two-pane. That is the
|
||
adaptive layout working, not an inconsistency — see the CHANGELOG.
|
||
|
||
## 7. Release tracks and pricing
|
||
|
||
- [ ] Create the app, complete *App content*, then roll out
|
||
**internal → closed → production**, checking the pre-launch report on the way.
|
||
- [ ] ⚠️ **Set the price before the first upload.** Play's own wording: *"You can change your
|
||
app from paid to free. Once your app has been offered for free, the app can't be
|
||
changed to paid."* So:
|
||
1. Create a **payments profile** (identity, tax, bank details).
|
||
2. *Products › App pricing*: base price **€4.49**.
|
||
3. Launch price **€2.29** for the first **14 days**, then diarise changing it back.
|
||
4. Do every step above **before uploading**.
|
||
- [x] **[code] no Billing client and no `BILLING` permission** — the price lives on the
|
||
listing, not in the build, so the APK contains no purchase code at all. Verified
|
||
2026-10-14 against the release APK. Full rationale and the rejected alternatives:
|
||
[MONETISATION](MONETISATION.md).
|