Files
foto-duplicate-finder/docs/PLAY_RELEASE_CHECKLIST.md
T
SoftwareApp f17b40c608 docs: tick the two checklist items that were already satisfied
The permission-set audit and the large-library scan test were both done, just not recorded:
aapt2 badging on the release APK matches the documentation table exactly (no INTERNET), and
the scans ran over 5,363 and 5,385 photos. Spell out what actually remains in the ROADMAP
paperwork row too - the keystore and versionCode are in place, so the open items are the
first upload + Play App Signing enrolment, the hosted privacy policy (still no git remote),
Data safety, the photo/video and foreground-service declarations, the App content questions
and the listing itself.
2026-10-14 00:13:06 +02:00

291 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
⚠️ **Expect a native-debug-symbols warning here, and ignore it.** Play scans the bundle, finds
`base/lib/<abi>/libandroidx.graphics.path.so`, and reports *"This App Bundle contains native
code, and you've not uploaded debug symbols"*. It is a recommendation, not an error: it does
not block the upload, review or rollout.
- That one library (4 ABIs, ~37 KB total) is **not** this project's code — it arrives
transitively through `androidx.compose.ui:ui-graphics` →
`androidx.graphics:graphics-path:1.0.1`. The project has no `ndk` / `externalNativeBuild` /
`jniLibs` config and no native sources at all.
- There is nothing to upload: the published `graphics-path-1.0.1.aar` ships **stripped** libs
(`symtab=0`, no `.debug_*` sections, no symbol entries anywhere in the AAR), and the
commonly-suggested fix does not apply — with `ndk.debugSymbolLevel = "FULL"` the build
succeeds but produces no `app/build/outputs/native-debug-symbols/` output and adds nothing
to the bundle, because AGP only packages symbols for native code it builds itself.
- Excluding the lib (`packaging { jniLibs { excludes += "**/libandroidx.graphics.path.so" } }`)
would silence the warning but risks `UnsatisfiedLinkError` inside Compose's path handling,
for a 37 KB saving. Not worth it.
- What it actually costs: symbolication of frames *inside that Compose helper*. Kotlin/Java
crashes are unaffected — the R8 mapping rides inside the bundle
(`BUNDLE-METADATA/com.android.tools.build.obfuscation/proguard.map`), so Play deobfuscates
them automatically.
- [ ] 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:<user>/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://<user>.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.
- [x] Confirm the **permission set** the Console will show. **[code]** done 2026-10-13: `aapt2 dump
badging` on the release APK reports exactly the table below (plus
`…DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION`) — and **no `INTERNET`**. Re-check from the
*merged* release manifest rather than the source one after any dependency change, 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+).
- [ ] **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.
- [x] Test with a library of several thousand photos to sanity-check scan time. **[code]** done on
the test device: the 5,363-photo library scans in **20.2 s cold / 11.0 s warm**, and the
screenshot run over 5,385 photos finished in **11.1 s** (6 unreadable files skipped).
- [ ] 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 `<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](../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.