4706e2669c
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/.
138 lines
7.2 KiB
Markdown
138 lines
7.2 KiB
Markdown
# 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](PLAY_RELEASE_CHECKLIST.md) § 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.
|
||
|
||
```bash
|
||
./tools/make-store-assets.sh # icon + feature graphic -> store-assets/
|
||
./tools/make-store-screenshots.sh # crop the adb captures -> store-assets/screenshots/
|
||
```
|
||
|
||
| 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 |
|
||
|
||
⚠️ **Two caveats before these go live.**
|
||
|
||
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
|
||
“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:
|
||
|
||
- 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](../PRIVACY.md) — replace the
|
||
`support@example.com` placeholder there first.
|