5786b618b7
The checklist still recorded the first artefacts (aab 4 682 814, apk 2 081 955); re-measured at 4 836 490 and 2 155 563 and noted why they moved, that the permission set did not change, and that a stale hash is worse than no hash because it looks checked. PRIVACY.md said the app keeps no copies of your files and deletes what you select directly. Both stopped being true when deletes started staging into the trash: the item is moved, not erased, and stays on the volume for 24 hours. A privacy policy that misdescribes deletion is not a small thing on a paid listing, so the deleting and retention sections now describe the trash, the index it keeps, and the one caveat that clearing app storage forgets the trash without emptying it. Also flags that the screenshots show none of search, undo, trash or receiving a share.
161 lines
8.8 KiB
Markdown
161 lines
8.8 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] re-verified 2026-10-15.** `bundleRelease` and `assembleRelease` both succeed
|
||
with R8 minification and resource shrinking on.
|
||
- `app-release.aab` is **4 836 490 bytes**
|
||
(`sha256 0c0702d3917e9b94f42dabfce8002cae6b9377ee4b25d054bf97f73b4c3a8652`).
|
||
- `app-release.apk` is **2 155 563 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.
|
||
- The figures moved from the first measurement (4 682 814 / 2 081 955) because of the
|
||
LZMA library behind `.xz` and 7z. The permission set did **not** change: the share
|
||
target, the file picker and the trash index all arrived without a single new
|
||
permission, which is the point of having designed them that way.
|
||
- ⚠️ **Re-measure after any further change.** These numbers are a record of a verified
|
||
artefact, not a constant — a stale hash here is worse than no hash, because it looks
|
||
checked.
|
||
- [ ] 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).
|
||
- [ ] The app now keeps a small local SQLite index of staged deletes (the trash), so that a
|
||
deletion can be undone after a restart. It never leaves the device, so **"collects
|
||
nothing" is still the honest answer** — but if a form asks whether the app *stores*
|
||
anything, the truthful answer is "on the device only, and it is the user's own file
|
||
paths". [PRIVACY.md](../PRIVACY.md) should say so explicitly.
|
||
- [ ] 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.
|
||
- [ ] The committed sets show browsing, a folder list, an image preview and storage usage.
|
||
**None of them show search, undo, the trash or receiving a share** — four features the
|
||
app now has. The existing shots stay accurate (no layout changed), so this is an
|
||
opportunity rather than a defect: two or three more captures would let the listing
|
||
advertise them. See `tools/make-store-screenshots.sh`.
|
||
|
||
## 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).
|