From 96fc9c19cfeefb5eda3371d1c6b673f8e255fd50 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 21 Oct 2026 17:23:50 +0200 Subject: [PATCH] fix: keep the device awake until an alarm or timer starts ringing The fire broadcast's wake lock ended with the receiver, and the ring services only took their own after a database read, so a dozing device could suspend in between and ring late. Both receivers now hold a short timed wake lock across the hand-off, and the alarm service takes its own before the read. --- .../alarm/receiver/AlarmFireReceiver.kt | 2 ++ .../clockula/alarm/ring/AlarmRingService.kt | 3 +- .../clockula/ring/HandoffWakeLock.kt | 28 +++++++++++++++++++ .../timer/receiver/TimerExpiryReceiver.kt | 2 ++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/clockula/ring/HandoffWakeLock.kt diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/AlarmFireReceiver.kt b/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/AlarmFireReceiver.kt index 0ea4ccb..176d0c6 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/AlarmFireReceiver.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/AlarmFireReceiver.kt @@ -5,6 +5,7 @@ import android.content.Intent import dagger.hilt.android.AndroidEntryPoint import de.jeanlucmakiola.clockula.alarm.AlarmEngine import de.jeanlucmakiola.clockula.alarm.AlarmIntents +import de.jeanlucmakiola.clockula.ring.HandoffWakeLock import de.jeanlucmakiola.floret.di.ApplicationScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -32,6 +33,7 @@ class AlarmFireReceiver : HiltBroadcastReceiver() { val alarmId = intent.getLongExtra(AlarmIntents.EXTRA_ALARM_ID, 0L) val action = intent.action ?: return + if (action == AlarmIntents.ACTION_FIRE) HandoffWakeLock.acquire(context, "clockula:alarm-fire") val pendingResult = goAsync() scope.launch { try { diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/alarm/ring/AlarmRingService.kt b/app/src/main/java/de/jeanlucmakiola/clockula/alarm/ring/AlarmRingService.kt index d37e5e6..9cccee9 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/alarm/ring/AlarmRingService.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/alarm/ring/AlarmRingService.kt @@ -67,6 +67,8 @@ class AlarmRingService : Service() { // foreground service a few seconds to post its notification, and a // database read is not something to spend them on. startForegroundPreparing() + // Also before the read: the fire broadcast's own wake lock is gone by now. + acquireWakeLock() ringing = scope.launch { val session = engine.ringSession() if (session == null) { @@ -89,7 +91,6 @@ class AlarmRingService : Service() { private fun ring(session: RingSession) { startForeground(session) - acquireWakeLock() // Vibration first: the sound may take seconds to open when another app // plays it, and the ring must not be still until then. diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ring/HandoffWakeLock.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ring/HandoffWakeLock.kt new file mode 100644 index 0000000..697e64f --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ring/HandoffWakeLock.kt @@ -0,0 +1,28 @@ +package de.jeanlucmakiola.clockula.ring + +import android.content.Context +import android.os.PowerManager + +/** + * Keeps the CPU awake across the gap between an AlarmManager broadcast and the + * service it starts. The platform's own wake lock ends with the receiver, and + * the service takes its own only after a database read, so without this a + * dozing device can suspend in between and ring minutes late. + * + * Timed rather than released: the service holds its own lock well before the + * timeout, and a lock that outlives a crashed hand-off cannot leak. + */ +object HandoffWakeLock { + + private const val TIMEOUT_MILLIS = 30_000L + + fun acquire(context: Context, tag: String) { + val power = context.getSystemService(PowerManager::class.java) ?: return + runCatching { + power.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag).apply { + setReferenceCounted(false) + acquire(TIMEOUT_MILLIS) + } + } + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/timer/receiver/TimerExpiryReceiver.kt b/app/src/main/java/de/jeanlucmakiola/clockula/timer/receiver/TimerExpiryReceiver.kt index 77ac9b3..497884d 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/timer/receiver/TimerExpiryReceiver.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/timer/receiver/TimerExpiryReceiver.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import dagger.hilt.android.AndroidEntryPoint import de.jeanlucmakiola.clockula.alarm.receiver.HiltBroadcastReceiver +import de.jeanlucmakiola.clockula.ring.HandoffWakeLock import de.jeanlucmakiola.clockula.timer.TimerEngine import de.jeanlucmakiola.floret.di.ApplicationScope import kotlinx.coroutines.CoroutineScope @@ -28,6 +29,7 @@ class TimerExpiryReceiver : HiltBroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { super.onReceive(context, intent) + HandoffWakeLock.acquire(context, "clockula:timer-expiry") val pendingResult = goAsync() scope.launch { try {