Files
file-manager/docs/PLAY_RELEASE_CHECKLIST.md
T
SoftwareApp cd0fdff31b Unpack .tar.bz2 and .tar.xz, which only half-extracted
formatOf matched .tar.gz/.tgz as a combined format and then fell through to the bare .bz2 and .xz wrappers, so a .tar.bz2 was read as one compressed file: extracting it decompressed a single layer and left a .tar to extract again, while the same archive named .tar.gz unpacked in one step. The store listing advertised "extract ZIP, TAR, TAR.GZ, GZ, BZ2, XZ and 7z", which reads as though they all behave alike.

Found by extracting one archive of every supported format through the UI on the API 35 AVD rather than trusting the suite. A real z4.tar.bz2 left z4/z4.tar - raw ustar headers and all - beside z3.tar.gz fully unpacked to z3/a.txt and z3/sub/b.txt.

TAR_BZIP2 and TAR_XZ now sit alongside TAR_GZIP, detection consumes the whole tar suffix (.tar.bz2/.tbz2, .tar.xz/.txz) before the bare compressor, and the engine gains the two readArchive branches. The bare spellings still mean a single compressed file.

The suite had a gap that let this hide: only ZIP, GZIP, XZ and 7z had round-trip tests, so neither plain TAR nor TAR_GZIP did, and there was nothing for the asymmetry to contradict. All four tar spellings have one now, built in memory with the same library that reads them. 212 JVM tests, lint clean.

Also records the format sweep in EDGE_CASE_TESTING and re-measures the release artefacts (aab 4 838 235 / sha256 e1d23130..., apk 2 155 455 / sha256 37fb46cb...). The APK byte count happened to land on the same figure as the previous build, so the new literals were confirmed present in the shipped dex rather than trusted to the size.
2026-10-15 12:14:39 +02:00

9.7 KiB
Raw Blame History

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

  • 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.
  • [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
  • [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

  • versionName [code] — 1.0.0 in app/build.gradle.kts.
  • versionCode [code] — 1. Play orders uploads by it, so every later upload must be higher.

3. Build the release artefacts

./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
  • [code] re-verified 2026-10-15. bundleRelease and assembleRelease both succeed with R8 minification and resource shrinking on. - app-release.aab is 4 838 235 bytes (sha256 e1d23130b307345b1147e5cf54413d42ed53a1693e0682464cd1b64552e0ea78). - app-release.apk is 2 155 455 bytes (sha256 37fb46cb3ef4908b73a9147442ff3f93905a893b8ddd27abb9e02610139a8cee), 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), together with the archive fix for .tar.bz2/.tar.xz. Small source changes barely register in a bundle dominated by libraries, hence 4 838 235 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 — 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:

$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.
  • 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 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.
  • [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.