diff --git a/CHANGELOG.md b/CHANGELOG.md index c7babda..30c614c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,19 @@ upload needs a **higher `versionCode`** than every previous upload. ### Added +- **The release checklist now has a step that runs the release build on a device.** The R8 defect + above was invisible to every check that existed: it passed the whole unit suite, passed lint, and + worked in the debug APK — the release APK was the only thing that disagreed, and nothing in the + process had ever run it. [docs/PLAY_RELEASE_CHECKLIST.md](docs/PLAY_RELEASE_CHECKLIST.md) § 4 is + that step: the install commands (the release build is a separate package, so it can sit beside the + debug one and be compared directly), the paths most likely to depend on something R8 cannot + see — thumbnails, the foreground service, the two SQLite databases, each viewer, the + `FileProvider` — and the one instruction that matters when something differs, which is to assume R8 + removed something rather than that the feature is broken. Section 3 also carries a fresh + measurement of both artefacts (**AAB 5 181 628 bytes**, **APK 2 294 021 bytes**, both hashed), + because the previous record predated the library views, the duplicate finder and video thumbnails — + and a stale hash is worse than no hash, since it reads as if it had been checked. + - **A video shows a frame of itself instead of a film-strip icon.** A phone holds more video than anything else, and a column of identical icons says nothing about which clip is which. Images and videos now take the same path — `EntryIcon` hands Coil the file, and for a video the request asks @@ -253,8 +266,8 @@ upload needs a **higher `versionCode`** than every previous upload. Verified on the real phone: four genuine groups found among the device's own files — a ZIP duplicated across two folders, a PDF twice, another PDF three times — with the selection label reading "Delete 1 copy" and no crash in `logcat -b crash`. The *delete* was deliberately not - pressed on a device that is not mine to empty; [EDGE_CASE_TESTING](EDGE_CASE_TESTING.md) records - what was and was not exercised. + pressed on a device that is not mine to empty; [docs/EDGE_CASE_TESTING.md](docs/EDGE_CASE_TESTING.md) + records what was and was not exercised. - **A file inside an archive can be read without extracting it, and search can answer for the whole volume in one query instead of walking it.** Both were the obvious next question after diff --git a/docs/EDGE_CASE_TESTING.md b/docs/EDGE_CASE_TESTING.md index fdcc6c8..7fbdda6 100644 --- a/docs/EDGE_CASE_TESTING.md +++ b/docs/EDGE_CASE_TESTING.md @@ -151,7 +151,9 @@ through the prompt, because the prompt was already covered in the M7 session. have been reported as "video thumbnails don't work" against a build nobody had run. Registering the decoder explicitly against an `ImageLoader` turns it into a code reference, and the release APK was then installed and driven to confirm it. The lesson generalises: **audit the release build, not the - debug one**, and never trust a registration that happens by naming a class in a file. + debug one**, and never trust a registration that happens by naming a class in a file. That lesson + is now a step rather than a note — [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) § 4 carries + the install commands, the paths to walk, and what to do when something differs. - **The search field lied about its own scope inside a library, and only a run could show it.** Opening the field from a library left the scope on "this folder": the hint said *Search this diff --git a/docs/MONETISATION.md b/docs/MONETISATION.md index d62b924..28390de 100644 --- a/docs/MONETISATION.md +++ b/docs/MONETISATION.md @@ -73,7 +73,7 @@ $ANDROID_HOME/build-tools/36.0.0/aapt2 dump permissions \ Checked 2026-10-14 against the release APK: **no billing reference in the source, no Billing dependency in the version catalog, no `com.android.vending.BILLING` permission, and no -`INTERNET`**. See [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 7. +`INTERNET`**. See [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 8. ## Console steps diff --git a/docs/PLAY_RELEASE_CHECKLIST.md b/docs/PLAY_RELEASE_CHECKLIST.md index f7f2c15..a9124c6 100644 --- a/docs/PLAY_RELEASE_CHECKLIST.md +++ b/docs/PLAY_RELEASE_CHECKLIST.md @@ -48,7 +48,7 @@ rest are things you must do in the Play Console or with your own accounts. (`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 + permission set in step 5 — **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 @@ -59,6 +59,20 @@ rest are things you must do in the Play Console or with your own accounts. 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. +- [x] **[code] re-verified 2026-10-17.** `bundleRelease` and `assembleRelease` both succeed with + R8 and resource shrinking on. + - `app-release.aab` is **5 181 628 bytes** + (`sha256 f053d90f2f7c22727da5607af6e0f8b9a0322849ec3488356cd3d049e3d2d08c`). + - `app-release.apk` is **2 294 021 bytes** + (`sha256 afe03deab97dae672ad4c2d887be1b980d8ffe45544a5b97bc6c9cb23b80e8c7`), signed by + the upload key above. + - Both report `versionCode=1 versionName=1.0.0 targetSdk=36` and the permission set in + step 5 — still **no `INTERNET`**, and now also no `READ_MEDIA_*`. + - What moved the numbers since 2026-10-15: the library views and their index (schema v3), + the duplicate finder, and video thumbnails — the last of which added + `io.coil-kt.coil3:coil-video`. More importantly, this is the first build of which the + **release APK itself was run on a device** (section 4), which is how a defect that had + passed every other check was found. - ⚠️ **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. @@ -72,7 +86,51 @@ rest are things you must do in the Play Console or with your own accounts. - [ ] 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 +## 4. Run the release build, on a device, before shipping + +**Nothing here is automated, and this is the step that catches what the debug build hides.** A +minified, shrunk and obfuscated build is a *different program* from the one the tests and the debug +APK exercise: R8 removes what it cannot see being used, and a feature that works everywhere else can +be inert in the artefact you are about to publish. + +That is not hypothetical. Video thumbnails were exactly this (2026-10-17, recorded in +[EDGE_CASE_TESTING](EDGE_CASE_TESTING.md)): `coil-video` registers its frame decoder through a +`META-INF/services` entry, R8 removed both the entry and the class it named, and the debug APK drew +frames while the release APK would have shown an icon to every user. No unit test, no lint run and +no debug build could have caught it — every check above passed. + +- [ ] Install the **signed release** APK. It is a different package from the debug build, so both + can be installed side by side, which makes the comparison trivial: + ```bash + adb install -r app/build/outputs/apk/release/app-release.apk + adb shell appops set de.softwareapp_hb.filemanager MANAGE_EXTERNAL_STORAGE allow + adb shell pm grant de.softwareapp_hb.filemanager android.permission.READ_EXTERNAL_STORAGE + ``` + ⚠️ On MIUI the install waits for its own confirmation dialog, which times out on its own — + a bare `INSTALL_FAILED_USER_RESTRICTED: Install canceled by user` means the dialog was + answered late, not that the APK is bad. +- [ ] Use the **demo library** from section 7 for the walk rather than whatever happens to be on + the phone: it is generated, it holds images, a real ZIP and — with one `ffmpeg` command, also + in section 7 — real video, and it keeps a reviewer's own files out of the way. +- [ ] Walk the paths most likely to depend on something R8 cannot see: + - **Thumbnails — images *and* video**, since a video frame arrives through a registered + decoder. + - **One operation long enough to show the progress notification**, and the foreground + service behind it. + - **Search everywhere** and **Trash**, which both open an SQLite database. + - **One of each built-in viewer**: image, text, PDF, media, archive, APK. + - **Receiving a share**, and **"open with"**, which run through the `FileProvider`. +- [ ] Confirm what the *installed* build actually holds, not only what the APK declares: + ```bash + adb shell dumpsys package de.softwareapp_hb.filemanager | grep -A20 'requested permissions' + ``` +- [ ] When something behaves differently from the debug build, **assume R8 removed something** + rather than that the feature is broken. The usual culprits are a class named only from a + resource (`META-INF/services`) and one reached only by reflection: register it in code, or add + a keep rule, and say why in a comment so it is not removed again. +- [ ] Re-measure the figures in section 3 afterwards — a keep rule or a new dependency moves them. + +## 5. Sensitive permissions — the Permissions Declaration Form This is the step that can block a file manager, and the one to get right. @@ -117,10 +175,10 @@ No `READ_MEDIA_*` appears, and that is deliberate. Browsing and every read go th 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 +permissions that are never used — which is exactly what section 5's "keep the permission set honest" rule forbids. -## 5. Data safety, content rating, audience +## 6. 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. @@ -133,7 +191,7 @@ set honest" rule forbids. - [ ] Content rating questionnaire: utility, no user-generated content, no ads. - [ ] Target audience: not directed at children. -## 6. Store listing +## 7. 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 @@ -153,7 +211,7 @@ graphic 1024×500, and the screenshots. 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 +## 8. Release tracks and pricing - [ ] Create the app, complete *App content*, then roll out **internal → closed → production**, checking the pre-launch report on the way. diff --git a/docs/STORE_LISTING.md b/docs/STORE_LISTING.md index 6b07a60..321cf2b 100644 --- a/docs/STORE_LISTING.md +++ b/docs/STORE_LISTING.md @@ -4,7 +4,7 @@ Ready-to-paste copy for the *Main store listing* page, plus the graphics that go Every graphic is generated from the app's own vectors, so a rebrand is a re-run rather than a redraw. -See [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) § 6 for where this fits in the +See [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) § 7 for where this fits in the release. ## App name