3581ffc6bb
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.
6.9 KiB
6.9 KiB
Roadmap
Foto Dup Finder is built against a fixed milestone plan (M0–M10). This file is the
single source of truth for what is done and what remains. Before this file existed the
plan lived only in developer notes, and the CHANGELOG refers to
milestone labels (M7, M8).
Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (proposed, not scheduled)
Milestones
| # | Milestone | Status | Evidence / notes |
|---|---|---|---|
| M0 | Toolchain & scaffold | ✅ | User-local SDK, Gradle 9.3.1 + AGP 9.1.0, wrapper and version catalog, empty debug APK builds |
| M1 | Pure-Kotlin duplicate engine + JVM tests | ✅ | SHA-256 quick/full hashing, pHash + RGB dHash, union-find grouping, Exact/Balanced/Aggressive policies; tests under app/src/test |
| M2 | Data layer | ✅ | MediaStore query, coroutine-parallel hashing, incremental SQLite hash index, permission + partial-access handling |
| M3 | Compose UI | ✅ | Scan progress, grouped results with thumbnails, two-step staging, selection helpers |
| M4 | Safe deletion | ✅ | MediaStore.createDeleteRequest / createTrashRequest; legacy direct delete on Android 10 |
| M5 | Performance pass | ✅ | Size short-circuit, provider-thumbnail hashing, batched hashing (folded into M2/M3) |
| M6 | Release config & Play readiness | ✅ | R8/minify, optional signing, .aab, PLAY_RELEASE_CHECKLIST, PRIVACY.md, README |
| M7 | On-device validation & hardening | ✅ | Foreground dataSync service, one failure boundary per file, core/DecodeMath, hostile fixtures (EDGE_CASE_TESTING), 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); 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. 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)
| Item | Detail |
|---|---|
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 |
|
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 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 |
| 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 |
| 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)
| Candidate | Rationale |
|---|---|
| Colour signature | Flat-colour images (blank scans, dark UI screenshots) hash identically regardless of colour, so they are reported as mutual duplicates — see the “Known gap” in EDGE_CASE_TESTING. A small colour histogram is orthogonal to the grayscale structure hash, and the benchmark harness already has an implementation to measure against |
| Video support | Only images are scanned today (README “Known limitations”) |
| “Similar shots” section | Bursts / same-subject shots as a separate, never-pre-selected list. Needs a different algorithm and must not feed the deletion flow |
| Warm-scan speed | A second scan is faster (11.0 s) but its “similar” stage still costs ~10 s even though all 5,357 perceptual hashes are cached — the per-photo withTimeoutOrNull wrapper still runs for all 5,363 photos. Profiling this could make an unchanged library re-scan in well under a second |
Suggested order
- ✅ Done — the scan-stall hardening and the
BoundedCallfix shipped as 1.0.1 (versionCode2). Only the on-device re-check of the 5,363-photo scan remains. - Finish the Play Console paperwork and roll out through internal → closed → production.
- ✅ 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, 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).
- Colour signature — the flat-colour false-positive fix.
- M9 — monetisation, once the product decision is made.
Deliberately deferred: video support and the “similar shots” section.