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:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user