Files
foto-duplicate-finder/docs/STORE_LISTING.md
T
SoftwareApp 3581ffc6bb 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.
2026-10-13 23:34:12 +02:00

10 KiB
Raw Blame History

Play Store listing (M10)

Everything the Main store listing page in the Play Console asks for, ready to copy. The graphics are generated from the app's own vectors, so they can be regenerated after any branding change instead of being edited by hand.

See PLAY_RELEASE_CHECKLIST § 4 for where these fit in the release.

Graphics

Generated into store-assets/ inside the repository, never into $HOME. These PNGs are the files that get uploaded to the Play Console, so they are committed and versioned with the release they belong to. Only the uncropped captures the screenshots are cut from (store-assets/raw-screenshots/) are git-ignored.

./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
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)
Feature graphic 1024×500, JPEG or 24-bit PNG without alpha store-assets/feature-graphic-1024x500.png — 1024×500, RGB tools/store-assets/feature-graphic.svg
Phone screenshots JPEG or 24-bit PNG without alpha, shorter side ≥ 320 px, longer side ≤ 3840 px, longer side ≤ 2× shorter side store-assets/screenshots/0{1,2,3}-*.png — 1080×2160, RGB tools/make-store-screenshots.sh, from raw adb exec-out screencap captures in store-assets/raw-screenshots/

Two details worth keeping in mind:

  • The icon is full bleed: the adaptive-icon background covers all 108 dp, so every pixel of the PNG ends up opaque and Play's mask can crop to any shape without showing a gap.
  • A phone screenshot at the test device's native 1080×2400 is 2.22:1 and therefore rejected — Play's limit is 2:1. The normaliser crops the status bar (93 px) and the navigation bar (130 px) and clamps to 1080×2160, which removes system chrome only, never app content. Re-measure both insets if you shoot on another device and pass STATUS_BAR_PX / NAV_BAR_PX.

Screenshots

Captured on the test device (1080×2400, Android 12 / MIUI), cropped to 1080×2160:

File Screen
01-empty.png Intro / empty state before the first scan
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

⚠️ 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. 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.

    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.
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.

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:

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,
  • Android's confirmation dialog for moving photos to the trash.

Store listing copy

App name (max 30 characters)

Foto Dup Finder

Short description (max 80 characters)

Find and remove duplicate photos. Free, ad-free, works offline.

Alternative, if the no-upload promise should lead:

Find and delete duplicate photos. No ads, no uploads, no account.

Full description (max 4000 characters)

Find and remove duplicate photos, then get the space back. Foto Dup Finder does one
job and does it without ads, without an account and without ever uploading anything.

WHY IT IS DIFFERENT
• No ads — no banners, no interstitials, no upsells.
• No internet permission at all, so the app has no way to upload your photos.
• No account, no sign-in, no tracking, no analytics.

WHAT IT DOES
• Finds exact copies: files that are byte-for-byte identical, even when their names
  differ.
• Finds near-duplicates: the same picture saved twice, or two shots of the same
  scene. A perceptual hash compares how the images actually look rather than only
  their bytes, so a re-compressed or slightly re-cropped copy is still caught.
• Suggests what to keep. The best copy of each group is marked BEST, and
  "Select all but best" marks the rest in a single tap.
• Lets you choose how far to go: Exact copies only, Balanced (recommended) or
  Aggressive.
• Deletes safely. Every removal goes through Android's own confirmation dialog, and
  you choose between moving photos to the trash (recoverable) and deleting them
  permanently. Nothing is removed without your confirmation.
• Copes with a messy library. RAW files and unreadable or corrupt images are skipped
  and reported instead of stopping the scan.
• Much faster the second time. File hashes are remembered on the device, so
  re-scanning an unchanged library takes a fraction of the first run.

HOW IT WORKS
1. Grant photo access — everything runs on your device.
2. Tap Start scan. A library of several thousand photos is done in seconds.
3. Review the groups, keep the copies you want and select the rest.
4. Tap "Remove all selected" and confirm once for the whole batch.

YOUR PHOTOS STAY YOURS
Foto Dup Finder declares no internet permission, ships no advertising or analytics
SDKs, and stores nothing besides a local index of file hashes so the next scan can be
quick. No photo ever leaves your phone, and uninstalling the app removes the index
with it.

GOOD TO KNOW
• Requires Android 8.0 or newer.
• Photos moved to the trash can be restored from your gallery app until the trash is
  emptied (Android 11 and newer).
• Works fully offline, in flight mode, always.

Every claim above is checkable against this repository: no INTERNET permission in the merged manifest, no ad or analytics dependency in gradle/libs.versions.toml, minSdk 26 in app/build.gradle.kts, and the second-run speed-up is the persistent hash index in data/PhotoIndexStore.kt.

Category and tags

  • Category: Tools (alternative: Photography — Tools matches the one-job utility framing better, and Photography is more crowded).
  • Tags: duplicate photos, photo cleanup, storage.
  • Contact details: the same e-mail as in PRIVACY.md — replace the support@example.com placeholder there first.