diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/AlarmActionReceiver.kt b/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/AlarmActionReceiver.kt new file mode 100644 index 0000000..f7b9b99 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/AlarmActionReceiver.kt @@ -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() + } + } + } +} 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 new file mode 100644 index 0000000..0ea4ccb --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/AlarmFireReceiver.kt @@ -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() + } + } + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/HiltBroadcastReceiver.kt b/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/HiltBroadcastReceiver.kt new file mode 100644 index 0000000..7db4464 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/HiltBroadcastReceiver.kt @@ -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 +} diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/SystemEventReceiver.kt b/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/SystemEventReceiver.kt new file mode 100644 index 0000000..594f6f4 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/clockula/alarm/receiver/SystemEventReceiver.kt @@ -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() + } + } + } +}