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.
This commit is contained in:
makiolaj committed 2026-10-21 17:25:39 +02:00
1 parent b883554dc4
commit 96fc9c19cf
4 files changed
+34 -1

No files matched your search

@@ -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 {
@@ -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.
@@ -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)
}
}
}
}
@@ -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 {