Files
foto-duplicate-finder/docs/PLAY_RELEASE_CHECKLIST.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

12 KiB
Raw Blame History

Google Play release checklist

Work top to bottom. Items marked [code] are already handled in this repository; the rest are things you must do in the Play Console / with your own accounts.

1. Identity and signing

  • Final application id chosen: de.softwareapp_hb.fotodup.finder (applicationId in app/build.gradle.kts). It can never be changed after the first release, so treat it as locked. namespace and the Kotlin package of the same name are aligned with it; debug builds append .debug.
  • Generate an upload keystore (keep it safe; if you lose it you cannot publish updates): bash keytool -genkeypair -v -keystore upload-keystore.jks \ -alias upload -keyalg RSA -keysize 4096 -validity 10000
  • Copy keystore.properties.example to keystore.properties and fill it in. [code] The Gradle build picks it up automatically and signs release builds; without it the release APK stays unsigned.
  • Enable Play App Signing in the Play Console and upload the certificate of your upload key.
  • Store the keystore + passwords in a password manager / secure vault. Back it up.

2. Versioning

  • Set versionName (user-visible, e.g. 1.0.0) [code] in app/build.gradle.kts.
  • Increment versionCode for every upload.

3. Build the release artefacts

./gradlew :app:bundleRelease      # -> app/build/outputs/bundle/release/app-release.aab
./gradlew :app:assembleRelease    # -> APK for local testing
  • Upload the .aab (not the APK) to the Play Console.
  • Keep app/build/outputs/mapping/release/mapping.txt for every release so you can de-obfuscate crash reports (upload it under Release > App bundle explorer).

4. Store listing

Ready-to-paste copy and the generated graphics live in docs/STORE_LISTING.md (milestone M10). Everything below is a Console action except the items marked [code].

  • App name (≤ 30 chars): "Foto Dup Finder".
  • Short description (≤ 80 chars) — two options are drafted and length-checked in STORE_LISTING.md.
  • Full description (≤ 4000 chars) — drafted and length-checked in STORE_LISTING.md.
  • App icon 512×512 PNG [code] — tools/make-store-assets.sh renders it from the same vectors as the adaptive icon in res/drawable/ic_launcher_*.
  • 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, 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).
  • Category: Tools (or Photography). Tags: photo cleanup, duplicates.

5. Policy and compliance

  • Host PRIVACY.md at a public URL and paste that URL into the Play Console. This is mandatory because the app accesses photos. ⚠️ Before hosting: replace the support@example.com placeholder in the Contact section with a real address — a policy whose contact is a placeholder is a rejection risk.

    > **Note on Play's automatic translation:** it translates the *store listing*, not
    > the hosted policy. If you want the policy in another language, host a translated
    > copy yourself and link it from the listing.
    
  • Complete the Data safety form:

    • Does your app collect or share any required user data types? → No.
    • Data types accessed: Photos — processed on-device only, not collected.
    • Is all user data encrypted in transit? → N/A (no transmission).
    • Do you provide a way for users to request data deletion? → Uninstalling removes all app data.
  • Confirm the permission set the Console will show. Read it from the merged release manifest, not the source one, so nothing a library quietly adds slips past:

    | Declared permission | Why | From |
    |---|---|---|
    | `READ_MEDIA_IMAGES` | read the photo library | app |
    | `READ_MEDIA_VISUAL_USER_SELECTED` | Android 14 partial access | app |
    | `READ_EXTERNAL_STORAGE` (`maxSdkVersion=32`) | same, on Android ≤ 12 | app |
    | `WRITE_EXTERNAL_STORAGE` (`maxSdkVersion=29`) | legacy delete on Android 10 only | app |
    | `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_DATA_SYNC` | keep a scan alive | app |
    | `POST_NOTIFICATIONS` | scan-progress notification | app |
    | `…DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION` | framework-generated, not user-facing | AndroidX |
    
    Deliberately **absent**: `INTERNET` and `MANAGE_EXTERNAL_STORAGE`. Both are
    load-bearing for the "nothing leaves the device" claim, so re-check after any new
    dependency:
    
    ```bash
    ./gradlew :app:processReleaseMainManifest
    grep -A1 '<uses-permission' \
      app/build/intermediates/merged_manifests/release/processReleaseManifest/AndroidManifest.xml \
      | grep -oE 'android.permission.[A-Z_]+' | sort -u
    ```
    
    (Match `<uses-permission` only. A bare `android.permission.` grep also picks up
    `android.permission.DUMP`, which AndroidX puts on its profile-installer *receiver* —
    a component restriction, not a permission the app requests.)
    
  • Answer the Photo and video permissions declaration in the Console. The app requests READ_MEDIA_IMAGES (plus READ_MEDIA_VISUAL_USER_SELECTED for Android 14 partial access) and deletes only user-selected files through the system dialog. Paste:

    > Foto Dup Finder is a photo-cleanup tool. Reading the photo library **is** its core
    > functionality: it compares photos on the device to find exact and near duplicates
    > and lets the user remove the copies they select. Nothing is uploaded — the app
    > declares no network permission. Deletion always goes through the system consent
    > dialog (`MediaStore.createDeleteRequest`); the app never requests
    > `MANAGE_EXTERNAL_STORAGE`.
    
  • Declare the foreground service type. This is the release-blocking item for the Android 14+ review. [code] The manifest already matches what you will declare:

    | Where | Value |
    |---|---|
    | `AndroidManifest.xml` | `<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />` |
    | `AndroidManifest.xml` | `<service android:name=".service.ScanService" android:foregroundServiceType="dataSync" android:exported="false" />` |
    | `ScanService.kt` | `ServiceCompat.startForeground(…, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)` |
    | `ScanService.kt` | `onTimeout()` stops the scan at Android 15's 6-hour `dataSync` cap |
    
    In the Console go to **App content › Foreground service permissions**, select
    **Data sync**, and paste:
    
    > The app finds duplicate photos on the device. The user starts a scan and may leave
    > the app while it runs; the foreground service keeps that user-initiated scan alive
    > and shows its progress. The scan reads the photo library and writes a small local
    > index of file hashes. It performs no network access and transfers no data.
    
    The declaration is **per type**: if a new foreground-service type is ever added to
    the manifest, it has to be declared here too.
    
  • Complete the content rating questionnaire (no objectionable content → typically rated Everyone / 3+).

  • Select the target audience (not designed for children).

  • Accept the Developer Program Policies and US export laws declarations.

6. Testing before production

  • Create an internal testing track and add yourself as a tester; install via the Play link on a real device.
  • Test on the oldest supported Android version you can (minSdk 26) and on the newest you have (Android 16 / API 36).
  • Test the permission flows:
    • Deny access — app must not crash and must explain what is needed.
    • Android 14+ partial access ("Select photos") — banner appears and the app scans the subset.
    • Grant full access — scan works.
  • Test deletion:
    • Move to trash (Android 11+).
    • Permanent delete (Android 11+).
    • Refuse the system dialog — nothing is deleted, the app says so.
  • Test with a library of several thousand photos to sanity-check scan time.
  • Verify no crashes in Play Console > Quality > Android vitals after rollout.

7. Release notes ("What's new") and rollout

Release notes are not stored in this repo — they are typed into the Play Console for each release.

Where:

  • Production: Test and release > Production, then Create new release
  • Testing: Test and release > Testing > Internal / Closed / Open testing

Rules (per Google's Prepare and roll out a release guide):

  • Up to 500 Unicode characters per language.
  • Language tags go on their own line, wrapping the text — a block starts with a line containing <en-US> and ends with a line containing </en-US>. Several <lang> blocks can be entered together.
  • Notes belong to a release, so every track has its own; "Copy from a previous release" avoids retyping.
  • Google explicitly forbids using release notes for promotion or to solicit user actions — keep them factual about what changed.
  • For app updates list only user-visible changes; the full developer history belongs in CHANGELOG.md.

Ready-to-paste block for the first release:

<en-US>
Initial release.

• Finds exact and near-duplicate photos
• Suggests which copy to keep
• Moves photos to the trash or deletes them via the system dialog
• No ads, no account, works fully offline
</en-US>

Ready-to-paste block for 1.0.1 (the scan-robustness patch):

<en-US>
• Fixed a scan that could appear to stop on very large photo libraries
• The app now recovers gracefully when the photo library is temporarily unresponsive
</en-US>
  • Upload the .aab, paste the notes above, and roll out to internal → closed → production (staged rollout recommended).
  • Monitor Android vitals and reviews; respond to feedback.

8. Post-launch maintenance

  • Play requires new apps/updates to target recent API levels (API 36 from August 2026 [code]). Re-check this requirement each year and bump targetSdk.
  • Watch for newer AGP; when it supports it, update compileSdk to 37 and bump the Compose BOM and Coil together (see the version note in the README).
  • Optionally add crash reporting later — but note this would change the Data safety answers, so weigh it against the "no data leaves the device" promise.

9. Optional monetisation

The app currently has no ads and no billing, which is a deliberate differentiator. If you later add a paid tier, use Play Billing (e.g. a one-time "Pro" purchase) and update:

  • the Data safety form (purchase history),
  • the privacy policy,
  • the store description.

Avoid ad SDKs entirely — they would contradict the app's main selling point.