From 131a28c1bea0a2f4ab9d0be0759dbfb06a8e503b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 22 Oct 2026 12:30:26 +0200 Subject: [PATCH] fix(bedtime): bounded fallback window, fresh-screen access check Without exact-alarm access the slot falls back to a ten-minute window instead of plain set(), whose slack could end an eight-hour night hours late. A freshly opened bedtime screen reads whether bedtime is on from the store, not from its still-loading state, so access granted while the screen was closed creates the rule. A restore that rolled back no longer forces the mode. Refs #1 --- .../de/jeanlucmakiola/clockula/backup/BackupEngine.kt | 3 ++- .../bedtime/android/AndroidBedtimeScheduler.kt | 11 ++++++++++- .../clockula/ui/bedtime/BedtimeViewModel.kt | 5 ++++- docs/ARCHITECTURE.md | 3 ++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt b/app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt index bfeb275..42da075 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt @@ -102,7 +102,8 @@ class BackupEngine @Inject constructor( // Forced only when the bedtime settings really changed, so a // restore of the same schedule leaves a hand-switched mode be. if (settingsApplied) { - val changed = previousSettings.bedtime != content.settings.bedtime + // After a rollback the stored settings are the previous ones. + val changed = rowsReplaced && previousSettings.bedtime != content.settings.bedtime runCatching { bedtimeEngine.resync(refreshRule = changed) } } } diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/bedtime/android/AndroidBedtimeScheduler.kt b/app/src/main/java/de/jeanlucmakiola/clockula/bedtime/android/AndroidBedtimeScheduler.kt index 487b0ff..9002657 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/bedtime/android/AndroidBedtimeScheduler.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/bedtime/android/AndroidBedtimeScheduler.kt @@ -40,11 +40,20 @@ class AndroidBedtimeScheduler @Inject constructor( val exact = runCatching { manager.setExact(AlarmManager.RTC_WAKEUP, millis, pendingIntent) }.isSuccess // Guarded too: bedtime losing its slot is survivable, a crash in the // receiver's process is not. - if (!exact) runCatching { manager.set(AlarmManager.RTC_WAKEUP, millis, pendingIntent) } + // A bounded window rather than `set`, whose slack grows with the + // distance to the trigger and could end an 8-hour night hours late. + if (!exact) { + runCatching { manager.setWindow(AlarmManager.RTC_WAKEUP, millis, FALLBACK_WINDOW_MILLIS, pendingIntent) } + } } override fun cancel() = manager.cancel(slotPendingIntent()) + private companion object { + /** The platform's own minimum window from Android 12 on. */ + const val FALLBACK_WINDOW_MILLIS = 10 * 60_000L + } + private fun slotPendingIntent(): PendingIntent = PendingIntent.getBroadcast( context, BedtimeIntents.REQUEST_SLOT, diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/bedtime/BedtimeViewModel.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/bedtime/BedtimeViewModel.kt index db0c391..01a4e18 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/bedtime/BedtimeViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/bedtime/BedtimeViewModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.time.DayOfWeek @@ -54,7 +55,9 @@ class BedtimeViewModel @Inject internal constructor( appScope.launch { // Also when access was granted while this screen was closed and // the system's broadcast did not reach the app: no rule yet. - val enabled = (state.value as? BedtimeUiState.Loaded)?.settings?.enabled == true + // Read from the store, not from [state]: on a fresh screen that + // is still Loading at the first resume. + val enabled = engine.bedtime.first().enabled if (changed || (granted && enabled && engine.ruleId() == null)) { runCatching { engine.resync(refreshRule = true) } } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 165a8cb..19d4346 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2759,7 +2759,8 @@ engines (`ArchitectureRulesTest` holds it to no `android.*` import): - `BedtimeScheduler`, **one** AlarmManager slot of its own. Its PendingIntent targets `BedtimeReceiver` with its own action and request code, so arming or cancelling it cannot replace or cancel an alarm's or a timer's registration. - It is `RTC_WAKEUP` and `setExact` (plain `set` without exact-alarm access), + It is `RTC_WAKEUP` and `setExact` (a ten-minute `setWindow` without + exact-alarm access, since plain `set`'s slack grows with the distance), and deliberately **not** allow-while-idle: Android rations allow-while-idle wake-ups per app in doze (one per nine minutes on Android 10 and 11, and for the inexact fallback without exact-alarm access), and the timer's expiry slot