diff --git a/README.md b/README.md index 310dc5b..d6907f6 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,10 @@ selects the 7-inch pipeline, and the tablet runs deliberately crop no system chr tablet frame is already inside Play's 2:1 cap. All three sets are captured — the phone set on a real device, the tablet sets on AOSP AVDs; see [docs/STORE_LISTING.md](docs/STORE_LISTING.md). `python3 tools/make-demo-photos.py` draws the synthetic photo library the results screenshot is -captured over, so the listing never shows a real one. +captured over, so the listing never shows a real one. The finished assets are **committed** — +including the foreground-service demonstration video that [the declaration](docs/PLAY_RELEASE_CHECKLIST.md) +links to — and Gitea serves them without a login (`…/raw/branch/main/store-assets/…`), so pushing +is the deployment step and that URL is the link itself. User-visible changes are tracked in [CHANGELOG.md](CHANGELOG.md); the Play "What's new" text you type per release is separate and lives only in the Play Console. diff --git a/docs/PLAY_RELEASE_CHECKLIST.md b/docs/PLAY_RELEASE_CHECKLIST.md index 929054c..76ef936 100644 --- a/docs/PLAY_RELEASE_CHECKLIST.md +++ b/docs/PLAY_RELEASE_CHECKLIST.md @@ -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: diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index dd57bbb..391c249 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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 ``, 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) | diff --git a/docs/STORE_LISTING.md b/docs/STORE_LISTING.md index eb664d3..467df9a 100644 --- a/docs/STORE_LISTING.md +++ b/docs/STORE_LISTING.md @@ -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/ +``` + +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