feat(alarm): the receivers — fire, boot, time set, zone and replace
All five are thin: inject, goAsync, make one call into the engine, finish in a finally. Anything resembling a decision belongs in the engine where it can be tested without a device. TIME_SET and TIMEZONE_CHANGED both mean the same thing to the engine — every resolved instant is now suspect — so both re-resolve from scratch rather than trying to patch the registrations they already made. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
package de.jeanlucmakiola.clockula.alarm.receiver
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import de.jeanlucmakiola.clockula.alarm.AlarmEngine
|
||||||
|
import de.jeanlucmakiola.clockula.alarm.AlarmIntents
|
||||||
|
import de.jeanlucmakiola.floret.di.ApplicationScope
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/** The notification's Snooze and Dismiss buttons. */
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class AlarmActionReceiver : HiltBroadcastReceiver() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var engine: AlarmEngine
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var scope: CoroutineScope
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
|
||||||
|
val alarmId = intent.getLongExtra(AlarmIntents.EXTRA_ALARM_ID, 0L)
|
||||||
|
val action = intent.action ?: return
|
||||||
|
val pendingResult = goAsync()
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
when (action) {
|
||||||
|
AlarmIntents.ACTION_SNOOZE -> engine.snooze(alarmId)
|
||||||
|
AlarmIntents.ACTION_DISMISS -> engine.dismiss(alarmId)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pendingResult.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package de.jeanlucmakiola.clockula.alarm.receiver
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import de.jeanlucmakiola.clockula.alarm.AlarmEngine
|
||||||
|
import de.jeanlucmakiola.clockula.alarm.AlarmIntents
|
||||||
|
import de.jeanlucmakiola.floret.di.ApplicationScope
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The two AlarmManager slots arriving. Thin by design: every decision the engine
|
||||||
|
* could make is the engine's, so the receiver only unpacks and dispatches.
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class AlarmFireReceiver : HiltBroadcastReceiver() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var engine: AlarmEngine
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var scope: CoroutineScope
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
// Hilt's injection happens in super.onReceive(); nothing above may touch
|
||||||
|
// an injected field.
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
|
||||||
|
val alarmId = intent.getLongExtra(AlarmIntents.EXTRA_ALARM_ID, 0L)
|
||||||
|
val action = intent.action ?: return
|
||||||
|
val pendingResult = goAsync()
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
when (action) {
|
||||||
|
AlarmIntents.ACTION_FIRE -> engine.onFire(
|
||||||
|
alarmId = alarmId,
|
||||||
|
// The instant the PendingIntent carried, not `now`: a fire
|
||||||
|
// delivered late must still name the occurrence it is for.
|
||||||
|
scheduledFor = Instant.fromEpochMilliseconds(
|
||||||
|
intent.getLongExtra(AlarmIntents.EXTRA_FIRE_AT, 0L),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
AlarmIntents.ACTION_AUTO_SILENCE -> engine.onAutoSilence(alarmId)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pendingResult.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package de.jeanlucmakiola.clockula.alarm.receiver
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `BroadcastReceiver.onReceive` is abstract, so a Kotlin subclass cannot call
|
||||||
|
* `super.onReceive(...)` — and that call is exactly where Hilt injects. One
|
||||||
|
* concrete no-op base fixes it: the Hilt plugin rewrites each receiver's
|
||||||
|
* superclass to its generated `Hilt_…` class, whose `onReceive` does the
|
||||||
|
* injection, and the `super` call above resolves there at runtime.
|
||||||
|
*/
|
||||||
|
abstract class HiltBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) = Unit
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package de.jeanlucmakiola.clockula.alarm.receiver
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import de.jeanlucmakiola.clockula.alarm.AlarmEngine
|
||||||
|
import de.jeanlucmakiola.floret.di.ApplicationScope
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Everything that invalidates a registration: a reboot clears AlarmManager, an
|
||||||
|
* app update clears it too, and a clock or zone change moves what "07:00" means.
|
||||||
|
* All four are answered the same way — re-resolve from now.
|
||||||
|
*
|
||||||
|
* `LOCKED_BOOT_COMPLETED` is deliberately not handled: the database and
|
||||||
|
* DataStore live in credential-encrypted storage, unreadable before first unlock.
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class SystemEventReceiver : HiltBroadcastReceiver() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var engine: AlarmEngine
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ApplicationScope
|
||||||
|
lateinit var scope: CoroutineScope
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
|
||||||
|
val action = intent.action ?: return
|
||||||
|
val pendingResult = goAsync()
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
when (action) {
|
||||||
|
Intent.ACTION_BOOT_COMPLETED -> engine.onBootCompleted()
|
||||||
|
|
||||||
|
Intent.ACTION_MY_PACKAGE_REPLACED -> engine.onPackageReplaced()
|
||||||
|
|
||||||
|
Intent.ACTION_TIME_CHANGED,
|
||||||
|
Intent.ACTION_TIMEZONE_CHANGED,
|
||||||
|
-> engine.onSystemTimeOrZoneChanged()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pendingResult.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user