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:
1 parent
119197106b
commit
2dce78281a
14 files changed
+148
-67
No files matched your search
@@ -166,8 +166,12 @@ class MainActivity : ComponentActivity() {
|
||||
.getOrNull()
|
||||
?.toList()
|
||||
.orEmpty()
|
||||
val forZenRule = runCatching { BedtimeIntents.ZEN_RULE_EXTRAS.any(intent::hasExtra) }.getOrDefault(false)
|
||||
return ShellNavigation.requestFor(intent.action, alarmId, alarmIds, forZenRule)
|
||||
// Settings opens the bedtime rule's configuration activity (this one)
|
||||
// with the rule id and no action; that is the bedtime screen (issue #1).
|
||||
val forZenRule = intent.action == null &&
|
||||
runCatching { BedtimeIntents.ZEN_RULE_EXTRAS.any(intent::hasExtra) }.getOrDefault(false)
|
||||
val action = if (forZenRule) BedtimeIntents.ACTION_SHOW_BEDTIME else intent.action
|
||||
return ShellNavigation.requestFor(action, alarmId, alarmIds)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
||||
@@ -8,6 +8,9 @@ import de.jeanlucmakiola.clockula.domain.time.ZoneProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.time.ZoneId
|
||||
import kotlin.math.ceil
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Instant
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -42,15 +45,21 @@ class BedtimeEngine @Inject constructor(
|
||||
|
||||
/**
|
||||
* A change made on the bedtime screen: stored, then applied at once. The
|
||||
* rule is rewritten when a field it carries changed, and the mode is
|
||||
* switched to what the new schedule says now, so turning bedtime on at
|
||||
* 23:30 for a 23:00 bedtime starts it straight away.
|
||||
* rule is rewritten when a field it carries changed. When the schedule
|
||||
* itself changed, the mode is switched to what it says now, so turning
|
||||
* bedtime on at 23:30 for a 23:00 bedtime starts it straight away; an
|
||||
* edit that cannot move a boundary (the reminder, an effect) leaves a
|
||||
* mode switched by hand alone.
|
||||
*/
|
||||
suspend fun update(transform: (BedtimeSettings) -> BedtimeSettings) = lock.withLock {
|
||||
val previous = settings.currentBedtime()
|
||||
val updated = transform(previous)
|
||||
settings.setBedtime(updated)
|
||||
apply(updated, refreshRule = previous.ruleFields() != updated.ruleFields(), force = true)
|
||||
apply(
|
||||
updated,
|
||||
refreshRule = previous.ruleFields() != updated.ruleFields(),
|
||||
force = previous.scheduleFields() != updated.scheduleFields(),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,9 +83,12 @@ class BedtimeEngine @Inject constructor(
|
||||
suspend fun onSlotFired() = lock.withLock {
|
||||
val current = settings.currentBedtime()
|
||||
val now = wallClock.now()
|
||||
apply(current, refreshRule = false, force = false, now = now)
|
||||
BedtimeSchedule.dueReminder(current, now, zones.current())?.let { window ->
|
||||
notifier.showReminder(window.start, current.reminderMinutes)
|
||||
val zone = zones.current()
|
||||
apply(current, refreshRule = false, force = false, now = now, zone = zone)
|
||||
BedtimeSchedule.dueReminder(current, now, zone)?.let { window ->
|
||||
// What is actually left: a late fire must not promise the full lead.
|
||||
val minutesLeft = ceil((window.start - now) / 1.minutes).toInt().coerceAtLeast(1)
|
||||
notifier.showReminder(window.start, minutesLeft)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +108,8 @@ class BedtimeEngine @Inject constructor(
|
||||
refreshRule: Boolean,
|
||||
force: Boolean,
|
||||
now: Instant = wallClock.now(),
|
||||
zone: ZoneId = zones.current(),
|
||||
) {
|
||||
val zone = zones.current()
|
||||
if (!current.enabled) {
|
||||
scheduler.cancel()
|
||||
notifier.cancelReminder()
|
||||
@@ -113,10 +125,13 @@ class BedtimeEngine @Inject constructor(
|
||||
// or the schedule changed under it) is stale.
|
||||
if (BedtimeSchedule.dueReminder(current, now, zone) == null) notifier.cancelReminder()
|
||||
if (mode.hasAccess()) {
|
||||
mode.install(current, refresh = refreshRule)
|
||||
if (force || active != settings.bedtimeAppliedActive()) {
|
||||
mode.setActive(active)
|
||||
settings.setBedtimeAppliedActive(active)
|
||||
// A rule that was just (re)created starts off, whatever was last
|
||||
// applied to its predecessor, so it is switched regardless.
|
||||
val created = mode.install(current, refresh = refreshRule)
|
||||
if (force || created || active != settings.bedtimeAppliedActive()) {
|
||||
// Recorded only once the system took it, so a refused call is
|
||||
// retried by the next pass rather than forgotten.
|
||||
if (mode.setActive(active)) settings.setBedtimeAppliedActive(active)
|
||||
}
|
||||
}
|
||||
val next = BedtimeSchedule.nextEvent(current, now, zone)
|
||||
@@ -125,4 +140,7 @@ class BedtimeEngine @Inject constructor(
|
||||
|
||||
/** What the system rule is built from; the reminder is not part of it. */
|
||||
private fun BedtimeSettings.ruleFields() = listOf(enabled, bedtime, wakeTime, effects)
|
||||
|
||||
/** What decides whether the mode should be on right now. */
|
||||
private fun BedtimeSettings.scheduleFields() = listOf(enabled, bedtime, wakeTime, days)
|
||||
}
|
||||
@@ -28,13 +28,14 @@ interface BedtimeMode {
|
||||
val supportsEffects: Boolean
|
||||
|
||||
/**
|
||||
* Creates the rule when it is missing. [refresh] also rewrites an existing
|
||||
* one from [settings]; otherwise an existing rule is left exactly as it is.
|
||||
* Creates the rule when it is missing, and returns whether it did. [refresh]
|
||||
* also rewrites an existing one from [settings]; otherwise an existing
|
||||
* rule is left exactly as it is.
|
||||
*/
|
||||
fun install(settings: BedtimeSettings, refresh: Boolean)
|
||||
fun install(settings: BedtimeSettings, refresh: Boolean): Boolean
|
||||
|
||||
/** Turns the mode on or off. Does nothing when there is no rule. */
|
||||
fun setActive(active: Boolean)
|
||||
/** Turns the mode on or off; false when there is no rule or the system refused. */
|
||||
fun setActive(active: Boolean): Boolean
|
||||
|
||||
/** Removes the rule, which also ends it if it is on. */
|
||||
fun uninstall()
|
||||
|
||||
@@ -60,8 +60,8 @@ class AndroidBedtimeMode @Inject constructor(
|
||||
|
||||
override fun hasAccess(): Boolean = guarded(false) { manager.isNotificationPolicyAccessGranted }
|
||||
|
||||
override fun install(settings: BedtimeSettings, refresh: Boolean) {
|
||||
guarded(Unit) {
|
||||
override fun install(settings: BedtimeSettings, refresh: Boolean): Boolean =
|
||||
guarded(false) {
|
||||
val ours = ownRules()
|
||||
// Only ever one. A second (two passes racing on another process,
|
||||
// say) is removed rather than left to fight the first.
|
||||
@@ -69,7 +69,9 @@ class AndroidBedtimeMode @Inject constructor(
|
||||
val existing = ours.entries.firstOrNull()
|
||||
if (existing == null) {
|
||||
manager.addAutomaticZenRule(rule(settings, enabled = true))
|
||||
} else if (refresh) {
|
||||
return@guarded true
|
||||
}
|
||||
if (refresh) {
|
||||
// Keeps what the user set for the rule in Settings: its on/off
|
||||
// switch (a change here must not re-enable a rule they
|
||||
// disabled) and who may interrupt, which Android 10 to 14
|
||||
@@ -79,12 +81,12 @@ class AndroidBedtimeMode @Inject constructor(
|
||||
rule(settings, enabled = existing.value.isEnabled, kept = existing.value.zenPolicy),
|
||||
)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun setActive(active: Boolean) {
|
||||
guarded(Unit) {
|
||||
val id = ownRules().keys.firstOrNull() ?: return@guarded
|
||||
override fun setActive(active: Boolean): Boolean =
|
||||
guarded(false) {
|
||||
val id = ownRules().keys.firstOrNull() ?: return@guarded false
|
||||
val state = if (active) Condition.STATE_TRUE else Condition.STATE_FALSE
|
||||
val summary = context.getString(R.string.bedtime_mode_name)
|
||||
val condition = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
@@ -95,8 +97,8 @@ class AndroidBedtimeMode @Inject constructor(
|
||||
Condition(conditionId, summary, state)
|
||||
}
|
||||
manager.setAutomaticZenRuleState(id, condition)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun uninstall() {
|
||||
guarded(Unit) {
|
||||
|
||||
@@ -24,9 +24,16 @@ object BedtimePrefs {
|
||||
|
||||
val wakeTime: Pref<TimeOfDay> = intPref("bedtime_wake_minutes", DEFAULT.wakeTime.minutesOfDay).asTimeOfDay()
|
||||
|
||||
/** The 7-bit mask `RepeatDays` uses; a corrupt value cannot widen the set. */
|
||||
/**
|
||||
* The 7-bit mask `RepeatDays` uses; a corrupt value cannot widen the set.
|
||||
* The screen never lets the last night go, so an empty mask (corrupt, or
|
||||
* written around it) reads as every night, as a backup's does.
|
||||
*/
|
||||
val days: Pref<RepeatDays> = intPref("bedtime_days", DEFAULT.days.mask)
|
||||
.map(decode = RepeatDays::fromMask, encode = { it.mask })
|
||||
.map(
|
||||
decode = { mask -> RepeatDays.fromMask(mask).takeIf { it.isRepeating } ?: RepeatDays.EVERY_DAY },
|
||||
encode = { it.mask },
|
||||
)
|
||||
|
||||
val reminderMinutes: Pref<Int> = intPref("bedtime_reminder_minutes", DEFAULT.reminderMinutes)
|
||||
.map(
|
||||
|
||||
@@ -23,11 +23,12 @@ object BedtimeSchedule {
|
||||
/**
|
||||
* The window that starts on [date], if [date] is one of the schedule's
|
||||
* nights. It ends at the first wake time after it starts, so a wake time
|
||||
* at or before the bedtime means the next morning, and an equal one a
|
||||
* full day.
|
||||
* before the bedtime means the next morning. An equal bedtime and wake
|
||||
* time is no window at all: read as a whole day, every night selected
|
||||
* would join into a Do Not Disturb that never ends.
|
||||
*/
|
||||
fun windowOn(settings: BedtimeSettings, date: LocalDate, zone: ZoneId): BedtimeWindow? {
|
||||
if (date.dayOfWeek !in settings.days) return null
|
||||
if (date.dayOfWeek !in settings.days || settings.bedtime == settings.wakeTime) return null
|
||||
val start = AlarmOccurrences.at(date, settings.bedtime, zone)
|
||||
val sameDayEnd = AlarmOccurrences.at(date, settings.wakeTime, zone)
|
||||
val end = if (sameDayEnd > start) sameDayEnd else AlarmOccurrences.at(date.plusDays(1), settings.wakeTime, zone)
|
||||
|
||||
@@ -7,6 +7,8 @@ import de.jeanlucmakiola.clockula.bedtime.BedtimeEngine
|
||||
import de.jeanlucmakiola.clockula.domain.TimeOfDay
|
||||
import de.jeanlucmakiola.clockula.domain.bedtime.BedtimeEffects
|
||||
import de.jeanlucmakiola.clockula.domain.bedtime.BedtimeSettings
|
||||
import de.jeanlucmakiola.floret.di.ApplicationScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -29,6 +31,7 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class BedtimeViewModel @Inject internal constructor(
|
||||
private val engine: BedtimeEngine,
|
||||
@param:ApplicationScope private val appScope: CoroutineScope,
|
||||
) : ViewModel() {
|
||||
|
||||
private val access = MutableStateFlow(engine.hasAccess())
|
||||
@@ -47,7 +50,7 @@ class BedtimeViewModel @Inject internal constructor(
|
||||
val granted = engine.hasAccess()
|
||||
val changed = granted != access.value
|
||||
access.value = granted
|
||||
viewModelScope.launch {
|
||||
appScope.launch {
|
||||
if (changed) runCatching { engine.resync(refreshRule = true) }
|
||||
ruleId.value = engine.ruleId()
|
||||
}
|
||||
@@ -78,8 +81,13 @@ class BedtimeViewModel @Inject internal constructor(
|
||||
private fun updateEffects(transform: (BedtimeEffects) -> BedtimeEffects) =
|
||||
update { it.copy(effects = transform(it.effects)) }
|
||||
|
||||
/**
|
||||
* On the application scope, like the editors' writes: leaving the screen
|
||||
* right after a change must not cancel it between storing the settings
|
||||
* and arming the slot or creating the rule.
|
||||
*/
|
||||
private fun update(transform: (BedtimeSettings) -> BedtimeSettings) {
|
||||
viewModelScope.launch {
|
||||
appScope.launch {
|
||||
runCatching { engine.update(transform) }
|
||||
ruleId.value = engine.ruleId()
|
||||
}
|
||||
|
||||
@@ -150,16 +150,9 @@ object ShellNavigation {
|
||||
* missing or non-positive alarm id, or an empty id list, falls back to the
|
||||
* Alarms tab (M9 D23).
|
||||
*/
|
||||
fun requestFor(
|
||||
action: String?,
|
||||
alarmId: Long?,
|
||||
alarmIds: List<Long>,
|
||||
forZenRule: Boolean = false,
|
||||
): ShellRequest? =
|
||||
fun requestFor(action: String?, alarmId: Long?, alarmIds: List<Long>): ShellRequest? =
|
||||
when (action) {
|
||||
// Settings opening the bedtime rule's configuration activity: an
|
||||
// explicit component, a rule id extra, and no action (issue #1).
|
||||
null -> if (forZenRule) ShellRequest.OpenBedtime else null
|
||||
null -> null
|
||||
InteropIntents.ACTION_OPEN_ALARM_EDITOR ->
|
||||
if (alarmId != null && alarmId > 0L) {
|
||||
ShellRequest.OpenAlarmEditor(alarmId)
|
||||
|
||||
@@ -469,7 +469,7 @@
|
||||
<string name="bedtime_effect_hide_aod">Hide the always-on display</string>
|
||||
<string name="bedtime_effects_unavailable">Screen effects need Android 15 or later. On this phone, bedtime silences notifications and calls the way Do Not Disturb does.</string>
|
||||
<string name="bedtime_open_mode">Open in system settings</string>
|
||||
<string name="bedtime_open_mode_summary">Who can reach you, and more effects, under Settings › Modes. Changes you make there are kept.</string>
|
||||
<string name="bedtime_open_mode_summary">Who can reach you, and more effects, under Settings › Modes. Changes you make there are kept while bedtime stays on.</string>
|
||||
<string name="bedtime_alarms_note">Bedtime never silences an alarm or a timer: its mode always lets alarms through.</string>
|
||||
<string name="bedtime_wellbeing_note">This is not Digital Wellbeing\'s Bedtime mode. Android lets only the system turn that one on, so if you use it too you will see two modes in Settings › Modes. Use one of them.</string>
|
||||
<string name="bedtime_mode_name">Clockula bedtime</string>
|
||||
|
||||
@@ -205,6 +205,43 @@ class BedtimeEngineTest {
|
||||
assertThat(h.notifier.showing).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a rule recreated during the night is switched on`(@TempDir tempDir: Path) = runTest {
|
||||
val h = harness(tempDir, now = instantAt(BERLIN, "2026-06-10T23:30"))
|
||||
h.engine.update(nightly)
|
||||
// Deleted in the system's settings, then the app is launched.
|
||||
h.mode.rule = null
|
||||
h.mode.on = false
|
||||
|
||||
h.engine.resync()
|
||||
|
||||
assertThat(h.mode.rule).isNotNull()
|
||||
assertThat(h.mode.on).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a late reminder says how long is actually left`(@TempDir tempDir: Path) = runTest {
|
||||
val h = harness(tempDir)
|
||||
h.engine.update { nightly(it).copy(reminderMinutes = 60) }
|
||||
|
||||
h.wallClock.instant = instantAt(BERLIN, "2026-06-10T22:41")
|
||||
h.engine.onSlotFired()
|
||||
|
||||
assertThat(h.notifier.reminders.single().second).isEqualTo(19)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an edit that cannot move a boundary leaves a hand-switched mode alone`(@TempDir tempDir: Path) = runTest {
|
||||
val h = harness(tempDir, now = instantAt(BERLIN, "2026-06-10T23:30"))
|
||||
h.engine.update(nightly)
|
||||
// Switched off by hand for tonight.
|
||||
h.mode.on = false
|
||||
|
||||
h.engine.update { it.copy(reminderMinutes = 30) }
|
||||
|
||||
assertThat(h.mode.on).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changing only the reminder does not rewrite the rule`(@TempDir tempDir: Path) = runTest {
|
||||
val h = harness(tempDir)
|
||||
|
||||
@@ -103,14 +103,11 @@ class BedtimeScheduleTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an equal bedtime and wake time is a whole day, back to back`() {
|
||||
val always = everyNight.copy(wakeTime = TimeOfDay(23, 0))
|
||||
fun `an equal bedtime and wake time is no window, never one that never ends`() {
|
||||
val same = everyNight.copy(wakeTime = TimeOfDay(23, 0))
|
||||
|
||||
val window = BedtimeSchedule.activeWindow(always, instantAt(BERLIN, "2026-06-10T22:00"), BERLIN)
|
||||
|
||||
assertThat(window?.start).isEqualTo(offsetInstant("2026-06-09T23:00+02:00"))
|
||||
assertThat(window?.end).isEqualTo(offsetInstant("2026-06-10T23:00+02:00"))
|
||||
assertThat(BedtimeSchedule.activeWindow(always, instantAt(BERLIN, "2026-06-10T23:00"), BERLIN)).isNotNull()
|
||||
assertThat(BedtimeSchedule.activeWindow(same, instantAt(BERLIN, "2026-06-10T23:30"), BERLIN)).isNull()
|
||||
assertThat(BedtimeSchedule.nextEvent(same, instantAt(BERLIN, "2026-06-10T12:00"), BERLIN)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -37,19 +37,25 @@ class FakeBedtimeMode(
|
||||
|
||||
override fun hasAccess(): Boolean = access
|
||||
|
||||
override fun install(settings: BedtimeSettings, refresh: Boolean) {
|
||||
if (!access) return
|
||||
override fun install(settings: BedtimeSettings, refresh: Boolean): Boolean {
|
||||
if (!access) return false
|
||||
if (rule == null) {
|
||||
rule = settings
|
||||
} else if (refresh) {
|
||||
// A new rule starts off, as the platform's does.
|
||||
on = false
|
||||
return true
|
||||
}
|
||||
if (refresh) {
|
||||
rule = settings
|
||||
refreshes++
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun setActive(active: Boolean) {
|
||||
if (!access || rule == null) return
|
||||
override fun setActive(active: Boolean): Boolean {
|
||||
if (!access || rule == null) return false
|
||||
on = active
|
||||
return true
|
||||
}
|
||||
|
||||
override fun uninstall() {
|
||||
|
||||
@@ -128,10 +128,11 @@ class ShellNavigationInteropTest {
|
||||
fun `the bedtime reminder and the mode's settings page both open the bedtime screen`() {
|
||||
assertThat(ShellNavigation.requestFor(BedtimeIntents.ACTION_SHOW_BEDTIME, null, emptyList()))
|
||||
.isEqualTo(ShellRequest.OpenBedtime)
|
||||
// Settings opens the rule's configuration activity with no action,
|
||||
// only the component and the rule id.
|
||||
assertThat(ShellNavigation.requestFor(null, null, emptyList(), forZenRule = true))
|
||||
.isEqualTo(ShellRequest.OpenBedtime)
|
||||
assertThat(ShellNavigation.requestFor(null, null, emptyList())).isNull()
|
||||
// MainActivity turns Settings' action-less, rule-id-carrying intent
|
||||
// into the same action; the extras themselves are its business.
|
||||
assertThat(BedtimeIntents.ZEN_RULE_EXTRAS).containsExactly(
|
||||
"android.app.extra.AUTOMATIC_RULE_ID",
|
||||
"android.service.notification.extra.RULE_ID",
|
||||
)
|
||||
}
|
||||
}
|
||||
+12
-6
@@ -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
|
||||
|
||||
Reference in new issue
Block a user