Play gives phones and each tablet size a screenshot slot of their own (Phone, 7-inch
tablet, 10-inch tablet) and asks for at least four screenshots per large-screen form
factor, so the normaliser takes FORM_FACTOR=phone|tablet-7in|tablet-10in and keeps one
raw and one output directory per factor:
store-assets/raw-screenshots/<form-factor>/ adb captures (git-ignored)
store-assets/screenshots/<form-factor>/ what gets uploaded
The tablet form factors crop 0 px of system chrome on purpose, and that is checked
rather than guessed: every tablet profile in the SDK's own device definitions is already
inside Play's 2:1 cap (7in WSVGA 1024x600, 10.1in WXGA 1280x800, Nexus 7 2013
1200x1920, and Nexus 10 / Medium Tablet / Pixel Tablet at 2560x1600), so the 93/130 px
phone crop exists only because a 1080x2400 phone frame is 2.22:1. Cropping a tablet is
therefore cosmetic, and is now opt-in through STATUS_BAR_PX / NAV_BAR_PX.
The 2:1 clamp still runs for every form factor, and each file is now checked against
Play's limits as it is written (shorter side >= 320 px, longer side <= 3840 px) - the
phone captures only ever got that check by hand. A missing input directory or an unknown
form factor now fails with an actionable message instead of a bash error.
The four existing phone captures move to raw-screenshots/phone/ and normalise to the
same 1080x2160 RGB files as before, byte for byte. The 7-inch and 10-inch captures were
taken with this script on AOSP AVDs; how, and the numbers they show, are recorded in
docs/STORE_LISTING.md.
tools/make-store-assets.sh now prints the two tablet invocations as well, so the obvious
next step after generating the icon and feature graphic is not the phone-only one.
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.
An Android duplicate-photo finder. Pure-Kotlin core (SHA-256 + dHash hashing, exact and
perceptual union-find grouping, three sensitivity presets) with MediaStore access, a
persistent SQLite hash index and a tiered ScanEngine behind a Jetpack Compose UI.
No INTERNET permission, no ads, no analytics, no account.
Ships versionCode 2 / versionName 1.0.1, carrying the scan-stall hardening (BoundedCall
breaker) and verified against a 5,363-photo device library. 80 JVM unit tests, lint clean.
Release paperwork is in docs/ (Play checklist, store listing, edge-case testing), the
upload-ready Play graphics and screenshots in store-assets/, and the generators for those
assets in tools/.