Changing the reminder time does not move it, rescheduling can fire it early, and a zone change never re-aims it #72

Closed
opened 2026-08-20 21:24:14 -05:00 by null · 0 comments
Owner

Verified against WorkManager 2.11.2's own bytecode, not only the app's code.

What is true now. ReminderScheduler.schedule (core/notifications/.../ReminderScheduler.kt:39-55) enqueues a 1-day periodic request with setInitialDelay under ExistingPeriodicWorkPolicy.UPDATE. WorkerUpdater.updateWorkImpl copies lastEnqueueTime and periodCount from the old spec, and WorkSpec.calculateNextRunTime computes periodCount == 0 ? lastEnqueueTime + initialDelay : lastEnqueueTime + interval. Two consequences:

  1. Changing 'When' from Morning to Evening AFTER the first reminder has fired does not move the reminder at all.
  2. Re-running schedule() BEFORE the first run — which every process start does, via the coordinator — sets the next run to original enqueue time + new delay, which is in the past by the next morning, so it fires early and every later period anchors off that.

Also: resetPeriodic() anchors the next period at completion time, so reminders drift later daily; the KDoc at :24-26 says the work is KEEP-replaced while :52 uses UPDATE; and delayUntil(reminderTime, ZoneId.systemDefault()) is evaluated once at schedule time with no receiver for ACTION_TIMEZONE_CHANGED or ACTION_TIME_CHANGED anywhere in the app (grep confirms none).

What it costs. A user who moves her reminder to the evening keeps getting it in the morning, and a user who flies across time zones gets it at the wrong hour indefinitely. Both read as 'this app's reminders are broken'.

What to do. Read the existing WorkInfo.nextScheduleTimeMillis; enqueue with KEEP when nothing exists; leave an overdue run alone (moving it skips today); leave it alone when it is already within a few minutes of the target; otherwise UPDATE with setNextScheduleTimeOverride(target). Add an unexported receiver for the two clock broadcasts that asks the coordinator to reschedule. Rewrite the KDoc to match the code and the screen copy to match reality.

Traps. Do NOT add a flex interval to make the 'few minutes either side' copy true — calculateNextRunTime puts the first run at start + interval − flex, so a 15-minute flex would skip today's reminder entirely. Do NOT use CANCEL_AND_REENQUEUE from the coordinator: it runs at every process start including the worker's own and would cancel the running worker. BOOT_COMPLETED needs no app receiver — WorkManager's own RescheduleReceiver handles it (its AAR manifest declares only that action, which is why the clock broadcasts do need one).

Verify: changing the reminder time moves the next run, scheduling again with the same time leaves the next run alone and an overdue run is not pushed to tomorrow in a new WorkManagerReminderSchedulerTest using WorkManagerTestInitHelper with a fixed clock; a clock change re-runs the schedule even when nothing else changed in ReminderCoordinatorTest.

Verified against WorkManager 2.11.2's own bytecode, not only the app's code. **What is true now.** `ReminderScheduler.schedule` (`core/notifications/.../ReminderScheduler.kt:39-55`) enqueues a 1-day periodic request with `setInitialDelay` under `ExistingPeriodicWorkPolicy.UPDATE`. `WorkerUpdater.updateWorkImpl` copies `lastEnqueueTime` and `periodCount` from the old spec, and `WorkSpec.calculateNextRunTime` computes `periodCount == 0 ? lastEnqueueTime + initialDelay : lastEnqueueTime + interval`. Two consequences: 1. Changing 'When' from Morning to Evening AFTER the first reminder has fired does not move the reminder at all. 2. Re-running `schedule()` BEFORE the first run — which every process start does, via the coordinator — sets the next run to *original enqueue time* + *new delay*, which is in the past by the next morning, so it fires early and every later period anchors off that. Also: `resetPeriodic()` anchors the next period at completion time, so reminders drift later daily; the KDoc at `:24-26` says the work is KEEP-replaced while `:52` uses UPDATE; and `delayUntil(reminderTime, ZoneId.systemDefault())` is evaluated once at schedule time with no receiver for `ACTION_TIMEZONE_CHANGED` or `ACTION_TIME_CHANGED` anywhere in the app (grep confirms none). **What it costs.** A user who moves her reminder to the evening keeps getting it in the morning, and a user who flies across time zones gets it at the wrong hour indefinitely. Both read as 'this app's reminders are broken'. **What to do.** Read the existing `WorkInfo.nextScheduleTimeMillis`; enqueue with KEEP when nothing exists; leave an overdue run alone (moving it skips today); leave it alone when it is already within a few minutes of the target; otherwise UPDATE with `setNextScheduleTimeOverride(target)`. Add an unexported receiver for the two clock broadcasts that asks the coordinator to reschedule. Rewrite the KDoc to match the code and the screen copy to match reality. **Traps.** Do NOT add a flex interval to make the 'few minutes either side' copy true — `calculateNextRunTime` puts the first run at `start + interval − flex`, so a 15-minute flex would skip today's reminder entirely. Do NOT use `CANCEL_AND_REENQUEUE` from the coordinator: it runs at every process start including the worker's own and would cancel the running worker. BOOT_COMPLETED needs no app receiver — WorkManager's own `RescheduleReceiver` handles it (its AAR manifest declares only that action, which is why the clock broadcasts do need one). Verify: `changing the reminder time moves the next run`, `scheduling again with the same time leaves the next run alone` and `an overdue run is not pushed to tomorrow` in a new `WorkManagerReminderSchedulerTest` using `WorkManagerTestInitHelper` with a fixed clock; `a clock change re-runs the schedule even when nothing else changed` in `ReminderCoordinatorTest`.
null added this to the Batch 12 — Reminders that tell the truth milestone 2026-08-20 21:24:14 -05:00
null added the
P1
label 2026-08-20 21:24:14 -05:00
null closed this issue 2026-08-20 22:42:12 -05:00
Sign in to join this conversation.
No Label
P0
P1
P2
release-blocker
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/Privacy-Period-Tracker#72
No description provided.