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
This commit is contained in:
makiolaj committed 2026-10-22 12:30:26 +02:00
1 parent 1a5552a221
commit 131a28c1be
4 files changed
+18 -4

No files matched your search

@@ -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) }
}
}
@@ -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,
@@ -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) }
}
+2 -1
View File
@@ -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