docs: answer the Advertising ID declaration from the dependency graph, not from the source

Play asks whether the app uses advertising ID "including any SDKs that your app imports that use
advertising ID", and that clause is the whole difficulty: the ID comes from
play-services-ads-identifier, which merges AD_ID into the manifest by itself - so one unrelated
dependency could turn this answer into a false statement without a line of app code changing. The
answer is No, and the checklist now records how that was established on 2026-10-14 against the
artefacts rather than against intentions:

- the release runtime classpath contains no gms/firebase/ads/billing/play-services entry at all, so
  there is no direct *or transitive* SDK that would ask for the ID
- aapt2 dump badging on the release APK lists the seven permissions the audit table already
  documents plus the framework's DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION - no AD_ID, and no
  Privacy Sandbox permission either
- the same check on base/manifest/AndroidManifest.xml inside the .aab finds nothing, which matters
  because the bundle is the artefact Play's own scanner reads
- the shipped classes.dex contains no com.google.android.gms classes, and the app has no INTERNET
  permission, so an ID could not be transmitted even if one were read

The last two rows are the ones to re-run after any dependency change: they are cheap, and this is
the declaration a stray library answers wrongly on the app's behalf. The same reasoning answers Data
safety's device-identifiers question, which the item now says out loud.

ROADMAP: the remaining-paperwork row names the advertising ID question rather than leaving it
implied by "ads".
This commit is contained in:
2026-10-14 13:48:04 +02:00
parent 6315af1f92
commit 64431d5757
2 changed files with 23 additions and 1 deletions
+22
View File
@@ -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.
+1 -1
View File
@@ -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 |