Root causes found by reproducing each on the emulator (2.8k-file fixture
with mixed jpg/mp4):
1. walk_dir_nostats (incremental rescan of >2000-file dirs) had NO
image-extension filter: it re-enumerated every file, injecting all
~1,727 videos into the DB. This is what made the count swing
8,439 -> 10,166 and the grid full of blank cells (404 rows).
Now filters through is_image_path() like every other walk.
2. walk_dir aborted the ENTIRE walk on any per-file stat failure
(file deleted mid-walk), so an error during the walk meant the
walk was never applied and deletions were never detected.
Per-file stat errors are now skipped; only readdir/root errors
fail the walk (an errored walk is still not applied).
3. dir_sig stat'ed every entry of every root dir every watch tick:
10k+ stats/second on the phone = 10s+ of FUSE I/O per second =
the long delays. On Android the MediaStore URI payloads already
carry the exact change signal, so the mtime-diff machinery now
runs desktop-only. The Android watch tick is a single read_dir
of our own app dir (no-op when empty): 1s backstop, 30s-throttled
fallback full rescan for events with no payload, 5-min full pass.
4. TOCTOU: a delete consumed while a full rescan was mid-walk lost
to the walk's stale snapshot (walk re-added the row). RESCANNING
gate parks payloads during the walk; they apply after it, so
mid-walk deletions always win. Reproduced deterministically
(churn during the startup walk), fixed, re-verified.
Emulator stress (all passing): full-walk convergence, add +1s,
delete +1s, video add/delete = zero rows, 60-op churn converges to
exact disk count, 3x relaunch idempotency, startup-walk race
converges (1s normally, 5-min backstop when the OS scanner drops
burst events), no-blank-cells visual check, 0.00% dropped frames.
Phone verified: 10,166 -> 8,439 on first launch (all video phantoms
purged), add appears ~2s after the OS indexes it, delete same,
DB returns to 8,439 exactly. ContentObserver registration + onChange
logging added (it fired reliably once the stale instance was
replaced).