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:
@@ -54,8 +54,10 @@ action except the items marked **[code]**.
|
||||
- [x] **Feature graphic** 1024×500 PNG **[code]** — same script.
|
||||
- [ ] At least 2 phone screenshots (up to 8). Three are captured (empty state, scanning,
|
||||
results), but **re-shoot them after granting photo access on a fresh install**: the
|
||||
existing ones still show the "Photo access needed" banner — see the caveat in
|
||||
STORE_LISTING.md.
|
||||
existing ones still show the "Photo access needed" banner, and the results capture shows
|
||||
the developer's own photo library. Capture the results screen over the generated demo
|
||||
library instead (`python3 tools/make-demo-photos.py`, then push and index it) — see the
|
||||
caveats in STORE_LISTING.md.
|
||||
⚠️ Play rejects a screenshot whose longer side is more than 2× its shorter side, so a
|
||||
1080×2400 phone capture has to be cropped to 1080×2160;
|
||||
`tools/make-store-screenshots.sh` does exactly that (status and navigation bars only).
|
||||
|
||||
+6
-5
@@ -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 | ⏳ | No billing code. Blocked on a product decision (free forever vs. one-time unlock vs. limited free scans); changes the Data Safety answers |
|
||||
| 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 (1080×2160); all listing copy is drafted and length-checked in [STORE_LISTING](STORE_LISTING.md). The captured screenshots still show the permission banner — see blockers |
|
||||
| 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 (1080×2160); `tools/make-demo-photos.py` generates the photo library the results capture is taken over, so no real photos are published; all listing copy is drafted and length-checked in [STORE_LISTING](STORE_LISTING.md). The captures committed today predate the demo library and still show the permission banner — see blockers |
|
||||
|
||||
## Release blockers (work that is not a numbered milestone)
|
||||
|
||||
@@ -31,7 +31,7 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo
|
||||
| ~~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; the ready-to-paste justification is in [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 5 — this is a Console action, not a code change |
|
||||
| **Remaining Play paperwork** | Keystore / Play App Signing, Data Safety form, hosted privacy policy and store listing — see [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) |
|
||||
| **Store screenshots still show the permission banner** | The three captures were taken with the debug-only `MANAGE_EXTERNAL_STORAGE` app-op, so the app's own permission check (correctly) still reported *no access* and the banner is in frame. Re-shoot on a clean install after tapping **Grant access** — the same run that closes the permission-flow verification gap. Files, specs and caveats: [STORE_LISTING](STORE_LISTING.md) |
|
||||
| **Store screenshots need one re-shoot** | Two things are wrong with the committed captures: they were taken with the debug-only `MANAGE_EXTERNAL_STORAGE` app-op, so the app's own permission check (correctly) still reported *no access* and the banner is in frame; and `03-results.png` shows the developer's real photo library. Both clear in a single run after tapping **Grant access**. ⚠️ An app-op cannot stand in for the grant — `appops set --uid <pkg> android:read_media_images allow` leaves the banner *and* a disabled *Start scan* — and MIUI refuses synthetic taps, so the re-shoot needs two real taps unless *USB debugging (Security settings)* is enabled in the developer options. The demo library that keeps real photos out of the capture is generated, validated and already indexed on the test device; files, specs and caveats: [STORE_LISTING](STORE_LISTING.md) |
|
||||
| **Device-only verifications** | RAW/DNG files, a *real* iPhone HEIC with a non-upright EXIF orientation, permission revoked mid-scan, and the Android 14 “Selected photos” flow. ✅ The **index-reuse** check is done: after a scan the index holds **5,363 rows, 5,357 with a perceptual hash**, and a second scan runs in **11.0 s vs 20.2 s** cold |
|
||||
|
||||
## Candidate milestones (proposed, not scheduled)
|
||||
@@ -49,9 +49,10 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo
|
||||
(`versionCode` 2). Only the on-device re-check of the 5,363-photo scan remains.
|
||||
2. Finish the Play Console paperwork and roll out through **internal → closed → production**.
|
||||
3. ✅ **Done** — **M10 store assets**: the icon and feature graphic are generated from the
|
||||
app's own vectors, the screenshot normaliser enforces Play's 2:1 limit, and the listing
|
||||
copy is drafted and length-checked. The only remainder is the screenshot re-shoot after a
|
||||
real permission grant (see blockers).
|
||||
app's own vectors, the screenshot normaliser enforces Play's 2:1 limit, the listing copy is
|
||||
drafted and length-checked, and the demo library that keeps real photos out of the results
|
||||
capture is generated and validated. The only remainder is the re-shoot after a real
|
||||
permission grant (see blockers).
|
||||
4. **Colour signature** — the flat-colour false-positive fix.
|
||||
5. **M9 — monetisation**, once the product decision is made.
|
||||
|
||||
|
||||
+70
-7
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user