A reminder can be switched on and still never arrive: the permission
denied, notifications off for the whole app, or the channel blocked.
notify() returns false in all three cases, and both call sites discarded
it while still counting the check-in. So with notifications denied the
counter climbed to §30's limit and the app stopped asking -- permanently,
having never once asked.
Counted only when posted now. canPost() also asks whether notifications
are enabled at all and whether this mode's channel is blocked; below API
33 the permission is granted by definition, so an app whose notifications
the user had switched off posted into nothing and called it asking.
The settings screen says so, in one row above the toggles, with a button
to the system setting that would fix it -- and re-reads on resume, so
somebody who leaves to switch notifications back on is believed when she
returns. Revocation after the fact was previously undetectable:
hasPermission() was called from nowhere in main.
A denial does not switch the toggle back off. That is the tempting fix and
it is wrong: she said she wants the reminder, and rewriting her answer
means a later grant changes nothing and she has to find the toggle again
to discover that. The preference records what she asked for; the row
records what the system is doing about it.
ReminderWorker now takes a ReminderNotifier rather than building one from
the application context, which is what made it testable. It had no test of
any kind -- the class that reads the history, applies the rules, posts, and
counts -- and every defect in this batch lived in that gap. Six now, over
a real repository and a real preference store with only the notifier
faked, since what is asserted is precisely what the worker does with the
notifier's answer.
Two things the prove-guard discipline caught that a green suite did not.
The posted-and-counts guard reddened nothing at first, because the worker
had no tests to redden -- the fix was unproven until the harness existed.
And an assertion of mine read vm.state.value, which is
stateIn(WhileSubscribed): with nobody collecting, it sits on the defaults,
where every reminder is already true. That test could not have failed. It
reads the store now.
closes#71
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>