diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapper.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapper.kt index e86e10c..7c03cb5 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapper.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/alarms/AlarmMapper.kt @@ -2,6 +2,7 @@ package de.jeanlucmakiola.clockula.data.alarms import de.jeanlucmakiola.clockula.domain.Alarm import de.jeanlucmakiola.clockula.domain.AlarmDraft +import de.jeanlucmakiola.clockula.domain.ClockLimits import de.jeanlucmakiola.clockula.domain.DismissChallenge import de.jeanlucmakiola.clockula.domain.RepeatDays import de.jeanlucmakiola.clockula.domain.TimeOfDay @@ -19,9 +20,9 @@ object AlarmMapper { skipNextOccurrence = entity.skipNextOccurrence, ringtoneUri = entity.ringtoneUri?.takeIf { it.isNotBlank() }, vibrate = entity.vibrate, - snoozeMinutes = entity.snoozeMinutes, - snoozeLimit = entity.snoozeLimit, - volumeRampSeconds = entity.volumeRampSeconds, + snoozeMinutes = entity.snoozeMinutes?.coerceIn(ClockLimits.SNOOZE_MINUTES), + snoozeLimit = entity.snoozeLimit?.coerceIn(ClockLimits.SNOOZE_LIMIT), + volumeRampSeconds = entity.volumeRampSeconds?.coerceIn(ClockLimits.VOLUME_RAMP_SECONDS), // An absent challenge is an *override that isn't set*, so it stays null; // a stored name the app no longer knows degrades to NONE. dismissChallenge = entity.dismissChallenge?.toEnum(DismissChallenge.NONE), 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 2abb93e..f0b0795 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 @@ -1,6 +1,7 @@ package de.jeanlucmakiola.clockula.data.backup import de.jeanlucmakiola.clockula.domain.ClockDefaults +import de.jeanlucmakiola.clockula.domain.ClockLimits import de.jeanlucmakiola.clockula.domain.DismissChallenge import de.jeanlucmakiola.clockula.domain.RepeatDays import de.jeanlucmakiola.clockula.domain.TimeOfDay @@ -10,6 +11,7 @@ import de.jeanlucmakiola.clockula.domain.backup.BackupContent import de.jeanlucmakiola.clockula.domain.backup.BackupSettings import de.jeanlucmakiola.clockula.domain.backup.BackupTimer import de.jeanlucmakiola.clockula.domain.backup.BackupWorldClock +import de.jeanlucmakiola.clockula.domain.timer.TimerPresets import de.jeanlucmakiola.floret.prefs.ThemeMode import de.jeanlucmakiola.floret.prefs.toEnum import kotlinx.serialization.SerializationException @@ -142,9 +144,11 @@ object BackupCodec { skipNextOccurrence = dto.skipNextOccurrence, ringtoneUri = dto.ringtoneUri?.takeIf { it.isNotBlank() }, vibrate = dto.vibrate, - snoozeMinutes = dto.snoozeMinutes, - snoozeLimit = dto.snoozeLimit, - volumeRampSeconds = dto.volumeRampSeconds, + // The same clamps the defaults pass through: an alarm's own + // override is no freer than the setting it overrides. + snoozeMinutes = dto.snoozeMinutes?.coerceIn(ClockLimits.SNOOZE_MINUTES), + snoozeLimit = dto.snoozeLimit?.coerceIn(ClockLimits.SNOOZE_LIMIT), + volumeRampSeconds = dto.volumeRampSeconds?.coerceIn(ClockLimits.VOLUME_RAMP_SECONDS), dismissChallenge = dto.dismissChallenge?.toEnum(DismissChallenge.NONE), deleteAfterUse = dto.deleteAfterUse, ) @@ -160,7 +164,7 @@ object BackupCodec { } timers += BackupTimer( label = dto.label, - duration = dto.durationMillis.milliseconds, + duration = dto.durationMillis.milliseconds.coerceIn(TimerPresets.MIN, TimerPresets.MAX), ringtoneUri = dto.ringtoneUri?.takeIf { it.isNotBlank() }, sortOrder = dto.sortOrder, deleteAfterUse = dto.deleteAfterUse, 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 f3e6a75..0cb43c1 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 @@ -1,5 +1,6 @@ package de.jeanlucmakiola.clockula.data.prefs +import de.jeanlucmakiola.clockula.domain.ClockLimits import de.jeanlucmakiola.clockula.domain.DismissChallenge import de.jeanlucmakiola.clockula.domain.Zones import de.jeanlucmakiola.floret.prefs.Pref @@ -15,13 +16,13 @@ import de.jeanlucmakiola.floret.prefs.nullableStringPref */ object ClockPrefs { - val snoozeMinutes: Pref = intPref("default_snooze_minutes", 10).clampedTo(1..60) + val snoozeMinutes: Pref = intPref("default_snooze_minutes", 10).clampedTo(ClockLimits.SNOOZE_MINUTES) - val snoozeLimit: Pref = intPref("default_snooze_limit", 3).clampedTo(0..10) + val snoozeLimit: Pref = intPref("default_snooze_limit", 3).clampedTo(ClockLimits.SNOOZE_LIMIT) val vibrate: Pref = booleanPref("default_vibrate", true) - val volumeRampSeconds: Pref = intPref("default_volume_ramp_seconds", 15).clampedTo(0..60) + val volumeRampSeconds: Pref = intPref("default_volume_ramp_seconds", 15).clampedTo(ClockLimits.VOLUME_RAMP_SECONDS) val alarmRingtoneUri: Pref = nullableStringPref("default_alarm_ringtone_uri").blankAsAbsent() 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 8c90bfa..73499b3 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/domain/ClockDefaults.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/domain/ClockDefaults.kt @@ -14,3 +14,14 @@ data class ClockDefaults( val dismissChallenge: DismissChallenge = DismissChallenge.NONE, val homeZoneId: String? = null, ) + +/** + * The ranges a user can choose from, for the defaults and for an alarm's own + * overrides alike. Anything read back from disk or a backup is clamped into + * them, so a value no picker offers never reaches the engine. + */ +object ClockLimits { + val SNOOZE_MINUTES: IntRange = 1..60 + val SNOOZE_LIMIT: IntRange = 0..10 + val VOLUME_RAMP_SECONDS: IntRange = 0..60 +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/domain/interop/InteropLimits.kt b/app/src/main/java/de/jeanlucmakiola/clockula/domain/interop/InteropLimits.kt index 907bc9a..d648199 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/domain/interop/InteropLimits.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/domain/interop/InteropLimits.kt @@ -1,5 +1,7 @@ package de.jeanlucmakiola.clockula.domain.interop +import de.jeanlucmakiola.clockula.domain.ClockLimits + /** The bounds an extra from another process is read against (M9 D5, D19). */ object InteropLimits { const val MAX_LABEL_LENGTH: Int = 256 @@ -12,7 +14,7 @@ object InteropLimits { val TIMER_SECONDS: IntRange = 1..86_400 /** `ClockPrefs`' own clamp: an intent cannot ask for a snooze the user could not choose. */ - val SNOOZE_MINUTES: IntRange = 1..60 + val SNOOZE_MINUTES: IntRange = ClockLimits.SNOOZE_MINUTES val RINGTONE_SCHEMES: List = listOf("content://", "android.resource://") } 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 718aef8..2f3f13b 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 @@ -10,6 +10,7 @@ import de.jeanlucmakiola.clockula.domain.backup.BackupContent import de.jeanlucmakiola.clockula.domain.backup.BackupSettings import de.jeanlucmakiola.clockula.domain.backup.BackupTimer import de.jeanlucmakiola.clockula.domain.backup.BackupWorldClock +import de.jeanlucmakiola.clockula.domain.timer.TimerPresets import de.jeanlucmakiola.floret.prefs.ThemeMode import org.junit.jupiter.api.Test import kotlin.time.Duration.Companion.minutes @@ -233,6 +234,29 @@ class BackupCodecTest { assertThat(content.alarms.single().dismissChallenge).isEqualTo(DismissChallenge.NONE) } + @Test + fun `out-of-range alarm overrides are clamped to what a picker offers`() { + val json = minimalDocument().replace( + "\"alarms\": []", + "\"alarms\": [{\"hour\":7,\"minute\":0,\"snooze_minutes\":0," + + "\"snooze_limit\":-5,\"volume_ramp_seconds\":100000}]", + ) + val alarm = (BackupCodec.decode(json) as BackupResult.Success).content.alarms.single() + assertThat(alarm.snoozeMinutes).isEqualTo(1) + assertThat(alarm.snoozeLimit).isEqualTo(0) + assertThat(alarm.volumeRampSeconds).isEqualTo(60) + } + + @Test + fun `timer durations are clamped to the setup range`() { + val json = minimalDocument().replace( + "\"timers\": []", + "\"timers\": [{\"duration_millis\":0},{\"duration_millis\":${Long.MAX_VALUE}}]", + ) + val timers = (BackupCodec.decode(json) as BackupResult.Success).content.timers + assertThat(timers.map { it.duration }).containsExactly(TimerPresets.MIN, TimerPresets.MAX).inOrder() + } + @Test fun `garbage text fails rather than throwing`() { assertThat(BackupCodec.decode("not json at all")).isInstanceOf(BackupResult.Failure::class.java)