fix(bedtime): switch a recreated rule, keep manual overrides, finish edits

A rule recreated by a pass (deleted in Settings, access re-granted) now
gets its state set even when no boundary was crossed, and the applied
state is recorded only once the system accepted it. Edits that can't move
a boundary (reminder, effects) no longer re-assert the mode over a manual
override, and the screen's writes run on the application scope so leaving
right after a change can't cut it short. A late reminder counts the
minutes actually left, and one slot fire reads the zone once.

An equal bedtime and wake time is now no window instead of a day-long one,
which on every night meant Do Not Disturb never ended, and an empty days
mask in prefs reads as every night, as a backup's does. The rule-id extras
are translated to the bedtime action in MainActivity, keeping the routing
table keyed on the action alone.

Refs #1
This commit is contained in:
makiolaj committed 2026-10-22 11:38:56 +02:00
1 parent 119197106b
commit 2dce78281a
14 files changed
+148 -67

No files matched your search

+12 -6
View File
@@ -2728,8 +2728,9 @@ A rule needs an owner `ConditionProviderService` or a configuration activity.
Clockula uses `MainActivity` as the configuration activity: it is already
exported, so no new component is exposed. Settings opens it with the explicit
component, the rule id in `NotificationManager.EXTRA_AUTOMATIC_RULE_ID` /
`ConditionProviderService.EXTRA_RULE_ID`, and **no action**, so the presence of
either extra is what makes the shell open the bedtime screen
`ConditionProviderService.EXTRA_RULE_ID`, and **no action**, so `MainActivity`
turns an action-less intent carrying either extra into the reminder's own
`ACTION_SHOW_BEDTIME`, and the shell opens the bedtime screen
(`ShellRequest.OpenBedtime`). There is no condition provider service:
Clockula reports the rule's state itself with `setAutomaticZenRuleState`.
@@ -2769,14 +2770,15 @@ engines (`ArchitectureRulesTest` holds it to no `android.*` import):
Every entry point is an idempotent pass over "where does the schedule stand
now", computed by `BedtimeSchedule`: a night's window runs from the bedtime on
one of the chosen days to the first wake time after it (an earlier wake time is
the next morning, an equal one a full day), resolved from local times and the
the next morning; an equal one is no window at all, since a day-long window on
every night would be a Do Not Disturb that never ends), resolved from local times and the
zone like an alarm, so a DST night is as long as the wall clock says. The slot
is armed for the earliest of the current window's end, the next window's
reminder and the next window's start.
| Entry point | Called from | What it does |
|---|---|---|
| `update(transform)` | the bedtime screen | stores the settings, rewrites the rule if a field it carries changed, sets the mode to what the schedule says now (on at 23:30 for a 23:00 bedtime starts it at once), arms the slot. Off removes the rule, which ends the mode, cancels the slot and clears the reminder |
| `update(transform)` | the bedtime screen, on the application scope so leaving the screen cannot cut a write short | stores the settings, rewrites the rule if a field it carries changed, and when the schedule itself changed sets the mode to what it says now (on at 23:30 for a 23:00 bedtime starts it at once; a reminder or effect edit leaves a hand-switched mode alone), arms the slot. Off removes the rule, which ends the mode, cancels the slot and clears the reminder |
| `onSlotFired()` | `BedtimeReceiver` | follows any boundary crossed, posts a due reminder, arms the next event. A reminder fire alone crosses no boundary, so it leaves a mode switched on by hand alone |
| `resync()` | `ClockulaApp.onCreate` (a force stop clears the slot), `SystemEventReceiver` (boot, app update, clock or zone change, exact-alarm permission) | creates the rule if it is missing, follows any boundary crossed, arms the slot |
| `resync(refreshRule = true)` | Do Not Disturb access changing (the screen's `ON_RESUME` check and the broadcast), a backup restore | the same, but also rewrites the rule and re-asserts the mode's state |
@@ -2785,12 +2787,16 @@ reminder and the next window's start.
coexist with a schedule. The last state Clockula set is persisted
(`bedtime_applied_active`, device state, not in the JSON backup), and a pass
that is not an edit switches the mode only when the schedule now says
something different. A launch in the afternoon leaves a mode the user turned
something different, or when the rule was just recreated (a new rule starts
off). The state is recorded only once the system accepted it, so a refused
call is retried by the next pass. A launch in the afternoon leaves a mode the user turned
on alone; a launch after a force stop that slept through the wake-up time
turns it off. Each pass reads the clock **once**, so the mode's state and the
next slot always agree about which side of a boundary "now" is on. A posted
reminder is withdrawn by any pass once no coming bedtime is due within its
lead, so moving the schedule away from it does not leave a stale one.
lead, so moving the schedule away from it does not leave a stale one, and its
title counts the minutes actually left, so a late fire does not promise the
full lead.
Every bedtime call on a shared path runs **after** the alarm, timer and
stopwatch passes and inside `runCatching` (in `SystemEventReceiver`, in its