# 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 - [x] 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`. - [x] Generate an **upload keystore** (keep it safe; if you lose it you cannot publish updates) — **[code]** done: `fotodup-upload.jks` in the project root (git-ignored, `alias upload`), `CN=Hadrian Burkhardt, L=Potsdam, C=DE`. - [x] Copy `keystore.properties.example` to `keystore.properties` and fill it in. **[code]** done — the file is in place, and 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: ```bash keytool -exportcert -alias upload -keystore fotodup-upload.jks -rfc > upload_cert.pem ``` Upload key fingerprint (SHA-256), to confirm the right key was registered: `AA:8C:3D:6C:D0:55:46:50:48:ED:04:3E:AF:34:2B:66:B4:13:87:C9:E0:DA:29:70:5A:0D:18:47:08:1A:4C:7A`. Enrolment happens with the first upload: Google then holds the app signing key and this one only signs uploads. - [ ] Store the keystore + passwords in a password manager / secure vault. Back it up. ## 2. Versioning - [x] Set `versionName` (user-visible) **[code]** — `1.0.1` in `app/build.gradle.kts`. - [x] `versionCode` **[code]** — currently **2**. It is the counter Play orders uploads by, so the first upload will be 2 and every later upload must be higher. ## 3. Build the release artefacts ```bash ./gradlew :app:bundleRelease # -> app/build/outputs/bundle/release/app-release.aab ./gradlew :app:assembleRelease # -> APK for local testing ``` - [x] **[code]** verified 2026-10-13: `bundleRelease` and `assembleRelease` both succeed. `app/build/outputs/bundle/release/app-release.aab` is 2,944,100 bytes (`sha256 f8e6a29a0daf60d025409e19be878c3e8f1359a06b3f9fca300bb4aec9ebb726`) and is signed with the upload key. The release APK reports `versionCode=2 versionName=1.0.1 targetSdk=36` and exactly the permission set listed in step 5 — **no `INTERNET`**, so "nothing leaves the device" holds in the shipped artefact, not just in the source manifest. - [ ] 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](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. - [x] **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_*`. - [x] **Feature graphic** 1024×500 PNG **[code]** — same script. - [x] **Screenshots** (at least 2, up to 8) **[code]** — four are captured and normalised to 1080×2160 in `store-assets/screenshots/` (git-ignored): empty state, scanning (`5353 / 5385`), results (**17 duplicate groups, 54 of 5385 photos, 83.5 MB reclaimable**) and the Options menu. They were shot after a real permission grant and over the generated demo library (`tools/make-demo-photos.py`), so neither the "Photo access needed" banner nor a personal photo is in frame; the demo library is deleted from the test device again once the captures are done. Upload the four, then drag **03-results** to first place — the results screen is the one that sells, and the empty state is the weakest opener. ⚠️ 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](../PRIVACY.md) at a public URL and paste that URL into the Play Console. **This is mandatory** because the app accesses photos. The contact address in it is final (`softwareapp.hb@gmail.com`), so the file publishes as-is. This is the one item here that cannot be finished from the working copy, because the repo has **no git remote** yet. Shortest route: 1. create a public repository and push this working copy: ```bash git remote add origin git@github.com:/foto-dup-finder.git git push -u origin main ``` 2. **Settings › Pages › Source: Deploy from a branch**, branch `main`, folder `/ (root)`; 3. the policy then renders at `https://.github.io/foto-dup-finder/PRIVACY.html` — open that in a private window to prove it needs no login, then paste it into **App content › Privacy policy**. Any static host works the same way; the requirement is only that the URL is public, stable and readable without an account. > **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 ' 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` | `` | | `AndroidManifest.xml` | `` | | `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+). - [ ] **App access** → *All functionality is available without special access*: no login, no account, nothing gated. - [ ] **Ads** → *No, my app does not contain ads* (and no ad SDK in `gradle/libs.versions.toml` to contradict the answer). - [ ] Select the target audience (not designed for children). - [ ] Answer the remaining *App content* declarations **No** / not applicable: news app, government app, financial features, health, COVID-19 contact tracing. - [ ] 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 ⚠️ **Which block applies first:** `versionCode` is already **2** (`versionName` 1.0.1), because 1.0.1 was cut before anything reached Play — so the *first* upload is 1.0.1 and takes the **initial release** block. The 1.0.1 block below applies only once 1.0.0 exists on a track. 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 `` and ends with a line containing ``. Several `` 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](../CHANGELOG.md). Ready-to-paste block for the first release: 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 Ready-to-paste block for **1.0.1** (the scan-robustness patch): • 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 - [ ] 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.