From cc2099c40a45ebee3f10e8d814bf841c0358304e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 21 Oct 2026 19:15:39 +0200 Subject: [PATCH] fix(timers): settle timer vibration in one transaction and announce its row as a switch The settle read the store, then wrote it in a second step; a write in between could leave it deciding on a stale snapshot. It is one edit now, which also skips the write once the value exists. The Vibrate row carries the switch role and its state, since the switch itself is display-only. The changelog says what an updated install gets, not just a fresh one. Refs #22 --- CHANGELOG.md | 8 +++++--- .../de/jeanlucmakiola/clockula/ClockulaApp.kt | 4 +++- .../clockula/data/prefs/SettingsPrefs.kt | 16 ++++++++++++---- .../clockula/ui/settings/TimerDefaultsScreen.kt | 12 +++++++++++- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c887e30..e278004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,9 +38,11 @@ The first release of Clockula: alarms, timers, a stopwatch and a world clock. timer turns the screen on and opens over the lock screen with Stop and +1 min. Timers keep counting through a reboot and aren't affected by changes to the clock. -- Timers have their own vibration switch, off by default, and an optional - gradual volume, both apart from the alarms'. A kitchen timer can just beep - while alarms still vibrate; a silent timer still vibrates. +- Timers have their own vibration switch and an optional gradual volume, both + apart from the alarms'. A kitchen timer can just beep while alarms still + vibrate; a silent timer still vibrates. New installs start with timer + vibration off; an updated install keeps vibrating its timers if it did + before. - **Stopwatch** with laps, the fastest and slowest marked, and Lap and Pause in the notification. It keeps counting while the app is closed. - **World clock.** An analog face for your own time, plus cities from the diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ClockulaApp.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ClockulaApp.kt index 9953f26..5945f12 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ClockulaApp.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ClockulaApp.kt @@ -105,7 +105,9 @@ class ClockulaApp : Application() { } catch (cancelled: CancellationException) { throw cancelled } catch (_: Exception) { - // Unsettled is safe; the next launch tries again. + // Unsettled reads as the old behaviour. The next launch tries again, + // and by then the store is no longer empty, so even a fresh install + // settles on the alarm default: vibrating, the safe side to err on. } } 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 977db31..bed61f8 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 @@ -1,5 +1,7 @@ package de.jeanlucmakiola.clockula.data.prefs +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import de.jeanlucmakiola.clockula.domain.ClockDefaults import de.jeanlucmakiola.clockula.domain.DismissChallenge import de.jeanlucmakiola.clockula.domain.backup.BackupSettings @@ -135,10 +137,16 @@ class SettingsPrefs @Inject constructor(private val store: PrefStore) { * side's fallback keeps the old behaviour, so a failed write costs nothing. */ suspend fun settleTimerVibrate(freshInstall: Boolean) { - val untouched = store.data.first().asMap().isEmpty() - val alarmVibrate = store.get(ClockPrefs.vibrate) - store.update(ClockPrefs.timerVibrate) { stored -> - stored ?: if (untouched && freshInstall) false else alarmVibrate + // One transaction, so nothing written meanwhile can make the snapshot + // it decides on stale. Raw keys for the reads: the kit reads a Pref + // only from a store, not from a transaction's preferences. + store.edit { prefs -> + if (stringPreferencesKey(ClockPrefs.timerVibrate.name) in prefs) return@edit + prefs[ClockPrefs.timerVibrate] = if (prefs.asMap().isEmpty() && freshInstall) { + false + } else { + prefs[booleanPreferencesKey(ClockPrefs.vibrate.name)] ?: ClockPrefs.vibrate.default + } } } 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 be6aca0..2d7c659 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 @@ -10,6 +10,11 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.toggleableState +import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.dp import de.jeanlucmakiola.clockula.R import de.jeanlucmakiola.clockula.ui.alarms.VolumeRampPicker @@ -49,6 +54,11 @@ internal fun TimerDefaultsScreen( // what the ring does with Silent and vibration both off: vibrates anyway. GroupedRow( title = stringResource(R.string.editor_vibrate), + // The switch below is display-only, so the row carries its role and state. + modifier = Modifier.semantics { + role = Role.Switch + toggleableState = ToggleableState(defaults.timerVibrate) + }, summary = if (defaults.timerVibrate) { vibrateText(on = true) } else { @@ -57,7 +67,7 @@ internal fun TimerDefaultsScreen( position = Position.Middle, trailing = { // Display only: the row is the one control, so a screen reader - // stops on it once rather than on the row and then the switch. + // stops once, on a row announced as a switch. Switch(checked = defaults.timerVibrate, onCheckedChange = null) }, onClick = { viewModel.onTimerVibrateChanged(!defaults.timerVibrate) },