fix(alarms): harden native alarm lifecycle
Build & Deploy PluriWave / Build APK + AAB release (push) Has been cancelled
Build & Deploy PluriWave / Análisis de código (push) Has been cancelled

This commit is contained in:
Javier Bautista Fernández
2026-05-29 13:13:39 +02:00
parent 8f6124fc1a
commit 028e2d69b1
8 changed files with 254 additions and 4 deletions
@@ -222,6 +222,7 @@ class AlarmScheduler(private val context: Context) {
fun onAlarmFired(id: String) {
val spec = readSpec(id) ?: return
val firedAt = spec.snoozeOriginMillis ?: spec.triggerAtMillis
saveHandledOccurrence(id, firedAt)
val next = spec.copy(
snoozeUntilMillis = null,
snoozeOriginMillis = null,
@@ -288,6 +289,7 @@ class AlarmScheduler(private val context: Context) {
fun cancelAlarm(id: String) {
Log.d(tag, "alarm.cancel id=$id")
removeScheduledAlarm(id)
removeHandledOccurrence(id)
cancelPending("fire", pendingFireIntent(id, PendingIntent.FLAG_NO_CREATE))
cancelPending("show", pendingShowIntent(id, PendingIntent.FLAG_NO_CREATE))
cancelPending("preNotice", pendingPreNoticeIntent(id, PendingIntent.FLAG_NO_CREATE))
@@ -325,6 +327,18 @@ class AlarmScheduler(private val context: Context) {
fun pendingAlarmCount(): Int =
prefs().getStringSet(KEY_IDS, emptySet()).orEmpty().size
fun handledOccurrences(): List<Map<String, Any>> =
prefs().getStringSet(KEY_HANDLED_IDS, emptySet()).orEmpty()
.mapNotNull { id ->
val handledAt = prefs().getLong("$KEY_HANDLED_PREFIX$id", 0L)
.takeIf { it > 0L }
?: return@mapNotNull null
mapOf(
"alarmId" to id,
"handledAtMillis" to handledAt
)
}
private fun preserveNativeSnooze(
existing: NativeAlarmSpec?,
requestedTriggerAtMillis: Long,
@@ -438,6 +452,24 @@ class AlarmScheduler(private val context: Context) {
.apply()
}
private fun saveHandledOccurrence(id: String, handledAtMillis: Long) {
val ids = prefs().getStringSet(KEY_HANDLED_IDS, emptySet()).orEmpty().toMutableSet()
ids.add(id)
prefs().edit()
.putStringSet(KEY_HANDLED_IDS, ids)
.putLong("$KEY_HANDLED_PREFIX$id", handledAtMillis)
.apply()
}
private fun removeHandledOccurrence(id: String) {
val ids = prefs().getStringSet(KEY_HANDLED_IDS, emptySet()).orEmpty().toMutableSet()
ids.remove(id)
prefs().edit()
.putStringSet(KEY_HANDLED_IDS, ids)
.remove("$KEY_HANDLED_PREFIX$id")
.apply()
}
private fun prefs() =
appContext.createDeviceProtectedStorageContext()
.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
@@ -619,6 +651,8 @@ class AlarmScheduler(private val context: Context) {
private const val PREFS = "pluriwave_alarm_scheduler"
private const val KEY_IDS = "scheduled_alarm_ids"
private const val KEY_ALARM_PREFIX = "scheduled_alarm_"
private const val KEY_HANDLED_IDS = "handled_alarm_ids"
private const val KEY_HANDLED_PREFIX = "handled_alarm_"
private const val PRE_NOTICE_MILLIS = 30 * 60 * 1000L
private const val SCHEDULE_UNICA = "unica"
private const val SCHEDULE_DIAS_SEMANA = "diasSemana"
@@ -178,6 +178,10 @@ class MainActivity : AudioServiceActivity() {
result.success(payload)
intent?.removeExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ACTION)
}
"getHandledAlarmOccurrences" -> {
Log.d(tag, "alarm.channel getHandledAlarmOccurrences")
result.success(alarmScheduler.handledOccurrences())
}
else -> result.notImplemented()
}
}
@@ -112,11 +112,13 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
.setContentText(title)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOngoing(true)
.setAutoCancel(false)
.setContentIntent(fullScreenIntent)
.setFullScreenIntent(fullScreenIntent, true)
.addAction(0, "Posponer $snoozeMinutes min", snoozePendingIntent(context, alarmId, snoozeMinutes))
.addAction(0, "Detener", stopPendingIntent(context, alarmId))
.build()
try {
@@ -250,6 +252,17 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
private fun stopPendingIntent(context: Context, alarmId: String): PendingIntent =
PendingIntent.getService(
context,
requestCode(alarmId, 40),
Intent(context, PluriWaveAlarmService::class.java).apply {
action = PluriWaveAlarmService.ACTION_STOP
putExtra(EXTRA_ALARM_ID, alarmId)
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
companion object {
const val TAG = "PluriWave"
const val CHANNEL_ID = "pluriwave_alarm_pre_notice"
@@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.IBinder
@@ -119,7 +120,11 @@ class PluriWaveAlarmService : Service() {
setAudioAttributes(alarmAudioAttributes())
isLooping = false
setVolume(volume, volume)
setDataSource(stationUrl)
setDataSource(
this@PluriWaveAlarmService,
Uri.parse(stationUrl),
mapOf("User-Agent" to "PluriWave/0.1.0 (native alarm)")
)
setOnPreparedListener {
if (activeAlarmId != alarmId) return@setOnPreparedListener
cancelStationFallback()
@@ -256,6 +261,7 @@ class PluriWaveAlarmService : Service() {
)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOngoing(true)
.setAutoCancel(false)
.setFullScreenIntent(openAlarmPendingIntent(alarmId, title, snoozeMinutes), true)
@@ -367,7 +373,7 @@ class PluriWaveAlarmService : Service() {
private const val TAG = "PluriWave"
private const val CHANNEL_ID = "pluriwave_alarm_native"
private const val NOTIFICATION_ID = 92841
private const val ACTION_STOP = "es.freetimelab.pluriwave.alarm.STOP_NATIVE"
const val ACTION_STOP = "es.freetimelab.pluriwave.alarm.STOP_NATIVE"
const val ACTION_SNOOZE = "es.freetimelab.pluriwave.alarm.SNOOZE_NATIVE"
const val EXTRA_SNOOZE_MINUTES = "snoozeMinutes"
private const val STATION_START_TIMEOUT_MILLIS = 15_000L
@@ -392,10 +398,15 @@ class PluriWaveAlarmService : Service() {
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, alarmId)
}
try {
context.stopService(intent)
Log.d(TAG, "alarm.service stop requested id=$alarmId")
context.startService(intent)
Log.d(TAG, "alarm.service stop action requested id=$alarmId")
} catch (error: Throwable) {
Log.e(TAG, "alarm.service stop request failed id=$alarmId", error)
try {
context.stopService(intent)
} catch (fallbackError: Throwable) {
Log.e(TAG, "alarm.service stop fallback failed id=$alarmId", fallbackError)
}
}
}