feat: record the FGS video on the real device, and fix what the take exposed

The declaration's video exists now: store-assets/fgs-demo/foreground-service-demo.mp4, recorded on
the test device (Android 12 / MIUI, 2a2ae895) with INSTALL=0 WARMUP=0, and then checked frame by
frame 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 at the end. Hosting it and pasting the link into the declaration is
what remains.

The script grew two flags, both because an earlier attempt got it wrong:

- WARMUP=0 skips the warm-up and keeps the *cold* scan on camera. A warm scan of this library
  takes 5.4 s - less than the notification shade takes to open - so the first phone take filmed an
  empty shade, convincingly. The warm-up only ever existed for the emulator's FGS deadline
  problem, which a real device does not have.
- INSTALL=0 records with the build already on the device, because MIUI does not allow adb
  installs: the command hangs on two on-device prompts (Google Play Protect, then MIUI's own
  Installieren) and auto-cancels as INSTALL_FAILED_USER_RESTRICTED if they go unanswered.

Also found by running this against a phone instead of an AVD:

- the trigger is chosen by mode - the intro's "Start scan" when the warm-up is skipped, the
  results screen's top-bar "Scan" when it is not - and the lookup result is checked rather than
  assumed,
- pacing is a variable now, tightened in the WARMUP=0 path, because the phone's cold scan is only
  ~20 s and the shade has to be open while it runs,
- the library check counts the demo folder rather than the whole device library: on a phone the
  rest of the library is the owner's own photos and is supposed to be there,
- the warm-up became a function and does not run at all under WARMUP=0, and a stray reference to
  its `attempt` variable (fatal under `set -u`) is gone.

Step 5 of the checklist now carries the recorded recipe, both flags with the reasons, and two
device facts that were invisible until a phone was attached: MIUI fails every synthetic tap with
INJECT_EVENTS until USB debugging is enabled, and toggling that resets the USB connection onto a
new /dev/bus/usb node. The emulator verdict is compressed to what it is - the platform kills the
app on every scan start there (am_crash ForegroundServiceDidNotStartInTimeException, no am_kill),
with QEMU's -icount clock dilation noted as the one thing that ever worked, and only once.
This commit is contained in:
2026-10-14 13:16:24 +02:00
parent ddc7fb44b0
commit 52cf2fc85a
3 changed files with 170 additions and 123 deletions
+50 -50
View File
@@ -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.