Files
file-manager/docs/PLAY_RELEASE_CHECKLIST.md
T
SoftwareApp bb1187881b Make the Android 13+ pass, and fix what it found
The phone runs Android 12, so the questions that matter most for a release were exactly the ones it could not answer. Installed an API 35 AVD (sdkmanager was already present and dl.google.com reachable) rather than waiting for a public test track to report back. It found two user-visible defects, neither of which any test could have caught.

On Android 13+ the app asked for no runtime permission at all, so the progress notification was dead. StorageAccessPolicy.missingPermissions returned an empty list from API 33 on the reasoning that "all files access" is a special permission and browsing therefore needs no runtime grant — true as far as browsing goes, but the early return also dropped POST_NOTIFICATIONS, and nothing else in the app requested it. startForeground still ran, so nothing crashed and all 203 tests passed, while dumpsys showed granted=false and importance=NONE. Settings could not fix that either, because Android will not let a user grant a permission the app has never asked for. The notification grant is now requested alongside the storage grant, which is the only moment Android accepts it.

The four READ_MEDIA_* declarations were in the same position, and the manifest comment claiming they back thumbnails and viewers on Android 13+ was wrong. Coil loads both from a real filesystem path — the code comments already say "all-files mode only" — and the one MediaStore use is a best-effort delete request that "all files access" already covers. They were never requested, so they were never granted, so nothing could have depended on them. Removed, which also settles the checklist rule that declaring an unused permission is itself a policy violation.

The selection bar drew its last three actions off the screen on a phone. With one archive selected it holds ten icon buttons, ~480 dp at the 48 dp minimum touch size against the 411 dp a phone has, and BottomAppBar is a plain Row that does not scroll. Compose laid the surplus out past the right edge: measured on a 1080 px screen the row ran Copy(167) to Compress(923-980, itself squeezed to 57 px) and stopped, so Extract here, Select all and Clear selection were unreachable, while the same selection in a 2400 px landscape window showed all ten. The logic was never wrong — ArchiveFormats.isExtractable had judged the .zip correctly — the app just looked like it could not extract. The action row now scrolls, verified by swiping it in portrait and seeing all ten appear.

Verified on the AVD rather than only built: the notification dialog now appears and importance goes NONE -> DEFAULT, a real 220 MB copy posts notification 1001 on channel file-operations with ONGOING_EVENT|FOREGROUND_SERVICE and tears it down plus stops the service when done, edge-to-edge is correct under API 35s enforcement, predictive back returns from a folder and keeps the activity resumed, and Compress to ZIP produced an archive the host unzip read back with the right relative paths and contents. 204 JVM tests, lint clean, release artefacts re-measured.
2026-10-15 11:39:48 +02:00

168 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 837 465 bytes**
(`sha256 9c2b3a007784297511f29b107a3be40e648709d7702315c53b93ef18f5979070`).
- `app-release.apk` is **2 155 455 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 barely moved again, but the permission set did: the four `READ_MEDIA_*`
declarations are gone, removed once the Android 15 pass showed the app never
requested them and never read through `MediaStore` (see below). Four manifest lines
are worth only a few hundred bytes, hence 4 837 465 rather than a visible drop; the
first measurement was 4 682 814 / 2 081 955, before the LZMA library behind `.xz`
and 7z. The permission set has only ever shrunk: the share target, the file picker,
the trash index and the archive work 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)
WRITE_EXTERNAL_STORAGE (maxSdkVersion=29)
FOREGROUND_SERVICE
FOREGROUND_SERVICE_DATA_SYNC
POST_NOTIFICATIONS
<applicationId>.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION (added by AndroidX)
```
No `READ_MEDIA_*` appears, and that is deliberate. Browsing and every read go through the
filesystem (Coil loads thumbnails and the viewers from a real path); the only `MediaStore`
use is the best-effort delete request, which "All files access" already covers. The app
never requested the media permissions either, so declaring them would mean declaring
permissions that are never used — which is exactly what section 4's "keep the permission
set honest" rule forbids.
## 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).