Everything the backend owns lived in memory, which made every claim about
working offline true for exactly as long as the process. Android kills processes
constantly, and starts them again for the accessibility service with no Activity
to have fetched anything — so the common case was the unhandled one:
* the focus guard woke with empty matchers, enforced nothing, and reported
"disabled", which is indistinguishable from the user switching it off;
* the dial read 00:00 until a reply landed, and with no signal, forever;
* Tasks and Today showed an error banner where yesterday's answer would do.
`LocalStore` is one small JSON file per subject under `filesDir` — not Room (no
queries, no relations, no migrations worth the dependency) and not
SharedPreferences (those are for what this *machine* owns; these are documents).
Every read degrades to "no cache" rather than throwing, and a file that fails to
parse is deleted on the way past, because a background service must not be
crash-looped by its own cache.
The timer state is stamped with the wall-clock moment it was received, which is
what lets a cold start subtract the time that passed while nothing was running —
wall clock deliberately, since `nanoTime` does not survive a reboot and the
question is how much of the *day* went by. A paused interval is left where it
was. None of it is marked Online: a cache is not a reply.
The guard keeps the rules and the last policy. The verdict is not adopted from
disk — `active` is a claim about this moment — but the expanded host list and
the masks do not go stale, so the guard can enforce before the first poll.
Read-only screens take a cache slot, so `Loader` paints the last good answer
before it asks. Changing the backend URL clears the lot: one server's report
under another's name is worse than an empty screen.
10 tests, including the two cold-start cases that failed before: a dial that
knows what is running, and a guard that still blocks with the server gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>