docs: finish the Console pre-flight and retire the screenshot blocker

Everything the Play forms ask for that can be settled from the working copy, settled:

- PRIVACY.md now carries the real contact address (softwareapp.hb@gmail.com), so the
  policy publishes as-is. Hosting is the one step left that cannot be done from here,
  because the repo has no git remote: step 5 of the checklist spells out the push +
  GitHub Pages route and the URL it yields.
- Checklist: the screenshot item is done (four captures, 1080x2160, over the demo
  library, no permission banner), the keystore and versionCode are recorded as already
  in place - including the upload key's SHA-256 for the Play App Signing enrolment - and
  the App access / Ads / remaining App content declarations are written down. The
  first-upload release-notes question is answered too: versionCode is already 2, so
  1.0.1 takes the *initial release* block, not the 1.0.1 one.
- The build facts were verified against the artefacts rather than the source: the
  release APK reports versionCode=2, versionName=1.0.1, targetSdk=36 and exactly the
  documented permission set, with no INTERNET in what actually ships.
- ROADMAP: the re-shoot row is struck out with the numbers that closed it, M10 no longer
  claims the captures predate the demo library, and the demo library's removal is
  recorded (MediaStore 5,386 -> 5,364; pruneTo drops the stale index rows next scan).

The staged state and the system confirmation dialog are documented as deliberately not
captured: staging clears the demo group, which would put real photos back in frame.
This commit is contained in:
2026-10-13 23:59:15 +02:00
parent b43718aca8
commit 55101886cb
4 changed files with 80 additions and 39 deletions
+59 -23
View File
@@ -9,24 +9,28 @@ the rest are things you must do in the Play Console / with your own accounts.
(`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.
- [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.
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
- [ ] Set `versionName` (user-visible, e.g. `1.0.0`) **[code]** in
`app/build.gradle.kts`.
- [ ] Increment `versionCode` for **every** upload.
- [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
@@ -35,6 +39,12 @@ the rest are things you must do in the Play Console / with your own accounts.
./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*).
@@ -52,12 +62,15 @@ action except the items marked **[code]**.
- [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.
- [ ] 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.
- [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).
@@ -66,10 +79,22 @@ action except the items marked **[code]**.
## 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.
⚠️ **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.
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
@@ -141,7 +166,13 @@ action except the items marked **[code]**.
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
@@ -164,6 +195,11 @@ action except the items marked **[code]**.
## 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.