From 058fc27749e014c77127d81dcfd9e9b83f0b658f Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 21 Oct 2026 19:33:26 +0200 Subject: [PATCH] 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 --- .../clockula/data/prefs/SettingsPrefs.kt | 5 ++++- .../clockula/timer/service/TimerService.kt | 12 ++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) 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 c1f81da..b4eea11 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 @@ -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 diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/timer/service/TimerService.kt b/app/src/main/java/de/jeanlucmakiola/clockula/timer/service/TimerService.kt index e4226bf..8722726 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/timer/service/TimerService.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/timer/service/TimerService.kt @@ -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() } }