docs: follow the asset publication through the documents that described it

Committing the store assets changed a rule that four files stated, so they now state the new one
instead of contradicting the tree:

- STORE_LISTING: "screenshots are not committed" becomes what is committed, why it is safe (every
  published frame is generated demo content), and what is still deliberately out of git (raw
  captures, demo photos, the video's milestone PNGs). It also spells out the public URL pattern,
  because that is now the deployment step rather than a footnote.
- PLAY_RELEASE_CHECKLIST: the two "(git-ignored)" annotations on the screenshot directories become
  "(committed)", and step 5's video item carries the exact link to paste plus what was verified
  about it (200, video/mp4, Content-Disposition: inline, sha256 match) and the two traps in using
  it (pinned to main, so a re-shoot overwrites in place; and replacing it at all means resubmitting
  the declaration).
- ROADMAP: M10 no longer claims the screenshots are left uncommitted, and the declaration's blocker
  row loses its "hosting remains" clause - the file is published, so only pasting is left.
- README: says where the link comes from, so the next reader does not go looking for a host.
This commit is contained in:
2026-10-14 13:22:38 +02:00
parent 9fb5e7de3d
commit 1cb188a56b
4 changed files with 58 additions and 24 deletions
+24 -11
View File
@@ -111,7 +111,7 @@ action except the items marked **[code]**.
same vectors as the adaptive icon in `res/drawable/ic_launcher_*`.
- [x] **Feature graphic** 1024×500 PNG **[code]** — same script.
- [x] **Phone screenshots** (at least 2, up to 8) **[code]** — four are captured and normalised to
1080×2160 in `store-assets/screenshots/phone/` (git-ignored): empty state, scanning
1080×2160 in `store-assets/screenshots/phone/` (committed): empty state, scanning
(`5353 / 5385`), results (**17 duplicate groups, 54 of 5385 photos, 83.5 MB reclaimable**)
and the Options menu. They were shot after a real permission grant and over the generated
demo library (`tools/make-demo-photos.py`), so neither the "Photo access needed" banner nor
@@ -124,7 +124,7 @@ action except the items marked **[code]**.
`tools/make-store-screenshots.sh` does exactly that (status and navigation bars only).
- [x] **7-inch tablet screenshots** **[code]** — captured on a `Nexus 7 2013` AOSP Android 12 AVD
(1200×1920 portrait, `sw600dp`) and normalised to `store-assets/screenshots/tablet-7in/`
(git-ignored) with `FORM_FACTOR=tablet-7in ./tools/make-store-screenshots.sh`: the same four
(committed) with `FORM_FACTOR=tablet-7in ./tools/make-store-screenshots.sh`: the same four
screens as the phone set, over the same generated demo library — empty state, scanning
(*“Looking for visually similar photos…”* 20 / 22), results (**2 duplicate groups, 16 of 22
photos, 43.7 MB reclaimable**, scanned in 39.7 s) and the Options menu (✓ on *Balanced*).
@@ -275,15 +275,28 @@ action except the items marked **[code]**.
INSTALL=0 WARMUP=0 ./tools/record-fgs-demo.sh 2a2ae895
```
The output is `store-assets/fgs-demo/foreground-service-demo.mp4` (git-ignored, like the
screenshots) with a checkpoint PNG per milestone beside it. It was checked frame by frame
afterwards rather than assumed: the intro at 0 s, the scan starting (*"Comparing file
signatures… 91/102"*, 8 s), the shade held open for ~6 s with the live notification
(*"Finding duplicate photos / Looking for visually similar photos… / CANCEL"*, 13–18 s)
while the scan kept running behind it, and the finished results (*17 duplicate groups ·
54 of 5,385 photos*) at the end. Host the file where a reviewer can open it without
signing in — an unlisted YouTube link is the usual choice — and paste that link into the
declaration. Replacing the video later means resubmitting it.
The output is `store-assets/fgs-demo/foreground-service-demo.mp4` (**committed**, so it is
already published; the checkpoint PNG per milestone beside it stays git-ignored). It was
checked frame by frame afterwards rather than assumed: the intro at 0 s, the scan starting
(*"Comparing file signatures… 91/102"*, 8 s), the shade held open for ~6 s with the live
notification (*"Finding duplicate photos / Looking for visually similar photos… /
CANCEL"*, 13–18 s) while the scan kept running behind it, and the finished results
(*17 duplicate groups · 54 of 5,385 photos*) at the end.
**The link to paste** is the committed file's own URL — no login, no second host:
```
https://gitea.softwareapp-hb.de/fishfacegit/foto-duplicate-finder/raw/branch/main/store-assets/fgs-demo/foreground-service-demo.mp4
```
Checked from outside this network: **200**, `Content-Type: video/mp4`,
`Content-Length: 1847900` and `Content-Disposition: inline`, so a reviewer's browser plays
it in the tab rather than prompting a download, and the served bytes are identical to the
recording (sha256 match). Two things to know before pasting: the URL is pinned to `main`,
so a re-shoot has to **overwrite the file in place** rather than be added alongside it; and
replacing the video at all means resubmitting the declaration.
(An unlisted YouTube upload of the same file works equally well if a link that survives
future commits is preferred — but then it is a second place to keep in step.)
Both flags exist because this take found two traps worth knowing before a re-shoot:
+2 -2
View File
@@ -21,7 +21,7 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo
| M7 | On-device validation & hardening | ✅ | Foreground `dataSync` service, one failure boundary per file, `core/DecodeMath`, hostile fixtures ([EDGE_CASE_TESTING](EDGE_CASE_TESTING.md)), `versionName` → 1.0.0 |
| M8 | Localisation groundwork | ✅ | Every user-visible string in `res/values/strings.xml` with real `<plurals>`, shared `text/TextRes.kt`; **English is the only locale** |
| M9 | Monetisation | ✅ | **Decided: a one-time paid app** — no free tier, no trial, no subscription and no in-app purchase, so there is **no Billing code** and no new permission. Checked before deciding: `com.android.billingclient:billing:9.1.0`'s manifest declares only `com.android.vending.BILLING`, so IAP would *not* have added `INTERNET` either — but a price on the listing is the one model that changes nothing at all about the "no internet permission" claim. Base price **€2.99** one-time with a **50% launch sale at €1.49**, both set in the Console (*Products > App pricing*). €1 as the *base* price was considered and rejected: a sale can always be added, but a low anchor cannot be undone, and a paid app's first transaction is its only transaction. Free → paid becomes impossible after the first upload, so this had to be settled before release. Rationale, copy changes and Console steps: [STORE_LISTING](STORE_LISTING.md) › App pricing, [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 4 |
| M10 | Store assets | ✅ | `tools/make-store-assets.sh` renders the 512×512 icon and the 1024×500 feature graphic from the app's own vectors; `tools/make-store-screenshots.sh` normalises device captures to Play's 2:1 limit, one directory per form factor (`phone` → 1080×2160, `tablet-7in` → 1200×1920, `tablet-10in` → 1600×2560); `tools/make-demo-photos.py` generates the photo library the results captures are taken over, so no real photos are published; all listing copy is drafted and length-checked in [STORE_LISTING](STORE_LISTING.md). The phone set was re-shot on a real device after a real permission grant, and the two tablet sets were captured on AOSP AVDs (see the blocker below); screenshots are deliberately not committed |
| M10 | Store assets | ✅ | `tools/make-store-assets.sh` renders the 512×512 icon and the 1024×500 feature graphic from the app's own vectors; `tools/make-store-screenshots.sh` normalises device captures to Play's 2:1 limit, one directory per form factor (`phone` → 1080×2160, `tablet-7in` → 1200×1920, `tablet-10in` → 1600×2560); `tools/make-demo-photos.py` generates the photo library the results captures are taken over, so no real photos are published; all listing copy is drafted and length-checked in [STORE_LISTING](STORE_LISTING.md). The phone set was re-shot on a real device after a real permission grant, and the two tablet sets were captured on AOSP AVDs (see the blocker below); the finished assets — icon, feature graphic, the twelve normalised screenshots and the demonstration video — are committed, so Gitea serves them publicly and pushing is the deployment step |
## Release blockers (work that is not a numbered milestone)
@@ -29,7 +29,7 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo
|---|---|
| ~~Large-library scan stall~~ ✅ **fixed — verified on device** | The 5,363-photo library used to freeze at *“74 / 100 · Comparing file signatures…”* (a **blocked syscall**: zero CPU, workers sleeping). `BoundedCall` now abandons a stuck provider call **and** trips a stall breaker after three consecutive timeouts. **Verified on the real test device (Android 12 / MIUI): the scan completes — 20.2 s cold, 11.0 s warm, 15 groups, 6 unreadable — where it used to hang.** Covered by `BoundedCallTest` and `ScanEngineEndToEndTest` |
| ~~Unreleased `BoundedCall` fix~~ ✅ | The provider-call deadline shipped in **1.0.1** (`versionCode` 2) |
| **Play Console: foreground-service declaration** | Declaring the `dataSync` type (App content › Foreground service permissions) is release-blocking. **[code]** the manifest already declares it, [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 5 carries all four things the form asks for (description, defer/interrupt impact, use case **Local processing: Other**, video link), and the **video now exists**: recorded on the test device on 2026-10-14 with `INSTALL=0 WARMUP=0 tools/record-fgs-demo.sh`, then verified frame by frame rather than assumed — intro at 0 s, the scan starting, **~6 s of the shade with the live "Finding duplicate photos / Looking for visually similar photos… / CANCEL" notification while the scan kept running**, and the results at the end. Only hosting it (an unlisted YouTube link) and pasting that link remain. Two details of the take are recorded in step 5 because they each cost an attempt: a *warm* scan is 5.4 s, shorter than the shade takes to open, so `WARMUP=0` films the cold one; and MIUI refuses `adb install` outright, so `INSTALL=0` records with the build already on the device. On a KVM-less emulator the same video cannot be made at all: every scan start dies as `am_crash … ForegroundServiceDidNotStartInTimeException` → *Force finishing activity* with no `am_kill`, i.e. emulation is too slow to reach `startForeground()` inside Android's budget (QEMU's `-icount` clock dilation made it work once, not reproducibly) |
| **Play Console: foreground-service declaration** | Declaring the `dataSync` type (App content › Foreground service permissions) is release-blocking. **[code]** the manifest already declares it, [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 5 carries all four things the form asks for (description, defer/interrupt impact, use case **Local processing: Other**, video link), and the **video now exists**: recorded on the test device on 2026-10-14 with `INSTALL=0 WARMUP=0 tools/record-fgs-demo.sh`, then verified frame by frame rather than assumed — intro at 0 s, the scan starting, **~6 s of the shade with the live "Finding duplicate photos / Looking for visually similar photos… / CANCEL" notification while the scan kept running**, and the results at the end. It is committed, and that is what published it: Gitea serves the raw file with `Content-Type: video/mp4` and `Content-Disposition: inline`, verified by hash from outside the network, so pasting that URL into the declaration is all that is left. Two details of the take are recorded in step 5 because they each cost an attempt: a *warm* scan is 5.4 s, shorter than the shade takes to open, so `WARMUP=0` films the cold one; and MIUI refuses `adb install` outright, so `INSTALL=0` records with the build already on the device. On a KVM-less emulator the same video cannot be made at all: every scan start dies as `am_crash … ForegroundServiceDidNotStartInTimeException` → *Force finishing activity* with no `am_kill`, i.e. emulation is too slow to reach `startForeground()` inside Android's budget (QEMU's `-icount` clock dilation made it work once, not reproducibly) |
| **Remaining Play paperwork** | Console actions only, no code: the **first upload + Play App Signing enrolment**, the **hosted privacy policy URL** (the working copy is now published to Gitea as [`fishfacegit/foto-duplicate-finder`](https://gitea.softwareapp-hb.de/fishfacegit/foto-duplicate-finder), so the policy renders at a public, login-free URL and only the Console paste is left), **Data safety**, the **Photo/video permissions** declaration, the **content rating / audience / app access / ads / export** questions, and the **listing copy + the three screenshot sets** (phone, 7-inch and 10-inch, all captured), plus the **paid-app setup**: a payments profile with tax rates, the €2.99 price and the 14-day €1.49 launch sale on *Products > App pricing* — the price must be set before the first upload, because free → paid is not possible afterwards (M9). Keystore, `versionCode`, listing copy, graphics, permission audit and the signed `.aab` (`sha256 f8e6a29a…`) are all in place; rollout goes internal → closed → production. Step-by-step: [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) |
| ~~Store screenshots need one re-shoot~~ ✅ **done — verified on device** | The old captures showed the *Photo access needed* banner (an app-op is not a grant) and `03-results.png` showed the developer's own library. After a real grant the set was re-shot over the generated demo library: **01-empty** (no tap needed — results and selection live in `ScannerViewModel`, so force-stop + relaunch returns to the empty state with the grant still in place), **02-scanning** (`5353 / 5385`), **03-results** (**17 groups, 54 of 5385 photos, 83.5 MB reclaimable**; the headline group's 43.1 MB out-ranks the device's own 39.8 MB, so no real photo is in frame) and **04-options**. Captures are taken by watching for UI **text** changes, because MIUI refuses synthetic taps (`INJECT_EVENTS`). The demo library is deleted from the test device afterwards — MediaStore **5,386 → 5,364** rows, and the 22 stale rows it leaves in the hash index are pruned by `PhotoIndexStore.pruneTo` on the next scan. Deliberately not captured: the staged state and the system confirmation dialog, because staging clears the demo group and would put real photos back in frame. Details: [STORE_LISTING](STORE_LISTING.md) |
| ~~Tablet store screenshots outstanding~~ ✅ **done — captured on AVDs** | Play has a **7-inch tablet** and a **10-inch tablet** screenshot slot of its own (asking for **≥ 4** screenshots on large screens). Both are now captured on AOSP Android 12 AVDs — `Nexus 7 2013` (1200×1920, `sw600dp`) and `medium_tablet` (1600×2560, `sw800dp`) — because this machine exposes no `/dev/kvm`: they ran in **pure software emulation** (`-accel off -no-window -gpu swiftshader_indirect`), ≈3 min to boot, with `hide_error_dialogs` on so SystemUI's ANR dialog could not sit in frame. Four screens per tablet over the same generated demo library: empty, scanning (20 / 22), results (**2 groups · 16 of 22 photos · 43.7 MB reclaimable**, 39.7 s / 53.1 s) and Options. `tools/make-store-screenshots.sh` is form-factor aware (`FORM_FACTOR=phone\|tablet-7in\|tablet-10in`) and the tablet form factors crop **0 px** of chrome, because a tablet frame is already inside Play's 2:1 cap. Recipe: [STORE_LISTING](STORE_LISTING.md) |
+28 -10
View File
@@ -9,12 +9,19 @@ release.
## Graphics
Generated into `store-assets/` **inside the repository**, never into `$HOME`. The icon and the
feature graphic are the files that get uploaded to the Play Console, so they are committed and
versioned with the release they belong to. **Screenshots are not committed** — neither the raw
captures (`store-assets/raw-screenshots/`) nor the normalised ones (`store-assets/screenshots/`) —
because a screenshot of this app is a picture of a photo library. Regenerate them with the
commands below and upload them straight from disk.
Generated into `store-assets/` **inside the repository**, never into `$HOME`, and **committed**:
the icon, the feature graphic, the twelve normalised screenshots and the foreground-service
demonstration video all live in git, versioned with the release they belong to. That is deliberate
— Play wants a *link* to the demonstration video, so an asset has to be reachable without a login,
and once one is, it is simpler to keep the whole listing beside the code that produces it.
Publishing them is safe because of where they are shot: every published frame contains the
**generated** demo library and nothing else (`tools/make-demo-photos.py`), so the screenshots show
the app *over* that library. The phone set was re-checked frame by frame before publication; the
sole trace of the test device's own 5,385-photo library in it is a count in the summary card. What
stays out of git is the scratch around them — the raw `adb` captures
(`store-assets/raw-screenshots/`), the demo photos themselves, and the recording script's milestone
PNGs. Regenerate any of it with the commands below and upload straight from disk.
Screenshots live **one directory per form factor**, because Play gives phones, 7-inch tablets and
10-inch tablets a slot each (`Main store listing` › *Graphics* › *Phone* / *7-inch tablet* /
@@ -22,7 +29,7 @@ Screenshots live **one directory per form factor**, because Play gives phones, 7
```
store-assets/raw-screenshots/{phone,tablet-7in,tablet-10in}/ git-ignored `adb` captures
store-assets/screenshots/{phone,tablet-7in,tablet-10in}/ the files that get uploaded
store-assets/screenshots/{phone,tablet-7in,tablet-10in}/ the files that get uploaded — committed
```
```bash
@@ -33,6 +40,18 @@ FORM_FACTOR=tablet-10in ./tools/make-store-screenshots.sh # 10-inch -> screens
python3 tools/make-demo-photos.py # synthetic photo library -> demo-photos/ (git-ignored)
```
Committing `store-assets/` is also what *publishes* it: Gitea serves every committed file without a
login, at
```
https://gitea.softwareapp-hb.de/fishfacegit/foto-duplicate-finder/raw/branch/main/<path>
```
so pushing the commit **is** the deployment step — there is no separate upload, and a re-shot
screenshot or re-recorded video goes live as soon as it is pushed. Play's foreground-service
declaration takes its video link from exactly there; the URL is written out in
[PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 5.
| Asset | Play's spec | Result | Source |
|---|---|---|---|
| App icon | 512×512, 32-bit PNG with alpha, ≤ 1 MB | `store-assets/icon-512.png` — 512×512, RGBA, 6 KB | `tools/store-assets/icon.svg`, mirrored from `app/src/main/res/drawable/ic_launcher_background.xml` + `ic_launcher_foreground.xml` (paths and group transforms are identical, so the store icon *is* the launcher icon) |
@@ -258,9 +277,8 @@ MediaStore id, so there is nothing else to reset.
Two further screens were weighed up and deliberately left out of the set: the staged state (*“All
duplicates are ready to go”*) after *Remove all selected*, and Android's confirmation dialog for the
batch that *Apply* launches. Staging every selected row also clears the demo group out of the list,
which promotes the device's own 39.8 MB group into frame — so those captures could not be published,
and since screenshots are not committed there would be nothing to keep them for. (Dismissing the
dialog is non-destructive, so nothing is lost by not going that far.)
which promotes the device's own 39.8 MB group into frame — so those captures could not be committed
or published. (Dismissing the dialog is non-destructive, so nothing is lost by not going that far.)
Captures are taken by hand, because each of these screens needs a tap and MIUI only accepts taps from
a human. A loop that takes a `uiautomator dump` and screencaps whenever the UI **text** changes is