fix: keep the built-in timer presets when a backup has none

A backup without the timer_presets field decoded it as an empty list,
which means "the user removed them all", so restoring it wiped the
built-in presets. A missing field now keeps them. BACKUP.md describes
this and the per-alarm clamps.
This commit is contained in:
makiolaj committed 2026-10-21 17:37:11 +02:00
1 parent 805da39074
commit 3003f75333
4 files changed
+27 -3

No files matched your search

@@ -189,7 +189,7 @@ object BackupCodec {
val settingsDto = settings
if ((settingsDto.alarmRingtoneUri?.length ?: 0) > MAX_URI_CHARS ||
(settingsDto.timerRingtoneUri?.length ?: 0) > MAX_URI_CHARS ||
settingsDto.timerPresets.size > 1_000
(settingsDto.timerPresets?.size ?: 0) > 1_000
) {
return BackupResult.Failure("settings contain oversized values")
}
@@ -206,7 +206,7 @@ object BackupCodec {
dismissChallenge = settingsDto.dismissChallenge.toEnum(DismissChallenge.NONE),
homeZoneId = settingsDto.homeZoneId?.let(Zones::normalise),
),
timerPresets = settingsDto.timerPresets.map { it.milliseconds },
timerPresets = settingsDto.timerPresets?.map { it.milliseconds } ?: TimerPresets.BUILT_IN,
)
return BackupResult.Success(
@@ -71,5 +71,10 @@ data class SettingsDto(
@SerialName("default_timer_ringtone_uri") val timerRingtoneUri: String? = null,
@SerialName("default_dismiss_challenge") val dismissChallenge: String = "NONE",
@SerialName("home_zone_id") val homeZoneId: String? = null,
@SerialName("timer_presets") val timerPresets: List<Long> = emptyList(),
/**
* Absent is not empty: an empty list is a user who removed every preset,
* while a file without the field (an older or hand-written one) keeps the
* built-in ones.
*/
@SerialName("timer_presets") val timerPresets: List<Long>? = null,
)
@@ -257,6 +257,19 @@ class BackupCodecTest {
assertThat(timers.map { it.duration }).containsExactly(TimerPresets.MIN, TimerPresets.MAX).inOrder()
}
@Test
fun `a file without timer presets keeps the built-in ones`() {
val settings = (BackupCodec.decode(minimalDocument()) as BackupResult.Success).content.settings
assertThat(settings.timerPresets).isEqualTo(TimerPresets.BUILT_IN)
}
@Test
fun `an empty preset list is honoured as all removed`() {
val json = minimalDocument().replace("\"settings\": {}", "\"settings\": {\"timer_presets\": []}")
val settings = (BackupCodec.decode(json) as BackupResult.Success).content.settings
assertThat(settings.timerPresets).isEmpty()
}
@Test
fun `garbage text fails rather than throwing`() {
assertThat(BackupCodec.decode("not json at all")).isInstanceOf(BackupResult.Failure::class.java)
+6
View File
@@ -38,6 +38,12 @@ written here.
`ClockPrefs`/`SettingsPrefs` already apply on every write, so a value from
a future, more permissive version degrades to the nearest valid one instead
of failing the import.
- An alarm's own `snooze_minutes`, `snooze_limit` and `volume_ramp_seconds`
are clamped at parse time to the same ranges as the defaults they
override, and a timer's `duration_millis` to the setup range (1 second to
24 hours). Room has no clamp of its own, so this is where it happens.
- A missing `timer_presets` keeps the built-in presets. An empty list is a
user who removed them all, and is imported as such.
- An enum field with a name this version does not recognise (a `dismiss_
challenge` or `theme_mode` from a future version, say) degrades to that
field's default, the same forgiving-read posture `AlarmMapper` already uses