diff --git a/docs/PLAY_RELEASE_CHECKLIST.md b/docs/PLAY_RELEASE_CHECKLIST.md index b20579a..929054c 100644 --- a/docs/PLAY_RELEASE_CHECKLIST.md +++ b/docs/PLAY_RELEASE_CHECKLIST.md @@ -267,61 +267,61 @@ action except the items marked **[code]**. fits *Other*. 4. **A link to a video** demonstrating the feature — *"The video should demonstrate the - steps the user needs to take in your app in order to trigger the feature."* Record it - with `./tools/record-fgs-demo.sh`, which drives a device through exactly that journey - (empty state → *Start scan* → Home → the notification visible in the shade while the - scan keeps running → results) and writes - `store-assets/fgs-demo/foreground-service-demo.mp4` (git-ignored, like the - screenshots). Put the file where a reviewer can open it without signing in — an - unlisted YouTube link is the usual choice — and paste that link in. Replacing the - video later means resubmitting the declaration. + steps the user needs to take in your app in order to trigger the feature."* - A hand recording is just as valid and needs no tooling at all: every Android 11+ phone - has a screen recorder in the quick settings, and the whole video is six steps — + **Recorded on the test device** (Android 12 / MIUI, `2a2ae895`) on 2026-10-14 with: - 1. the app on its intro, - 2. tap *Start scan* — this is the trigger the declaration is about, - 3. press Home, so the app is no longer on screen, - 4. pull the notification shade down and let "Scan progress" sit there for a few - seconds, with its live counter and *Cancel* button, while the scan keeps running, - 5. tap the app back into the foreground for the finished results, - 6. stop the recording and upload it. - - Keep it under two minutes and don't cut mid-way; a reviewer has to be able to follow - the trigger and see the service doing its job after the app leaves the screen. - ⚠️ **Record it on a real device — it cannot be done on a KVM-less emulator.** Every - attempt to start a scan on this machine's software-emulated AVD ended the same way, and - logcat's events buffer names the cause each time: - - ``` - am_crash: [..., android.app.ForegroundServiceDidNotStartInTimeException, - Context.startForegroundService() did not then call - Service.startForeground(): .../ScanService] - am_proc_died: [...] + ```bash + INSTALL=0 WARMUP=0 ./tools/record-fgs-demo.sh 2a2ae895 ``` - The platform kills the app and force-finishes the activity, so the launcher appears - where the scan should have started. It is not a crash in the app's own code, it is not - memory pressure (there is no `am_kill` or low-memory record), and it is not something a - warm-up can fix: the last one happened on a process that had been alive and idle for over - a minute. Software emulation is simply too slow to get from `startForegroundService()` to - `startForeground()` inside Android's budget — while on the phone this app scans a - 5,363-photo library in 20 s with the notification up throughout. + The output is `store-assets/fgs-demo/foreground-service-demo.mp4` (git-ignored, like the + screenshots) with a checkpoint PNG per milestone beside it. It was checked frame by frame + afterwards rather than assumed: the intro at 0 s, the scan starting (*"Comparing file + signatures… 91/102"*, 8 s), the shade held open for ~6 s with the live notification + (*"Finding duplicate photos / Looking for visually similar photos… / CANCEL"*, 13–18 s) + while the scan kept running behind it, and the finished results (*17 duplicate groups · + 54 of 5,385 photos*) at the end. Host the file where a reviewer can open it without + signing in — an unlisted YouTube link is the usual choice — and paste that link into the + declaration. Replacing the video later means resubmitting it. - One emulator-side lever is worth knowing, because it is the only thing that ever got this - app's foreground service started on this machine: QEMU's virtual instruction counter - dilates the **guest** clock, so Android's deadline is measured against time that passes - more slowly than the host's. Booting with `-qemu -icount shift=1,sleep=off` made the guest - clock run **2.30× slow** (measured: 30.9 s of host time to 13.4 s of guest uptime), and on - that boot a scan start *worked* — the app's notification appeared five seconds after the - tap, with no `am_crash`, the only success recorded here. It then stopped working: later - attempts produced neither a notification nor a crash, which means the tap never reached - the app, or `ScanService.start()` silently swallowed a - `ForegroundServiceStartNotAllowedException` (it catches `IllegalStateException` and does - nothing with it, so that path leaves no trace in logcat at all — a diagnosability gap - worth a log line if this is ever chased further). So treat `-icount` as a lead to retry - with patience rather than a recipe to rely on, and use `SETTLE_SECONDS` / `WARM_SECONDS` - to scale this script's waits for a dilated clock. + Both flags exist because this take found two traps worth knowing before a re-shoot: + + - **`WARMUP=0` keeps the *cold* scan on camera.** A warm scan of this library takes + **5.4 s**, which is less than the notification shade takes to open — so the first take + filmed an empty shade, convincingly. The cold first scan (~20 s) is what gives the + shade time to appear while the notification is still live, and reinstalling the app + restores that window. + - **`INSTALL=0` skips the install**, because MIUI does not let `adb` install anything: + the command **hangs** waiting for two prompts on the device — Google Play Protect + (*"App zum Sicherheitsscan senden?"*) and then MIUI's own *Installieren* — and + auto-cancels as `INSTALL_FAILED_USER_RESTRICTED` if they go unanswered. The simplest + route is to install once by hand and then record with `INSTALL=0`. + + A hand recording is just as valid and needs no tooling: every Android 11+ phone has a + screen recorder in the quick settings, and the whole video is six steps — the intro, tap + *Start scan* (the trigger the declaration asks to see), press Home, pull the shade down + and leave the notification sitting there while the scan runs, tap the app back for the + results, stop. Under two minutes and uncut: a reviewer has to be able to follow the + trigger and see the service still working once the app has left the screen. + ⚠️ **MIUI blocks synthetic input until the matching developer option is on.** Every + `adb shell input tap` on this phone fails with *"Injecting to another application + requires INJECT_EVENTS permission"* — which is why the project's earlier captures were all + taken by hand. With **USB debugging** enabled (and the security-settings toggle that comes + with it) the block lifts, `tools/record-fgs-demo.sh` drives the phone end to end and + nothing is needed from a human. Toggling it resets the USB connection, though: the device + comes back on a **new** `/dev/bus/usb` node, which has to be made readable before `adb` + sees it again. + + ⚠️ **It cannot be recorded on a KVM-less emulator**, and the failure is the platform's, + not the app's: every scan start there ends in + `am_crash … ForegroundServiceDidNotStartInTimeException` → *Force finishing activity*, + with no `am_kill` or low-memory record, because software emulation is too slow to get + from `startForegroundService()` to `startForeground()` inside Android's budget. QEMU's + virtual instruction counter dilates the guest clock and made it work exactly once + (`-qemu -icount shift=1,sleep=off`, guest clock measured 2.30× slow) but not + reproducibly, so that is a lead rather than a recipe; `SETTLE_SECONDS` / `WARM_SECONDS` + scale this script's waits if it is ever retried. The declaration is **per type**: if a new foreground-service type is ever added to the manifest, it has to be declared here too. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7cf83f4..dd57bbb 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -29,7 +29,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, and [PLAY_RELEASE_CHECKLIST](PLAY_RELEASE_CHECKLIST.md) step 5 now carries all four things the form asks for: the description, the user impact if the scan is deferred or interrupted, the use case (**Local processing: Other** — the only `dataSync` option that fits a local, user-initiated scan) and the **video link** Play requires. `tools/record-fgs-demo.sh` records that video — empty state → *Start scan* → Home → the notification in the shade while the scan continues → results — into the git-ignored `store-assets/fgs-demo/`; hosting it and pasting the link is still a manual step. The take itself belongs on a real device: on this machine's KVM-less AVD every scan start ended in `am_crash … ForegroundServiceDidNotStartInTimeException` → *Force finishing activity*, with no `am_kill` or low-memory record, i.e. software emulation is too slow to reach `startForeground()` inside Android's budget — and that, not a mysterious "mid-scan death", is what an earlier note here was misreading (the script's old check matched the service *record*, which exists before `startForeground()` is called, so it reported success on runs that were already dying). The evidence that this is the emulator and not the app is the phone's own history: 5,363 photos scanned in 20 s with the notification up throughout | +| **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. Only hosting it (an unlisted YouTube link) and pasting that link remain. 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) | | ~~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) | diff --git a/tools/record-fgs-demo.sh b/tools/record-fgs-demo.sh index 2bc193d..c09f45f 100755 --- a/tools/record-fgs-demo.sh +++ b/tools/record-fgs-demo.sh @@ -145,7 +145,14 @@ checkpoint() { #