From cac5b2dfca4d66649e7debbf2074a5ad9666a675 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 5 Oct 2026 19:28:16 +0200 Subject: [PATCH] refactor(settings): remove the unused default timer duration Settings > Timers offered a "Default duration", but nothing that creates a timer ever read it: the keypad starts empty. The row, its preference, ClockDefaults.timerDuration and the backup field are gone, and the screen's hint and the hub's subtitle now only speak of the sound. Backups no longer write default_timer_duration_millis. Older files that carry it still import, since the codec ignores unknown keys. Closes #5 --- .../clockula/backup/BackupEngine.kt | 1 - .../clockula/data/backup/BackupCodec.kt | 2 - .../clockula/data/backup/BackupDto.kt | 7 +++- .../clockula/data/prefs/ClockPrefs.kt | 17 -------- .../clockula/data/prefs/SettingsPrefs.kt | 6 +-- .../clockula/domain/ClockDefaults.kt | 4 -- .../clockula/domain/timer/TimerPresets.kt | 2 +- .../clockula/ui/settings/SettingsScreen.kt | 2 +- .../clockula/ui/settings/SettingsViewModel.kt | 3 -- .../ui/settings/TimerDefaultsScreen.kt | 39 ++----------------- .../clockula/ui/timers/TimerSetupScreen.kt | 4 +- .../clockula/ui/timers/TimersViewModel.kt | 2 +- app/src/main/res/values/strings.xml | 9 +---- .../clockula/data/backup/BackupCodecTest.kt | 27 ++++++++++++- .../clockula/data/prefs/ClockPrefsTest.kt | 3 -- .../ui/settings/SettingsViewModelTest.kt | 3 -- docs/ARCHITECTURE.md | 17 ++++---- docs/BACKUP.md | 8 ++-- 18 files changed, 55 insertions(+), 101 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 1118894..acf28b5 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt @@ -76,7 +76,6 @@ class BackupEngine @Inject constructor( settings.setAlarmRingtoneUri(backup.defaults.alarmRingtoneUri) settings.setTimerRingtoneUri(backup.defaults.timerRingtoneUri) settings.setDismissChallenge(backup.defaults.dismissChallenge) - settings.setTimerDuration(backup.defaults.timerDuration) settings.setHomeZoneId(backup.defaults.homeZoneId) settings.setTimerPresets(backup.timerPresets) } diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt index f87fcb0..69b07af 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt @@ -103,7 +103,6 @@ object BackupCodec { alarmRingtoneUri = defaults.alarmRingtoneUri, timerRingtoneUri = defaults.timerRingtoneUri, dismissChallenge = defaults.dismissChallenge.name, - timerDurationMillis = defaults.timerDuration.inWholeMilliseconds, homeZoneId = defaults.homeZoneId, timerPresets = timerPresets.map { it.inWholeMilliseconds }, ) @@ -175,7 +174,6 @@ object BackupCodec { alarmRingtoneUri = settingsDto.alarmRingtoneUri?.takeIf { it.isNotBlank() }, timerRingtoneUri = settingsDto.timerRingtoneUri?.takeIf { it.isNotBlank() }, dismissChallenge = settingsDto.dismissChallenge.toEnum(DismissChallenge.NONE), - timerDuration = settingsDto.timerDurationMillis.milliseconds, homeZoneId = settingsDto.homeZoneId?.let(Zones::normalise), ), timerPresets = settingsDto.timerPresets.map { it.milliseconds }, diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt index 9ae8d52..35c15c5 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt @@ -54,7 +54,11 @@ data class WorldClockDto( @SerialName("sort_order") val sortOrder: Int = 0, ) -/** Field names mirror `ClockPrefs`'/`SettingsPrefs`' on-disk keys. */ +/** + * Field names mirror `ClockPrefs`'/`SettingsPrefs`' on-disk keys. Older files + * may still carry `default_timer_duration_millis`, a setting that no longer + * exists; the codec ignores unknown keys, so they still import. + */ @Serializable data class SettingsDto( @SerialName("theme_mode") val themeMode: String = "SYSTEM", @@ -66,7 +70,6 @@ data class SettingsDto( @SerialName("default_alarm_ringtone_uri") val alarmRingtoneUri: String? = null, @SerialName("default_timer_ringtone_uri") val timerRingtoneUri: String? = null, @SerialName("default_dismiss_challenge") val dismissChallenge: String = "NONE", - @SerialName("default_timer_duration_millis") val timerDurationMillis: Long = 300_000L, @SerialName("home_zone_id") val homeZoneId: String? = null, @SerialName("timer_presets") val timerPresets: List = emptyList(), ) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefs.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefs.kt index b8618aa..f3e6a75 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefs.kt @@ -6,13 +6,7 @@ import de.jeanlucmakiola.floret.prefs.Pref import de.jeanlucmakiola.floret.prefs.booleanPref import de.jeanlucmakiola.floret.prefs.enumPref import de.jeanlucmakiola.floret.prefs.intPref -import de.jeanlucmakiola.floret.prefs.longPref import de.jeanlucmakiola.floret.prefs.nullableStringPref -import kotlin.time.Duration -import kotlin.time.Duration.Companion.hours -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.minutes -import kotlin.time.Duration.Companion.seconds /** * The clock defaults' on-disk keys. Reads and writes both clamp through @@ -21,9 +15,6 @@ import kotlin.time.Duration.Companion.seconds */ object ClockPrefs { - private val MIN_TIMER_DURATION = 1.seconds - private val MAX_TIMER_DURATION = 24.hours - val snoozeMinutes: Pref = intPref("default_snooze_minutes", 10).clampedTo(1..60) val snoozeLimit: Pref = intPref("default_snooze_limit", 3).clampedTo(0..10) @@ -39,14 +30,6 @@ object ClockPrefs { val dismissChallenge: Pref = enumPref("default_dismiss_challenge", DismissChallenge.NONE) - val timerDurationMillis: Pref = longPref( - "default_timer_duration_millis", - 5.minutes.inWholeMilliseconds, - ).map( - decode = { it.milliseconds.coerceIn(MIN_TIMER_DURATION, MAX_TIMER_DURATION) }, - encode = { it.coerceIn(MIN_TIMER_DURATION, MAX_TIMER_DURATION).inWholeMilliseconds }, - ) - /** Normalised through [Zones] both ways, so a zone the device dropped reads as absent. */ val homeZoneId: Pref = nullableStringPref("home_zone_id") .map(decode = Zones::normalise, encode = Zones::normalise) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt index 4db715c..3b8aca1 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt @@ -62,13 +62,11 @@ class SettingsPrefs @Inject constructor(private val store: PrefStore) { }, store.flow(ClockPrefs.alarmRingtoneUri), store.flow(ClockPrefs.timerRingtoneUri), - store.flow(ClockPrefs.timerDurationMillis), store.flow(ClockPrefs.homeZoneId), - ) { partial, alarmRingtoneUri, timerRingtoneUri, timerDuration, homeZoneId -> + ) { partial, alarmRingtoneUri, timerRingtoneUri, homeZoneId -> partial.copy( alarmRingtoneUri = alarmRingtoneUri, timerRingtoneUri = timerRingtoneUri, - timerDuration = timerDuration, homeZoneId = homeZoneId, ) }.distinctUntilChanged() @@ -90,8 +88,6 @@ class SettingsPrefs @Inject constructor(private val store: PrefStore) { suspend fun setDismissChallenge(challenge: DismissChallenge) = store.set(ClockPrefs.dismissChallenge, challenge) - suspend fun setTimerDuration(duration: Duration) = store.set(ClockPrefs.timerDurationMillis, duration) - suspend fun setHomeZoneId(zoneId: String?) = store.set(ClockPrefs.homeZoneId, zoneId) // --- timer presets (M6) --- diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/domain/ClockDefaults.kt b/app/src/main/java/de/jeanlucmakiola/clockula/domain/ClockDefaults.kt index e7336d3..8c90bfa 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/domain/ClockDefaults.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/domain/ClockDefaults.kt @@ -1,8 +1,5 @@ package de.jeanlucmakiola.clockula.domain -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes - /** * The app-wide defaults an alarm or timer inherits when it has no override. * Backed by DataStore, edited in M10's settings screen. @@ -15,6 +12,5 @@ data class ClockDefaults( val alarmRingtoneUri: String? = null, val timerRingtoneUri: String? = null, val dismissChallenge: DismissChallenge = DismissChallenge.NONE, - val timerDuration: Duration = 5.minutes, val homeZoneId: String? = null, ) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/domain/timer/TimerPresets.kt b/app/src/main/java/de/jeanlucmakiola/clockula/domain/timer/TimerPresets.kt index ed332ef..8e89c90 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/domain/timer/TimerPresets.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/domain/timer/TimerPresets.kt @@ -12,7 +12,7 @@ object TimerPresets { val BUILT_IN: List = listOf(1.minutes, 2.minutes, 3.minutes, 5.minutes, 10.minutes, 15.minutes, 30.minutes, 1.hours) - /** `ClockPrefs`' own clamp, so nothing the user can choose is clamped away on the next read. */ + /** The range a timer can run for: what the keypad can start and what a preset can hold. */ val MIN: Duration = 1.seconds val MAX: Duration = 24.hours diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsScreen.kt index e282032..21f3d72 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsScreen.kt @@ -206,7 +206,7 @@ private fun SettingsHub( ) GroupedRow( title = stringResource(R.string.settings_section_timers), - summary = stringResource(R.string.settings_timers_subtitle), + summary = stringResource(R.string.settings_timers_sound_subtitle), position = Position.Middle, leading = { CategoryIcon(Icons.Default.Timer, ChipAccent.Tertiary) }, onClick = { onOpenSection(SettingsSection.Timers) }, diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModel.kt index 75876f9..a3ac056 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModel.kt @@ -34,7 +34,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlin.time.Duration import javax.inject.Inject /** @@ -139,8 +138,6 @@ class SettingsViewModel @Inject internal constructor( fun onDismissChallengeChanged(value: DismissChallenge) = viewModelScope.launch { settings.setDismissChallenge(value) } - fun onTimerDurationChanged(value: Duration) = viewModelScope.launch { settings.setTimerDuration(value) } - // --- the default ringtone pickers --- internal fun onOpenRingtonePicker(kind: DefaultRingtoneKind) { diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/TimerDefaultsScreen.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/TimerDefaultsScreen.kt index 88c8938..cc68b9a 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/TimerDefaultsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/TimerDefaultsScreen.kt @@ -3,63 +3,30 @@ package de.jeanlucmakiola.clockula.ui.settings import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import de.jeanlucmakiola.clockula.R -import de.jeanlucmakiola.clockula.domain.timer.TimerPresets import de.jeanlucmakiola.floret.components.CollapsingScaffold import de.jeanlucmakiola.floret.components.GroupedRow -import de.jeanlucmakiola.floret.components.OptionPicker import de.jeanlucmakiola.floret.components.Position -import kotlin.time.Duration -/** What a new timer starts with: its sound and its length. */ +/** What a new timer starts with: its sound. */ @Composable internal fun TimerDefaultsScreen( state: SettingsUiState.Loaded, viewModel: SettingsViewModel, onBack: () -> Unit, ) { - var showDuration by rememberSaveable { mutableStateOf(false) } - CollapsingScaffold(title = stringResource(R.string.settings_section_timers), onBack = onBack) { - SettingsHint(stringResource(R.string.settings_timer_defaults_hint)) + SettingsHint(stringResource(R.string.settings_timer_sound_hint)) Spacer(Modifier.height(8.dp)) GroupedRow( title = stringResource(R.string.editor_ringtone), summary = ringtoneSummary(state.defaults.timerRingtoneUri), - position = Position.Top, + position = Position.Alone, onClick = { viewModel.onOpenRingtonePicker(DefaultRingtoneKind.TIMER) }, ) - GroupedRow( - title = stringResource(R.string.settings_timer_duration), - summary = durationMinutesText(state.defaults.timerDuration), - position = Position.Bottom, - onClick = { showDuration = true }, - ) - } - - if (showDuration) { - OptionPicker( - title = stringResource(R.string.settings_timer_duration), - options = TimerPresets.BUILT_IN, - selected = state.defaults.timerDuration, - label = { durationMinutesText(it) }, - onSelect = viewModel::onTimerDurationChanged, - onDismiss = { showDuration = false }, - ) } } - -@Composable -private fun durationMinutesText(duration: Duration): String { - val minutes = duration.inWholeMinutes.toInt() - return pluralStringResource(R.plurals.settings_duration_minutes, minutes, minutes) -} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/timers/TimerSetupScreen.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/timers/TimerSetupScreen.kt index 5dab5a3..92d4c10 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/timers/TimerSetupScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/timers/TimerSetupScreen.kt @@ -173,9 +173,7 @@ internal fun StartRow( } FilledIconButton( onClick = onStart, - // Nothing outside 1 s…24 h is startable, which is `ClockPrefs`' - // own clamp — so nothing the user can choose is clamped away on - // the next read (M5 D16). + // Nothing outside 1 s…24 h (`TimerPresets.MIN..MAX`) is startable. enabled = entry.canStart, modifier = Modifier .size(StartButtonSize) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/timers/TimersViewModel.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/timers/TimersViewModel.kt index 0f833c6..513164e 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/timers/TimersViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/timers/TimersViewModel.kt @@ -52,7 +52,7 @@ class TimersViewModel @Inject internal constructor( /** Creates a timer of [duration] and starts it. Re-entrant taps create one timer (D23). */ fun onStartNew(duration: Duration) { if (creating) return - // Outside `ClockPrefs`' own clamp there is nothing honest to create: the + // Outside `TimerPresets.MIN..MAX` there is nothing honest to create: the // panel's Start is disabled there, so this is the stale-caller guard. if (duration !in TimerPresets.MIN..TimerPresets.MAX) return creating = true diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38e94b9..733f2cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -294,13 +294,8 @@ Sound, snooze and dismiss for new alarms New alarms start with these. Each alarm can still change them in its editor. Timers - Sound and length for new timers - New timers start with these. - Default duration - - %1$d minute - %1$d minutes - + Sound for new timers + New timers ring with this sound. Each timer can still change it in its editor. Clock Home time zone World clocks show how far each city is from your home time zone. diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt index 3ef6cc8..930e2f3 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt @@ -58,7 +58,6 @@ class BackupCodecTest { alarmRingtoneUri = "content://media/1", timerRingtoneUri = "content://media/2", dismissChallenge = DismissChallenge.HOLD, - timerDuration = 10.minutes, homeZoneId = "America/New_York", ), timerPresets = listOf(1.minutes, 2.minutes, 10.minutes), @@ -113,6 +112,32 @@ class BackupCodecTest { assertThat(decoded).isInstanceOf(BackupResult.Success::class.java) } + @Test + fun `an older file with the retired default timer duration still imports`() { + val json = """ + { + "format": "clockula-backup", + "version": 1, + "exportedAt": 0, + "alarms": [], + "timers": [], + "worldClocks": [], + "settings": { "default_snooze_minutes": 7, "default_timer_duration_millis": 600000 } + } + """.trimIndent() + + val decoded = BackupCodec.decode(json) + + assertThat(decoded).isInstanceOf(BackupResult.Success::class.java) + val defaults = (decoded as BackupResult.Success).content.settings.defaults + assertThat(defaults).isEqualTo(ClockDefaults(snoozeMinutes = 7)) + } + + @Test + fun `the retired default timer duration is no longer written`() { + assertThat(BackupCodec.encode(sampleContent())).doesNotContain("default_timer_duration_millis") + } + @Test fun `wrong format rejects the whole file`() { val decoded = BackupCodec.decode(minimalDocument(format = "something-else")) diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt index db30250..869cca3 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path -import kotlin.time.Duration.Companion.seconds /** * The clock defaults every alarm and timer inherits. A value the user could @@ -53,7 +52,6 @@ class ClockPrefsTest { prefs.setAlarmRingtoneUri("content://a") prefs.setTimerRingtoneUri("content://t") prefs.setDismissChallenge(DismissChallenge.HOLD) - prefs.setTimerDuration(90.seconds) prefs.setHomeZoneId("Europe/Berlin") assertThat(prefs.defaults.first()).isEqualTo( @@ -65,7 +63,6 @@ class ClockPrefsTest { alarmRingtoneUri = "content://a", timerRingtoneUri = "content://t", dismissChallenge = DismissChallenge.HOLD, - timerDuration = 90.seconds, homeZoneId = "Europe/Berlin", ), ) diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt index 94e4a53..1af202e 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt @@ -21,7 +21,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension import org.junit.jupiter.api.io.TempDir import java.nio.file.Path -import kotlin.time.Duration.Companion.minutes /** * The real [SettingsViewModel] over the real [de.jeanlucmakiola.clockula.data.prefs.SettingsPrefs] @@ -120,7 +119,6 @@ class SettingsViewModelTest { viewModel.onSnoozeLimitChanged(2) viewModel.onVolumeRampChanged(30) viewModel.onDismissChallengeChanged(DismissChallenge.MATH) - viewModel.onTimerDurationChanged(20.minutes) advanceUntilIdle() val defaults = (viewModel.state.value as SettingsUiState.Loaded).defaults @@ -129,7 +127,6 @@ class SettingsViewModelTest { assertThat(defaults.snoozeLimit).isEqualTo(2) assertThat(defaults.volumeRampSeconds).isEqualTo(30) assertThat(defaults.dismissChallenge).isEqualTo(DismissChallenge.MATH) - assertThat(defaults.timerDuration).isEqualTo(20.minutes) } @Test diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index eebcd8e..6d12835 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -597,7 +597,7 @@ Four slices: | Slice | Keys | Owner | |---|---|---| | Appearance (M1) | `theme_mode`, `dynamic_color` | `AppearancePrefs` in `core-prefs` — the family's shared key names | -| Clock defaults (M2) | `default_snooze_minutes`, `default_snooze_limit`, `default_vibrate`, `default_volume_ramp_seconds`, `default_alarm_ringtone_uri`, `default_timer_ringtone_uri`, `default_dismiss_challenge`, `default_timer_duration_millis`, `home_zone_id` | `ClockPrefs`, surfaced as `SettingsPrefs.defaults: Flow` | +| Clock defaults (M2) | `default_snooze_minutes`, `default_snooze_limit`, `default_vibrate`, `default_volume_ramp_seconds`, `default_alarm_ringtone_uri`, `default_timer_ringtone_uri`, `default_dismiss_challenge`, `home_zone_id` | `ClockPrefs`, surfaced as `SettingsPrefs.defaults: Flow` | | Stopwatch run record (M2) | `stopwatch_state`, `stopwatch_started_elapsed_millis`, `stopwatch_accumulated_millis`, `stopwatch_last_lap_cumulative_millis` — the last of which M7 is the first caller of, for the in-progress lap's split | `StopwatchPrefs`, behind `StopwatchStateStore` | | System facts (M3) | `last_boot_id` | `SystemPrefs`, behind `BootStateStore` — the persisted half of the reboot gate | | Timers (M6) | `timer_presets`, `timer_ring_sounding_since_millis` | `TimerPrefs` — the presets surfaced as `SettingsPrefs.timerPresets`, the ring session's anchor behind `TimerRingStateStore` | @@ -612,7 +612,7 @@ process killed mid-write — ever sees `RUNNING` without its start anchor. **Clamp on read as well as on write.** Every bounded value goes through `Pref.map`, which applies the same clamp in both directions: snooze 1–60 -minutes, snooze limit 0–10, volume ramp 0–60 s, timer duration 1 s–24 h. A +minutes, snooze limit 0–10, volume ramp 0–60 s. A hand-edited file or a restore from a future version therefore cannot produce a value the user could never have chosen. Unknown enum names degrade to their default; a `home_zone_id` the device's tzdata no longer knows reads back as @@ -1063,7 +1063,7 @@ M10 adds no ``. | Reordering timers by hand. `sort_order` and `TimerRepository.reorder` exist and stay caller-less: the list is in storage order, M5 gave the same answer for alarms, and a drag surface would also mean abandoning the `LazyColumn` | not planned for v1 | | Per-timer vibrate, volume-ramp or dismiss-challenge overrides. `timers` has exactly one nullable settings column, its ringtone; four more would be a schema change for settings the roadmap does not ask for | not planned for v1 | | A "stop all" for several expired timers, and undo after a timer delete. Each expired timer is acknowledged on its own, and the delete confirmation asks before rather than after | not planned for v1 | -| A presets management screen (add/remove the quick-start durations themselves). M10's settings screen edits `default_timer_duration_millis` and `default_timer_ringtone_uri`, but the preset list is still only editable from the two-gesture surface on the setup panel | not planned for v1 | +| A presets management screen (add/remove the quick-start durations themselves). M10's settings screen edits `default_timer_ringtone_uri`, but the preset list is still only editable from the two-gesture surface on the setup panel | not planned for v1 | | A timer that ramps its volume, a dismiss challenge or a snooze for a timer | not planned for v1 | | Surfacing `TimerSnapshot.anchorIsStale` in the UI. The boot repair makes it vanishingly rare and the value is already honest; a row does not say "estimated" in v1 | not planned for v1 | | Lap export, sharing, deletion or editing, and any "previous run" history. A run's laps belong to the run that produced them; keeping old runs is a different feature with its own table | not planned for v1 | @@ -2018,16 +2018,17 @@ death; the pure `TimerDurationEntry` is derived from the saved digit string, and digits in from the right and reads them as `h*3600 + m*60 + s` **without clamping minutes or seconds to 59**, so `0:00:99` is 99 seconds — Google Clock's forgiving behaviour, and a pure function with obvious tests. `canStart` is false -outside 1 s…24 h, which is `ClockPrefs`' own clamp, so nothing the user can -choose is clamped away on the next read. +outside 1 s…24 h. Presets are **app-wide** quick-start durations in DataStore (§6), not a property of one row: a `timers` row already *is* its own preset, because it persists and `reset` returns it to its configured duration. The whole management surface is two gestures on the setup panel — a Save chip when the typed duration is not -already one, and each chip's own remove affordance. A new timer's pre-filled -duration is `ClockDefaults.timerDuration`, the `default_timer_duration_millis` -preference M2 built and nothing had called until now. +already one, and each chip's own remove affordance. The keypad starts empty; +there is no default timer length. (M2 built a `default_timer_duration_millis` +preference and M10 gave it a settings row, but nothing that creates a timer +ever read it, so it was removed. A backup file that still carries the key +imports with it ignored.) A timer has exactly **one** per-setting override, its ringtone, because `timers` has exactly one nullable settings column. Vibrate and the (zero) ramp come from diff --git a/docs/BACKUP.md b/docs/BACKUP.md index 9447366..dafe8d2 100644 --- a/docs/BACKUP.md +++ b/docs/BACKUP.md @@ -33,7 +33,7 @@ written here. defines — `format`, `version`, an alarm's `hour`/`minute`, a world clock's `zone_id`, a timer's `duration_millis` — rejects the **whole file** with a specific reason. Nothing is imported half-way. -- Settings values (snooze minutes, volume ramp, timer duration, …) are +- Settings values (snooze minutes, volume ramp, …) are **not** range-checked at parse time; they pass through the same clamps `ClockPrefs`/`SettingsPrefs` already apply on every write, so a value from a future, more permissive version degrades to the nearest valid one instead @@ -118,13 +118,15 @@ device or after a reboot (`docs/PLAN.md` §5). Every imported timer lands "default_alarm_ringtone_uri": null, "default_timer_ringtone_uri": null, "default_dismiss_challenge": "NONE", - "default_timer_duration_millis": 300000, "home_zone_id": null, "timer_presets": [60000, 120000, 180000, 300000, 600000, 900000, 1800000, 3600000] } ``` -Field names mirror `ClockPrefs`'/`SettingsPrefs`' own on-disk keys. Every +Field names mirror `ClockPrefs`'/`SettingsPrefs`' own on-disk keys. Older +files may also carry `default_timer_duration_millis`, from a default timer +length setting that has since been removed; it is ignored on import like any +other unknown field. Every numeric/duration value is clamped on *apply*, through the existing pref setters — never on parse.