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