diff --git a/docs/PLAY_RELEASE_CHECKLIST.md b/docs/PLAY_RELEASE_CHECKLIST.md index 934660f..acffc89 100644 --- a/docs/PLAY_RELEASE_CHECKLIST.md +++ b/docs/PLAY_RELEASE_CHECKLIST.md @@ -289,21 +289,31 @@ action except the items marked **[code]**. 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.** The capture proved unreliable on this machine's - software-emulated AVD (no KVM), and both failures are platform-level rather than - app-level: + ⚠️ **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: - - A **cold** app can miss Android's 5-second `startForeground` deadline and get killed - (`ForegroundServiceDidNotStartInTimeException` → *Force finishing activity*), so the - launcher appears where the scan should have started. The script's warm-up phase gets a - process past that — a process that has already started the service once succeeds — but - - the app also died **mid-scan** on the emulator during testing, and the launcher was on - screen afterwards with no exception from the app in logcat at all. A warmer process - does not fix that one. + ``` + am_crash: [..., android.app.ForegroundServiceDidNotStartInTimeException, + Context.startForegroundService() did not then call + Service.startForeground(): .../ScanService] + am_proc_died: [...] + ``` - Neither failure has been seen on the real test device, where the same 22-photo library - scans without incident. So treat `tools/record-fgs-demo.sh` as the checklist for the - journey it records, and run it against the phone. + 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. + + An earlier version of this note claimed a second, unexplained mid-scan death. That was + this same failure misread: the script's old "is a scan running?" check matched the + service *record*, which exists from the moment `startForegroundService()` is called, so + it reported success on runs that were already dying. `tools/record-fgs-demo.sh` now waits + for the app's notification instead, which is the only honest proof the service started, + and reports the `am_crash` line when it never does. 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 394a9dc..7cf83f4 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 the app missed Android's 5-second `startForeground` deadline when cold (`ForegroundServiceDidNotStartInTimeException` → *Force finishing activity*) and died mid-scan once with no exception of its own in logcat, neither of which the script's warm-up phase fully removes, and neither of which reproduces on the phone | +| **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 | | **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 e074d71..c7bb72a 100755 --- a/tools/record-fgs-demo.sh +++ b/tools/record-fgs-demo.sh @@ -112,13 +112,22 @@ wait_for_exact() { return 1 } -scan_running() { "${adb[@]}" shell dumpsys activity services 2>/dev/null | grep -q -i ScanService; } - notifications_from_app() { "${adb[@]}" shell dumpsys notification --noredact 2>/dev/null \ | grep -c 'NotificationRecord(.*fotodup' || true } +# The signal that actually matters. A service *record* appears as soon as startForegroundService() is +# called, so it does not prove that startForeground() succeeded - and that five-second gap is exactly +# what kills this app on a slow emulator. The first version of this script used the record as its +# "scan started" evidence and therefore reported success on runs where the app was being killed +# (logcat's events buffer shows am_crash ForegroundServiceDidNotStartInTimeException moments later). +# The app's notification only exists once startForeground() has been called, so wait for that. +fgs_started() { [ "$(notifications_from_app)" != "0" ]; } + +# Secondary signal, for logging only. +scan_running() { "${adb[@]}" shell dumpsys activity services 2>/dev/null | grep -q -i ScanService; } + checkpoint() { #