fix: clamp an alarm's own snooze and ramp values when they are read
Backup import clamped the app-wide defaults but passed an alarm's own snooze length, snooze limit and volume ramp through unchanged, so a snooze of 0 re-rang at once and a negative limit turned snoozing off. Both import and the database read now clamp them to the ranges the pickers offer, and imported timer lengths to the setup range.
This commit is contained in:
1 parent
96fc9c19cf
commit
99b357f4fb
6 files changed
+54
-11
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Int> = intPref("default_snooze_minutes", 10).clampedTo(1..60)
|
||||
val snoozeMinutes: Pref<Int> = intPref("default_snooze_minutes", 10).clampedTo(ClockLimits.SNOOZE_MINUTES)
|
||||
|
||||
val snoozeLimit: Pref<Int> = intPref("default_snooze_limit", 3).clampedTo(0..10)
|
||||
val snoozeLimit: Pref<Int> = intPref("default_snooze_limit", 3).clampedTo(ClockLimits.SNOOZE_LIMIT)
|
||||
|
||||
val vibrate: Pref<Boolean> = booleanPref("default_vibrate", true)
|
||||
|
||||
val volumeRampSeconds: Pref<Int> = intPref("default_volume_ramp_seconds", 15).clampedTo(0..60)
|
||||
val volumeRampSeconds: Pref<Int> = intPref("default_volume_ramp_seconds", 15).clampedTo(ClockLimits.VOLUME_RAMP_SECONDS)
|
||||
|
||||
val alarmRingtoneUri: Pref<String?> = nullableStringPref("default_alarm_ringtone_uri").blankAsAbsent()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<String> = listOf("content://", "android.resource://")
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user