diff --git a/docs/PLAY_RELEASE_CHECKLIST.md b/docs/PLAY_RELEASE_CHECKLIST.md index 3dc4f71..536d4e7 100644 --- a/docs/PLAY_RELEASE_CHECKLIST.md +++ b/docs/PLAY_RELEASE_CHECKLIST.md @@ -440,6 +440,28 @@ action except the items marked **[code]**. account, nothing gated. - [ ] **Ads** → *No, my app does not contain ads* (and no ad SDK in `gradle/libs.versions.toml` to contradict the answer). +- [ ] **Advertising ID** → *No, my app does not use advertising ID.* The question adds "This + includes any SDKs that your app imports that use advertising ID", so the answer has to come + from the dependency graph rather than from the source: `play-services-ads-identifier` is what + provides the ID and it merges `AD_ID` into the manifest on its own, so adding any ad or + analytics SDK would flip this answer to *Yes* without one line of app code changing. Audited + 2026-10-14 against the shipped artefacts rather than against intentions: + + | Check | Command | Result | + |---|---|---| + | no advertising/analytics SDK, direct **or transitive** | `./gradlew --offline -q :app:dependencies --configuration releaseRuntimeClasspath \| grep -iE 'gms\|firebase\|ads\|billing\|play-services'` | no output | + | no `AD_ID` and no Privacy Sandbox permission in the release APK | `aapt2 dump badging app/build/outputs/apk/release/app-release.apk \| grep -i permission` | exactly the audit table above — 7 requested, plus the framework's `…DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION` | + | the same for the **`.aab`**, which is the artefact Play scans | `unzip -p app/build/outputs/bundle/release/app-release.aab base/manifest/AndroidManifest.xml \| strings \| grep -x 'AD_ID'` | 0 hits | + | no Play Services classes in the shipped code | `unzip -p app/build/outputs/apk/release/app-release.apk classes.dex \| grep -a -c 'com/google/android/gms'` | 0 | + | nothing could be sent even if an ID were read | the permission audit above | no `INTERNET` | + + This is the machine-checked version of what [PRIVACY.md](../PRIVACY.md) promises in words + ("does not collect … advertising identifiers", "does not contain advertising SDKs or + third-party trackers"), so re-run the first two rows after **any** dependency change — + including one that looks unrelated. This is the declaration a stray library can answer + wrongly on the app's behalf, and the *Data safety* form's *Device or other IDs* answer is + *No* for the same reason. + - [ ] 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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8d66bf2..e142f8b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -30,7 +30,7 @@ Legend: ✅ done · 🚧 in progress · ⏳ not started · 💡 candidate (propo | ~~Large-library scan stall~~ ✅ **fixed — verified on device** | The 5,363-photo library used to freeze at *“74 / 100 · Comparing file signatures…”* (a **blocked syscall**: zero CPU, workers sleeping). `BoundedCall` now abandons a stuck provider call **and** trips a stall breaker after three consecutive timeouts. **Verified on the real test device (Android 12 / MIUI): the scan completes — 20.2 s cold, 11.0 s warm, 15 groups, 6 unreadable — where it used to hang.** Covered by `BoundedCallTest` and `ScanEngineEndToEndTest` | | ~~Unreleased `BoundedCall` fix~~ ✅ | The provider-call deadline shipped in **1.0.1** (`versionCode` 2) | | **Play Console: foreground-service declaration** | Declaring the `dataSync` type (App content › Foreground service permissions) is release-blocking. **[code]** the manifest already declares it, [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 5 carries all four things the form asks for (description, defer/interrupt impact, use case **Local processing: Other**, video link), and the **video now exists**: recorded on the test device on 2026-10-14 with `INSTALL=0 WARMUP=0 tools/record-fgs-demo.sh`, then verified frame by frame rather than assumed — intro at 0 s, the scan starting, **~6 s of the shade with the live "Finding duplicate photos / Looking for visually similar photos… / CANCEL" notification while the scan kept running**, and the results at the end. It is committed, and that is what published it — the link is the file's **page** in Gitea (`/src/branch/main/…`), which plays it anonymously. The `/raw/` URL is deliberately not the link: Gitea sends `Content-Security-Policy: default-src 'none'` with raw responses, so a browser navigating to the video sits on a blank tab even though the bytes are perfect. Pasting the page URL into the declaration is all that is left. Two details of the take are recorded in step 5 because they each cost an attempt: a *warm* scan is 5.4 s, shorter than the shade takes to open, so `WARMUP=0` films the cold one; and MIUI refuses `adb install` outright, so `INSTALL=0` records with the build already on the device. On a KVM-less emulator the same video cannot be made at all: every scan start dies as `am_crash … ForegroundServiceDidNotStartInTimeException` → *Force finishing activity* with no `am_kill`, i.e. emulation is too slow to reach `startForeground()` inside Android's budget (QEMU's `-icount` clock dilation made it work once, not reproducibly) | -| **Remaining Play paperwork** | Console actions only, no code: the **first upload + Play App Signing enrolment**, the **hosted privacy policy URL** (the working copy is now published to Gitea as [`fishfacegit/foto-duplicate-finder`](https://gitea.softwareapp-hb.de/fishfacegit/foto-duplicate-finder), so the policy renders at a public, login-free URL and only the Console paste is left), **Data safety**, the **Photo/video permissions** declaration, the **content rating / audience / app access / ads / export** questions, and the **listing copy + the three screenshot sets** (phone, 7-inch and 10-inch, all captured), plus the **paid-app setup**: a payments profile with tax rates, the €2.99 price and the 14-day €1.49 launch sale on *Products > App pricing* — the price must be set before the first upload, because free → paid is not possible afterwards (M9). Keystore, `versionCode`, listing copy, graphics, permission audit and the signed `.aab` (`sha256 f8e6a29a…`) are all in place; rollout goes internal → closed → production. Step-by-step: [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) | +| **Remaining Play paperwork** | Console actions only, no code: the **first upload + Play App Signing enrolment**, the **hosted privacy policy URL** (the working copy is now published to Gitea as [`fishfacegit/foto-duplicate-finder`](https://gitea.softwareapp-hb.de/fishfacegit/foto-duplicate-finder), so the policy renders at a public, login-free URL and only the Console paste is left), **Data safety**, the **Photo/video permissions** declaration, the **content rating / audience / app access / ads / advertising ID / export** questions, and the **listing copy + the three screenshot sets** (phone, 7-inch and 10-inch, all captured), plus the **paid-app setup**: a payments profile with tax rates, the €2.99 price and the 14-day €1.49 launch sale on *Products > App pricing* — the price must be set before the first upload, because free → paid is not possible afterwards (M9). Keystore, `versionCode`, listing copy, graphics, permission audit and the signed `.aab` (`sha256 f8e6a29a…`) are all in place; rollout goes internal → closed → production. Step-by-step: [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) | | ~~Store screenshots need one re-shoot~~ ✅ **done — verified on device** | The old captures showed the *Photo access needed* banner (an app-op is not a grant) and `03-results.png` showed the developer's own library. After a real grant the set was re-shot over the generated demo library: **01-empty** (no tap needed — results and selection live in `ScannerViewModel`, so force-stop + relaunch returns to the empty state with the grant still in place), **02-scanning** (`5353 / 5385`), **03-results** (**17 groups, 54 of 5385 photos, 83.5 MB reclaimable**; the headline group's 43.1 MB out-ranks the device's own 39.8 MB, so no real photo is in frame) and **04-options**. Captures are taken by watching for UI **text** changes, because MIUI refuses synthetic taps (`INJECT_EVENTS`). The demo library is deleted from the test device afterwards — MediaStore **5,386 → 5,364** rows, and the 22 stale rows it leaves in the hash index are pruned by `PhotoIndexStore.pruneTo` on the next scan. Deliberately not captured: the staged state and the system confirmation dialog, because staging clears the demo group and would put real photos back in frame. Details: [STORE_LISTING](STORE_LISTING.md) | | ~~Tablet store screenshots outstanding~~ ✅ **done — captured on AVDs** | Play has a **7-inch tablet** and a **10-inch tablet** screenshot slot of its own (asking for **≥ 4** screenshots on large screens). Both are now captured on AOSP Android 12 AVDs — `Nexus 7 2013` (1200×1920, `sw600dp`) and `medium_tablet` (1600×2560, `sw800dp`) — because this machine exposes no `/dev/kvm`: they ran in **pure software emulation** (`-accel off -no-window -gpu swiftshader_indirect`), ≈3 min to boot, with `hide_error_dialogs` on so SystemUI's ANR dialog could not sit in frame. Four screens per tablet over the same generated demo library: empty, scanning (20 / 22), results (**2 groups · 16 of 22 photos · 43.7 MB reclaimable**, 39.7 s / 53.1 s) and Options. `tools/make-store-screenshots.sh` is form-factor aware (`FORM_FACTOR=phone\|tablet-7in\|tablet-10in`) and the tablet form factors crop **0 px** of chrome, because a tablet frame is already inside Play's 2:1 cap. Recipe: [STORE_LISTING](STORE_LISTING.md) | | **Device-only verifications** | RAW/DNG files, a *real* iPhone HEIC with a non-upright EXIF orientation, permission revoked mid-scan, and the Android 14 “Selected photos” flow. ✅ The **index-reuse** check is done: after a scan the index holds **5,363 rows, 5,357 with a perceptual hash**, and a second scan runs in **11.0 s vs 20.2 s** cold |