diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/bedtime/BedtimeEngine.kt b/app/src/main/java/de/jeanlucmakiola/clockula/bedtime/BedtimeEngine.kt
index a20c589..496fdd0 100644
--- a/app/src/main/java/de/jeanlucmakiola/clockula/bedtime/BedtimeEngine.kt
+++ b/app/src/main/java/de/jeanlucmakiola/clockula/bedtime/BedtimeEngine.kt
@@ -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
diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/domain/bedtime/BedtimeSchedule.kt b/app/src/main/java/de/jeanlucmakiola/clockula/domain/bedtime/BedtimeSchedule.kt
index 3cf1caa..fe9c2fa 100644
--- a/app/src/main/java/de/jeanlucmakiola/clockula/domain/bedtime/BedtimeSchedule.kt
+++ b/app/src/main/java/de/jeanlucmakiola/clockula/domain/bedtime/BedtimeSchedule.kt
@@ -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()
}
diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/bedtime/BedtimeScreen.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/bedtime/BedtimeScreen.kt
index ca32b6a..7547eee 100644
--- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/bedtime/BedtimeScreen.kt
+++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/bedtime/BedtimeScreen.kt
@@ -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 = {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1cff9f1..08e9ead 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -455,7 +455,7 @@
Schedule
Bedtime
Wake up
- 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.
+ 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.
Reminder
Remind me before bedtime
Off
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/bedtime/BedtimeEngineTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/bedtime/BedtimeEngineTest.kt
index 28ccf88..16068d1 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/bedtime/BedtimeEngineTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/bedtime/BedtimeEngineTest.kt
@@ -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)
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/domain/bedtime/BedtimeScheduleTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/domain/bedtime/BedtimeScheduleTest.kt
index 0846dad..fe31724 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/domain/bedtime/BedtimeScheduleTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/domain/bedtime/BedtimeScheduleTest.kt
@@ -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)
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/testing/FakeBedtimeSeams.kt b/app/src/test/java/de/jeanlucmakiola/clockula/testing/FakeBedtimeSeams.kt
index 4858ce9..d84d529 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/testing/FakeBedtimeSeams.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/testing/FakeBedtimeSeams.kt
@@ -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
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index fae1276..7f42f3e 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -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