fix(bedtime): small hours end at 06:00, retry daytime removals
Only a bedtime before 06:00 now counts as the small hours after the chosen evening, so a day sleeper's 08:00 bedtime runs on the days picked. A rule whose removal was refused while turning bedtime off in the daytime is removed by the next pass, and a restored reminder length the picker doesn't offer stays choosable. Refs #1
This commit is contained in:
1 parent
b701859156
commit
8b182c999b
8 files changed
+49
-12
No files matched your search
@@ -114,7 +114,11 @@ class BedtimeEngine @Inject constructor(
|
||||
if (!current.enabled) {
|
||||
// Already off and cleaned up: nothing to do, which keeps every
|
||||
// launch and boot of a user who never turned bedtime on cheap.
|
||||
if (!force && !refreshRule && !settings.bedtimeAppliedActive()) return
|
||||
if (!force && !refreshRule && !settings.bedtimeAppliedActive() &&
|
||||
(!mode.hasAccess() || mode.ruleId() == null)
|
||||
) {
|
||||
return
|
||||
}
|
||||
scheduler.cancel()
|
||||
notifier.cancelReminder()
|
||||
// Removing the rule ends the mode if it is on. Without access there
|
||||
|
||||
@@ -23,8 +23,9 @@ object BedtimeSchedule {
|
||||
/**
|
||||
* The window for the night of [date], if [date] is one of the schedule's
|
||||
* nights. A night is named by its evening, as people name it: a bedtime
|
||||
* before noon (00:30, say) is the small hours *after* that evening, so
|
||||
* "Thursday night" at 00:30 starts on Friday's calendar date. It ends at the first wake time after it starts, so a wake time
|
||||
* in the small hours (before 06:00; 00:30, say) is *after* that evening,
|
||||
* so "Thursday night" at 00:30 starts on Friday's calendar date. A later
|
||||
* one (a day sleeper's 08:00) is on the day itself. It ends at the first wake time after it starts, so a wake time
|
||||
* 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.
|
||||
@@ -34,7 +35,7 @@ object BedtimeSchedule {
|
||||
// Next morning or same day is decided on the local times, before DST
|
||||
// resolution: a bedtime in a spring-forward gap shifts later and must
|
||||
// not push a same-night wake time to the following day.
|
||||
val startDate = if (settings.bedtime.minutesOfDay < NOON) date.plusDays(1) else date
|
||||
val startDate = if (settings.bedtime.minutesOfDay < SMALL_HOURS_END) date.plusDays(1) else date
|
||||
val endDate =
|
||||
if (settings.wakeTime.minutesOfDay > settings.bedtime.minutesOfDay) startDate else startDate.plusDays(1)
|
||||
val start = AlarmOccurrences.at(startDate, settings.bedtime, zone)
|
||||
@@ -94,7 +95,7 @@ object BedtimeSchedule {
|
||||
private fun reminderAt(settings: BedtimeSettings, window: BedtimeWindow): Instant =
|
||||
window.start - settings.reminderMinutes.minutes
|
||||
|
||||
private const val NOON = 12 * 60
|
||||
private const val SMALL_HOURS_END = 6 * 60
|
||||
|
||||
private fun today(now: Instant, zone: ZoneId): LocalDate = now.toJavaInstant().atZone(zone).toLocalDate()
|
||||
}
|
||||
@@ -225,7 +225,8 @@ fun BedtimeScreen(
|
||||
)
|
||||
BedtimePicker.REMINDER -> OptionPicker(
|
||||
title = stringResource(R.string.bedtime_reminder),
|
||||
options = BedtimeLimits.REMINDER_CHOICES,
|
||||
// A restored value the picker does not offer stays choosable.
|
||||
options = (BedtimeLimits.REMINDER_CHOICES + settings.reminderMinutes).distinct().sorted(),
|
||||
selected = settings.reminderMinutes,
|
||||
label = { reminderText(it) },
|
||||
onSelect = {
|
||||
|
||||
@@ -455,7 +455,7 @@
|
||||
<string name="bedtime_section_schedule">Schedule</string>
|
||||
<string name="bedtime_start">Bedtime</string>
|
||||
<string name="bedtime_wake">Wake up</string>
|
||||
<string name="bedtime_days_hint">The nights bedtime is for, named by their evening: a bedtime after midnight on Thursday night falls early on Friday. A wake-up time before the bedtime is the next morning.</string>
|
||||
<string name="bedtime_days_hint">The nights bedtime is for, named by their evening: a bedtime between midnight and 6:00 on Thursday night falls early on Friday. A wake-up time before the bedtime is the next morning.</string>
|
||||
<string name="bedtime_section_reminder">Reminder</string>
|
||||
<string name="bedtime_reminder">Remind me before bedtime</string>
|
||||
<string name="bedtime_reminder_off">Off</string>
|
||||
|
||||
@@ -254,6 +254,20 @@ class BedtimeEngineTest {
|
||||
assertThat(h.mode.on).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a removal refused in the daytime is retried by the next pass`(@TempDir tempDir: Path) = runTest {
|
||||
val h = harness(tempDir)
|
||||
h.engine.update(nightly)
|
||||
h.mode.refuseRemoval = true
|
||||
h.engine.update { it.copy(enabled = false) }
|
||||
assertThat(h.mode.rule).isNotNull()
|
||||
|
||||
h.mode.refuseRemoval = false
|
||||
h.engine.resync()
|
||||
|
||||
assertThat(h.mode.rule).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `changing only the reminder does not rewrite the rule`(@TempDir tempDir: Path) = runTest {
|
||||
val h = harness(tempDir)
|
||||
|
||||
@@ -156,6 +156,19 @@ class BedtimeScheduleTest {
|
||||
.isEqualTo(offsetInstant("2026-06-15T00:30+02:00"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a day sleeper's morning bedtime stays on the day picked`() {
|
||||
val daySleep = everyNight.copy(
|
||||
bedtime = TimeOfDay(8, 0),
|
||||
wakeTime = TimeOfDay(16, 0),
|
||||
days = RepeatDays.of(DayOfWeek.MONDAY),
|
||||
)
|
||||
|
||||
// 2026-06-15 is a Monday.
|
||||
assertThat(BedtimeSchedule.activeWindow(daySleep, instantAt(BERLIN, "2026-06-15T09:00"), BERLIN))
|
||||
.isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no nights means nothing to schedule`() {
|
||||
val none = everyNight.copy(days = RepeatDays.NONE)
|
||||
|
||||
@@ -58,8 +58,11 @@ class FakeBedtimeMode(
|
||||
return true
|
||||
}
|
||||
|
||||
/** The system refusing `removeAutomaticZenRule`, as an OEM build might. */
|
||||
var refuseRemoval: Boolean = false
|
||||
|
||||
override fun uninstall(): Boolean {
|
||||
if (!access) return false
|
||||
if (!access || refuseRemoval) return false
|
||||
rule = null
|
||||
on = false
|
||||
return true
|
||||
|
||||
@@ -2769,9 +2769,9 @@ 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 nights (named by its evening, so a bedtime before noon such as
|
||||
00:30 falls on the next calendar day: "Thursday night" at 00:30 is early
|
||||
Friday) to the first wake time after it (an earlier wake time is
|
||||
one of the chosen nights (named by its evening, so a bedtime in the small hours, before 06:00, falls
|
||||
on the next calendar day: "Thursday night" at 00:30 is early Friday, while a
|
||||
day sleeper's 08:00 stays on the day picked) to the first wake time after it (an earlier wake time is
|
||||
the next morning, decided on the local times so a bedtime in a spring-forward
|
||||
gap cannot push a same-night wake time a day later; an equal one is no window
|
||||
at all, since a day-long window on every night would be a Do Not Disturb that
|
||||
@@ -2832,7 +2832,8 @@ Settings' own confirmation dialog), and Clockula cannot remove a rule without
|
||||
access, so a rule left by a revoke from outside Settings is the platform's.
|
||||
Turning bedtime off removes the rule cleanly; the "last applied" state is
|
||||
cleared only once the removal succeeded, so a refused removal is retried by the
|
||||
next pass rather than leaving the mode on.
|
||||
next pass rather than leaving the mode on (an off schedule's cheap early return
|
||||
still checks that no rule of Clockula's is left).
|
||||
|
||||
### Backup
|
||||
|
||||
|
||||
Reference in new issue
Block a user