Add a generated demo photo library for store screenshots

The results screenshot is a screenshot *of a photo library*, so the committed
capture showed the developer's own photos. tools/make-demo-photos.py draws the
library instead: 22 procedural scenes (sunset, mountains, beach, conifers in
haze, dunes, city skyline, night sky, bokeh) with grain and a vignette, so there
are no people, no third-party content and nothing to license.

The duplicate structure is deliberate, because a folder of unrelated pictures
demonstrates nothing:

- headline group: one scene re-saved at falling quality and size until its
  reclaimable size reaches --target-mb (45 MB). Results are ranked by
  reclaimable bytes, so this group has to out-rank whatever is already on the
  device or a real photo is the first thing back in frame.
- exact pair: two byte-identical files, for the SHA-256 path.
- singles: the other scenes, once each, so the library looks lived-in.

Verified with the app's own engine on the desktop before pushing anything:
22 images -> 2 groups (PERCEPTUAL 14 copies, EXACT 2 copies) with the six
singles left alone. Regeneration is byte-identical, the output path resolves
from __file__ so it runs from any cwd, and nothing is written outside the repo.

demo-photos/ is git-ignored, like fixtures/ and store-assets/raw-screenshots/.
STORE_LISTING.md documents the library, why the headline group has a target
size, the MediaProvider scan_file recipe and the cleanup step; ROADMAP, README
and the release checklist point at it.
This commit is contained in:
2026-10-13 23:34:12 +02:00
parent 1145944659
commit 3581ffc6bb
6 changed files with 534 additions and 14 deletions
+70 -7
View File
@@ -17,6 +17,7 @@ release they belong to. Only the uncropped captures the screenshots are cut from
```bash
./tools/make-store-assets.sh # icon + feature graphic -> store-assets/
./tools/make-store-screenshots.sh # crop the adb captures -> store-assets/screenshots/
python3 tools/make-demo-photos.py # synthetic photo library -> demo-photos/ (git-ignored)
```
| Asset | Play's spec | Result | Source |
@@ -45,19 +46,81 @@ Captured on the test device (1080×2400, Android 12 / MIUI), cropped to 1080×21
| `02-scanning.png` | Scanning progress — *“Looking for visually similar photos…”* 5341 / 5363 |
| `03-results.png` | Results — 15 duplicate groups, 39.8 MB reclaimable, `BEST` badge, selection bar |
⚠️ **Two caveats before these go live.**
⚠️ **Both caveats below are outstanding for the files committed today.** One re-shoot fixes both.
1. All three still show the *“Photo access needed”* banner. The test device (Xiaomi M2101K7AG
/ MIUI) refuses `pm grant`, `install -g` and synthetic taps, so the runtime permission was
never actually granted there — the scan was driven with a debug-only
`MANAGE_EXTERNAL_STORAGE` app-op, which the app's permission check does not honour (see
[EDGE_CASE_TESTING](EDGE_CASE_TESTING.md)). **Re-shoot on a clean install after tapping
`MANAGE_EXTERNAL_STORAGE` app-op. That app-op gets the scan through MediaStore but does
**not** satisfy the app's own `checkSelfPermission`, which is why the banner — and the
disabled *Start scan* — are genuinely in frame. **Re-shoot on a clean install after tapping
“Grant access”**: that also closes the one remaining device-only verification gap.
2. The results screenshot shows real photos from the test library. They are the developer's
own, so they can be published, but check that nothing personal is recognisable first.
Screens still worth capturing once a device grants access through the real flow — each needs
one tap, which is exactly what the MIUI test device cannot be driven to do:
Enabling *USB debugging (Security settings)* in the MIUI developer options turns synthetic
taps and `pm grant` back on. That is what would let the capture run from a script instead of
by hand.
2. `03-results.png` currently shows real photos from the test library. They are the developer's
own, so nothing is licensed against us, but the library is not ours to publish. The next
capture must be taken over the generated demo library below.
### Demo library: generated photos, so none of anyone's real ones are published
`03-results.png` is a screenshot *of a photo library*, so whatever is on the test device ends up
in the store listing along with it. `tools/make-demo-photos.py` exists so that library can be
generated instead of borrowed: every scene is drawn procedurally — sunset, mountains, beach,
conifers in haze, dunes, city skyline, night sky, bokeh — so there are no people, no third-party
content and nothing to license. It writes to `demo-photos/foto-dup-demo/`, which is git-ignored
and regenerated on demand, exactly like `fixtures/`.
The duplicate structure is deliberate, because a folder of unrelated pictures demonstrates
nothing:
- **headline group** — one scene re-saved at falling quality and size until its reclaimable
size reaches `--target-mb` (default 45 MB),
- **exact pair** — two byte-identical files, for the SHA-256 path,
- **singles** — the other scenes, once each, so the library looks lived-in.
```bash
python3 tools/make-demo-photos.py
adb push demo-photos/foto-dup-demo /sdcard/DCIM/foto-dup-demo
adb shell content call --uri content://media --method scan_file \
--arg /sdcard/DCIM/foto-dup-demo
```
Two non-obvious details, both load-bearing:
- **Why the headline group has a target size.** Results are ranked by reclaimable bytes, so the
first group on screen is the largest one. A demo group smaller than anything already on the
device would be pushed below the fold and a real photo would be back in frame. 45 MB clears
the test device's own 39.8 MB with room to spare; raise `--target-mb` if you capture on a
library whose biggest group is larger.
- **`scan_file` on the folder, not on the volume.** It indexes just that path (API 30+) instead
of forcing a MediaStore-wide rescan on a 5,000-photo device. It does recurse into the folder,
and re-running it after the folder is deleted drops those rows again — both verified on the
test device.
Check the grouping *before* pushing, rather than reading it off the phone: the desktop harness
runs the app's real engine over a scaled copy of the library and reports the two intended groups
and nothing else.
```bash
magick mogrify -resize 1024x -quality 90 -path build/demo-validate-raw demo-photos/foto-dup-demo/*.jpg
python3 tools/normalise-for-local-grouping.py build/demo-validate-raw build/demo-validate
./gradlew :app:testDebugUnitTest --tests '*LocalGroupingTest*' \
-DfotoLocal.dir="$PWD/build/demo-validate"
# images=22 groups=2 -> PERCEPTUAL 14 copies, EXACT 2 copies; the six singles stay ungrouped
```
Cleanup, once the captures are done:
```bash
adb shell rm -rf /sdcard/DCIM/foto-dup-demo
adb shell content call --uri content://media --method scan_file --arg /sdcard/DCIM/foto-dup-demo
```
Screens still worth capturing once a device grants access through the real flow (the capture run
needs two taps on the phone — *Grant access*, then *Scan* — which is the one thing the MIUI test
device will not accept from a script):
- the **Options** sheet with the three sensitivity presets,
- the staged state (*“All duplicates are ready to go”*) after selecting duplicates,