diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmDao.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmDao.kt
index 26a209f..8af9f12 100644
--- a/app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmDao.kt
+++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmDao.kt
@@ -43,10 +43,14 @@ abstract class AlarmDao {
)
abstract suspend fun clearScheduledDate(id: Long, expected: Long, updatedAt: Long)
- /** Clears the pause only while it is still [expectedStart]..[expectedEnd]. */
+ /**
+ * Clears the pause only while it is still [expectedStart]..[expectedEnd],
+ * in either order: the mapper reads a reversed row as the ordered range.
+ */
@Query(
"UPDATE alarms SET pause_start = NULL, pause_end = NULL, updated_at = :updatedAt " +
- "WHERE id = :id AND pause_start = :expectedStart AND pause_end = :expectedEnd",
+ "WHERE id = :id AND MIN(pause_start, pause_end) = :expectedStart " +
+ "AND MAX(pause_start, pause_end) = :expectedEnd",
)
abstract suspend fun clearPause(id: Long, expectedStart: Long, expectedEnd: Long, updatedAt: Long)
diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmDatePickers.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmDatePickers.kt
index 0210ce9..cd90c84 100644
--- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmDatePickers.kt
+++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmDatePickers.kt
@@ -126,6 +126,9 @@ internal fun PauseRangePickerDialog(
)
val pickedStart = state.selectedStartDateMillis
val pickedEnd = state.selectedEndDateMillis ?: pickedStart
+ // A begun pause may keep its start in the past, but a range that is over
+ // before today would pause nothing, so it cannot be confirmed.
+ val confirmable = pickedEnd != null && !PickerDates.toDate(pickedEnd).isBefore(today)
DatePickerDialog(
onDismissRequest = onDismiss,
confirmButton = {
@@ -135,7 +138,7 @@ internal fun PauseRangePickerDialog(
onConfirm(PauseRange.ordered(PickerDates.toDate(pickedStart), PickerDates.toDate(pickedEnd)))
}
},
- enabled = pickedStart != null,
+ enabled = confirmable,
) { Text(stringResource(R.string.action_ok)) }
},
dismissButton = { DismissButtons(onRemove = onRemove, onDismiss = onDismiss) },
diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModel.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModel.kt
index 87d45f8..e897411 100644
--- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModel.kt
+++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorViewModel.kt
@@ -146,6 +146,9 @@ class AlarmEditorViewModel @Inject internal constructor(
/**
* null removes the date. A date the picker should not have offered — one
* on which this alarm's time is already past — is refused, not stored.
+ * Picking a date switches the alarm on: a date is a request for it to
+ * ring then, and a switched-off alarm would show "Scheduled for" and stay
+ * silent.
*/
fun onScheduledDateChosen(date: LocalDate?) {
val now = wallClock.now()
@@ -155,7 +158,7 @@ class AlarmEditorViewModel @Inject internal constructor(
alarm.repeatDays.isRepeating -> alarm
date == null -> alarm.copy(scheduledDate = null)
!AlarmSchedule.isAhead(date, alarm.time, now, zone) -> alarm
- else -> alarm.copy(scheduledDate = date)
+ else -> alarm.copy(scheduledDate = date, enabled = true)
}
}
}
diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmRow.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmRow.kt
index 0f254be..4cc6cb0 100644
--- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmRow.kt
+++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmRow.kt
@@ -157,16 +157,17 @@ internal fun scheduleSummaryText(row: AlarmRowState): String {
}
/**
- * "Paused Oct 26 – 28" while the pause is still ahead, "Paused, back on Thu,
- * Oct 29" once it has begun: by then its start is history, and naming the
- * pause's last day would read as the day it ends *before*.
+ * "Paused Oct 26 – 28" while the pause is still ahead, "Paused through Wed,
+ * Oct 28" once it has begun: by then its start is history. "Through", not
+ * "until", because the last day is itself paused; and the last day rather than
+ * the day after, which for a weekday alarm need not be a day it rings.
*/
@Composable
private fun pausedText(pause: PauseRange, begun: Boolean): String =
if (!begun) {
stringResource(R.string.alarm_paused_dates, pauseRangeText(pause))
} else {
- stringResource(R.string.alarm_paused_back_on, scheduledDateText(pause.end.plusDays(1)))
+ stringResource(R.string.alarm_paused_through, scheduledDateText(pause.end))
}
/** Ringing and snoozed say so; anything else is the countdown, or nothing. */
diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/widget/NextAlarmWidgetContent.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/widget/NextAlarmWidgetContent.kt
index fad435d..eb05606 100644
--- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/widget/NextAlarmWidgetContent.kt
+++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/widget/NextAlarmWidgetContent.kt
@@ -139,7 +139,7 @@ private fun timeLabel(context: Context, model: NextAlarmWidgetModel): String {
private fun dayLabel(context: Context, model: NextAlarmWidgetModel): String = when {
model.isToday -> context.getString(R.string.widget_next_alarm_today)
- model.date != null -> Locale.getDefault().let { locale ->
+ model.date != null -> currentLocale(context).let { locale ->
// The year only when it is not this one, as the alarm list does.
val skeleton = if (model.dateNeedsYear) "yEEEMMMd" else "EEEMMMd"
DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(locale, skeleton), locale).format(model.date)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9aef65a..051ebd3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -122,8 +122,8 @@
Scheduled for %1$s
Paused %1$s
-
- Paused, back on %1$s
+
+ Paused through %1$s
" \u00b7 "
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/alarm/AlarmEngineScheduleTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/alarm/AlarmEngineScheduleTest.kt
index c0dc523..4db1d81 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/alarm/AlarmEngineScheduleTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/alarm/AlarmEngineScheduleTest.kt
@@ -107,4 +107,18 @@ class AlarmEngineScheduleTest {
// A read never writes: the ended pause waits for the next reschedule.
assertThat(harness.alarms.find(1L)!!.pause).isEqualTo(pause)
}
+
+ @Test
+ fun `a spent pause stored with its ends reversed is cleared too`(@TempDir tempDir: Path) = runTest {
+ val harness = alarmEngineHarness(tempDir, now = instantAt(BERLIN, "2026-06-20T05:00"))
+ val entity = AlarmMapper.toEntity(alarmAt(hour = 7, repeatDays = RepeatDays.EVERY_DAY)).copy(
+ pauseStart = LocalDate.of(2026, 6, 12).toEpochDay(),
+ pauseEnd = LocalDate.of(2026, 6, 10).toEpochDay(),
+ )
+ harness.alarmDao.insert(entity)
+
+ harness.engine.reschedule()
+
+ assertThat(harness.alarmDao.stored.single().pauseStart).isNull()
+ }
}
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/testing/FakeDaos.kt b/app/src/test/java/de/jeanlucmakiola/clockula/testing/FakeDaos.kt
index 8c6f1e5..4469e06 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/testing/FakeDaos.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/testing/FakeDaos.kt
@@ -80,7 +80,11 @@ open class FakeAlarmDao(
override suspend fun clearPause(id: Long, expectedStart: Long, expectedEnd: Long, updatedAt: Long) {
rows.update { rows ->
rows.map {
- if (it.id == id && it.pauseStart == expectedStart && it.pauseEnd == expectedEnd) {
+ val start = it.pauseStart
+ val end = it.pauseEnd
+ val matches = start != null && end != null &&
+ minOf(start, end) == expectedStart && maxOf(start, end) == expectedEnd
+ if (it.id == id && matches) {
it.copy(pauseStart = null, pauseEnd = null, updatedAt = updatedAt)
} else {
it
diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorScheduleTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorScheduleTest.kt
index ba02a3d..e8b1d94 100644
--- a/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorScheduleTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/clockula/ui/alarms/AlarmEditorScheduleTest.kt
@@ -279,4 +279,20 @@ class AlarmEditorScheduleTest {
assertThat(harness.scheduler.next?.fireAt).isEqualTo(offsetInstant("2026-06-11T08:00+02:00"))
}
+
+ @Test
+ fun `scheduling a date on a switched-off alarm switches it on`(@TempDir tempDir: Path) =
+ runTest(main.dispatcher) {
+ val harness = alarmEngineHarness(tempDir, now = fiveToday)
+ harness.given(alarmAt(hour = 7, enabled = false))
+ val viewModel = editor(harness)
+ advanceUntilIdle()
+
+ viewModel.onScheduledDateChosen(june18)
+ advanceUntilIdle()
+
+ assertThat(harness.stored().enabled).isTrue()
+ assertThat(harness.scheduler.next)
+ .isEqualTo(Registration(offsetInstant("2026-06-18T07:00+02:00"), 1L))
+ }
}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index fe334f7..3a3260a 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -1341,7 +1341,8 @@ date only while its occurrence is still ahead and a pause only while its last
day has not passed. The editor's date picker starts at `AlarmSchedule.earliestDate`,
the date of the alarm's next plain occurrence, so neither a past day nor
today-after-the-time-has-gone can be picked; moving the time earlier than now
-on a date set for today drops the date. Picking a repeat day clears the date
+on a date set for today drops the date. Picking a date switches the alarm on, since
+a switched-off alarm would read "Scheduled for" and stay silent. Picking a repeat day clears the date
and clearing the last one clears the pause, as in Google Clock. The
`AlarmClock` contract cannot express either field, so `SET_ALARM` never
re-uses an alarm that carries one (§17).