fix(timers): apply a timer setting changed mid-ring

The service ignored a decision for the timer already sounding, so switching timer vibration off in Settings while a timer rang kept it buzzing until the ring ended. A decision whose settings changed now restarts the ring, its ramp picking up where it was.

The package-manager check for a fresh install also moved out of the settle's DataStore transaction.

Refs #22
This commit is contained in:
makiolaj committed 2026-10-21 19:33:26 +02:00
1 parent 820317f9d9
commit 058fc27749
2 files changed
+14 -3

No files matched your search

@@ -141,6 +141,9 @@ class SettingsPrefs @Inject constructor(private val store: PrefStore) {
*/
suspend fun settleTimerVibrate(freshInstall: () -> Boolean) {
if (store.get(ClockPrefs.timerVibrate) != null) return
// Asked outside the transaction: a binder call has no business holding
// the store's write lock.
val freshPackage = freshInstall()
// 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.
@@ -149,7 +152,7 @@ class SettingsPrefs @Inject constructor(private val store: PrefStore) {
// Settled meanwhile; a garbled value falls through and is replaced.
if (stored?.toBooleanStrictOrNull() != null) return@edit
val neverRan = stringPreferencesKey(SystemPrefs.lastBootId.name) !in prefs
prefs[ClockPrefs.timerVibrate] = if (neverRan && freshInstall()) {
prefs[ClockPrefs.timerVibrate] = if (neverRan && freshPackage) {
false
} else {
prefs[booleanPreferencesKey(ClockPrefs.vibrate.name)] ?: ClockPrefs.vibrate.default
@@ -18,6 +18,7 @@ import de.jeanlucmakiola.clockula.domain.timer.TimerNotificationState
import de.jeanlucmakiola.clockula.domain.timer.TimerMode
import de.jeanlucmakiola.clockula.domain.timer.TimerRing
import de.jeanlucmakiola.clockula.domain.timer.TimerRingDecision
import de.jeanlucmakiola.clockula.domain.timer.TimerSettings
import de.jeanlucmakiola.clockula.ring.RingAudioPlayer
import de.jeanlucmakiola.clockula.ring.RingVibrator
import de.jeanlucmakiola.clockula.timer.AlarmRingStatus
@@ -93,6 +94,9 @@ class TimerService : Service() {
/** Which timer the audio is currently sounding for, so an unchanged decision does nothing. */
private var sounding: Long? = null
/** What [sounding] rings with, so a settings change mid-ring is applied rather than ignored. */
private var soundingSettings: TimerSettings? = null
/** Owned here, scoped to the ring session it was spent in: the rule itself is pure (D17). */
private var alreadyAlerted: Boolean = false
@@ -228,10 +232,13 @@ class TimerService : Service() {
when (decision) {
is TimerRingDecision.Sound -> {
// An unchanged decision does nothing: restarting the player on
// every emission would stutter the ring.
if (sounding == decision.subjectId) return
// every emission would stutter the ring. A changed setting (the
// user switched timer vibration off mid-ring, say) restarts it,
// the ramp picking up where it was.
if (sounding == decision.subjectId && soundingSettings == decision.settings) return
releaseRing()
sounding = decision.subjectId
soundingSettings = decision.settings
acquireWakeLock()
// Vibration first, and the sound without waiting for it: a sound
// another app plays can take seconds to open, and this runs
@@ -334,6 +341,7 @@ class TimerService : Service() {
/** The whole ring released, not just the audio: an orphaned looping player cannot be stopped. */
private fun releaseRing() {
sounding = null
soundingSettings = null
audio.stop()
vibrator.stop()
wakeLock?.let { if (it.isHeld) runCatching { it.release() } }