Two scheduling mechanisms, and only one of them is allowed to be precise
Context
Android has two ways to make something happen later, and they are not interchangeable:
- WorkManager survives reboots and app updates, batches with other work, and is subject to Doze. It will run your job eventually — which for a daily nudge is exactly right, and for anything with a real deadline is not.
- AlarmManager exact alarms fire at the minute. They are lost on reboot, and since Android 14 at
targetSdk33+ the permission is denied by default.
Two more platform facts shape everything below:
SCHEDULE_EXACT_ALARMandUSE_EXACT_ALARMare not alternatives. The second is auto-granted and cannot be revoked, which sounds like the better deal until you read Play’s policy: it is permitted only for apps whose core function is an alarm clock or a calendar. Declaring it in anything else puts the whole listing at risk to save one settings screen.- Vendor ROMs add a second layer of policy. On Xiaomi’s HyperOS, without autostart granted the
ROM does not start the process for a broadcast at all — not
BOOT_COMPLETED, notMY_PACKAGE_REPLACED, not an explicitam broadcastto a manifest receiver. Observed: a 03:00 exact alarm landing at 06:50 without autostart, and on time in deep Doze with it.
Decision
One daily WorkManager sweep drives everything that is not time-critical. It asks “what is due today?” — derived from the data every time — and re-enqueues itself for tomorrow.
Exact alarms are reserved for the things whose lateness has consequences, declared with
SCHEDULE_EXACT_ALARM, and the app assumes nothing about the grant: it reads
canScheduleExactAlarms(), says which mechanism is actually in play, and falls back to
setAndAllowWhileIdle — late but real.
Nothing persists a schedule. Due dates are derived; the OS schedule was only ever a cache of that derivation. So there is nothing per-item to cancel, orphan, or double-enqueue.
Notification permission is asked from the app’s own screen, never from a bare system dialog on launch. Android permits two denials before the dialog stops appearing for good, and there is one place in the app that spends them.
Alternatives
One scheduled work item per reminder. Sounds simpler and is not: every item is something to cancel when the reminder is edited, to re-enqueue on boot, and to reconcile after a restore. Getting any of those wrong produces a notification that fires twice, or one that stops firing with nothing to show for it. A sweep has none of those states.
A periodic work request. WorkManager’s minimum period is 15 minutes with a flex window it chooses, so “every day at 09:00” is not expressible. A one-time request that enqueues the next one is exact to the minute and survives being run late.
Exact alarms for everything. Spends a restricted permission, a battery-optimisation exemption and the user’s patience on nudges that do not need any of it. If everything is urgent, nothing is.
Consequences
BootReceiverandPackageReplacedReceiverexist and do almost nothing, deliberately. The update receiver is not optional: a schema-bumping update leaves the database at the old version until something opens it, and every background entry point correctly declines to work over one.- A notification channel’s importance is set at creation and can only be lowered afterwards, by the user. Creating a channel quiet is a decision with no second attempt.
- Testing any of this on a vendor ROM requires reading the autostart state first. A run against an
unknown one proves nothing either way —
scripts/device-gate.pyreads it.
Amendment — 2026-10-18: Kozica schedules nothing
Kozica has no reminders and nothing in PLAN.md schedules anything, so both
mechanisms are gone (phase-0.md § 1): the sweep, the receivers, exact alarms,
WorkManager and the notification ask. The artifact no longer carries POST_NOTIFICATIONS,
RECEIVE_BOOT_COMPLETED, SCHEDULE_EXACT_ALARM, WAKE_LOCK, ACCESS_NETWORK_STATE or
FOREGROUND_SERVICE, and scripts/aab-permissions.py now forbids the network-state pair outright
(ADR-0008).
What outlives the deletion is the background schema check (data/BackgroundSchemaGuard.kt), which
phase 6’s recording service will ask before it touches the database, and phase 6 re-reads this ADR
for the notification ask and the vendor-ROM facts above. If scheduled work ever returns, the
decision above is the starting point rather than a blank page.
Amendment — 2026-10-20: WorkManager returns, as Play Asset Delivery’s
Phase 2b puts the asset-delivery library into the release build (phase-2b.md
§ 2), and it depends on WorkManager for its ExtractionWorker. So the artifact carries WorkManager
again, with WAKE_LOCK, RECEIVE_BOOT_COMPLETED, FOREGROUND_SERVICE and
FOREGROUND_SERVICE_DATA_SYNC; ACCESS_NETWORK_STATE stays stripped. Nothing of Kozica’s own is
scheduled: the work is the library’s, started by a Region download, and the amendment above still
describes the app.
Amendment — 2026-10-26: the recording service and the notification ask
Phase 6’s checkpoint B brings Kozica’s first foreground service of its own, location, for recording
a Hike (phase-6.md § 2), and with it POST_NOTIFICATIONS, asked once at the first
Record and never at launch. It is started from the visible app and schedules nothing: no alarm, no
work, no boot receiver. Its channel is created at IMPORTANCE_LOW for the reason above, and the
service asks schemaBlocksBackgroundWork() on every start, as the 2026-10-18 amendment promised. The
decision that Kozica schedules nothing stands.