fix(alarms): harden native alarm lifecycle
This commit is contained in:
@@ -222,6 +222,7 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
fun onAlarmFired(id: String) {
|
fun onAlarmFired(id: String) {
|
||||||
val spec = readSpec(id) ?: return
|
val spec = readSpec(id) ?: return
|
||||||
val firedAt = spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
val firedAt = spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
||||||
|
saveHandledOccurrence(id, firedAt)
|
||||||
val next = spec.copy(
|
val next = spec.copy(
|
||||||
snoozeUntilMillis = null,
|
snoozeUntilMillis = null,
|
||||||
snoozeOriginMillis = null,
|
snoozeOriginMillis = null,
|
||||||
@@ -288,6 +289,7 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
fun cancelAlarm(id: String) {
|
fun cancelAlarm(id: String) {
|
||||||
Log.d(tag, "alarm.cancel id=$id")
|
Log.d(tag, "alarm.cancel id=$id")
|
||||||
removeScheduledAlarm(id)
|
removeScheduledAlarm(id)
|
||||||
|
removeHandledOccurrence(id)
|
||||||
cancelPending("fire", pendingFireIntent(id, PendingIntent.FLAG_NO_CREATE))
|
cancelPending("fire", pendingFireIntent(id, PendingIntent.FLAG_NO_CREATE))
|
||||||
cancelPending("show", pendingShowIntent(id, PendingIntent.FLAG_NO_CREATE))
|
cancelPending("show", pendingShowIntent(id, PendingIntent.FLAG_NO_CREATE))
|
||||||
cancelPending("preNotice", pendingPreNoticeIntent(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 =
|
fun pendingAlarmCount(): Int =
|
||||||
prefs().getStringSet(KEY_IDS, emptySet()).orEmpty().size
|
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(
|
private fun preserveNativeSnooze(
|
||||||
existing: NativeAlarmSpec?,
|
existing: NativeAlarmSpec?,
|
||||||
requestedTriggerAtMillis: Long,
|
requestedTriggerAtMillis: Long,
|
||||||
@@ -438,6 +452,24 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
.apply()
|
.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() =
|
private fun prefs() =
|
||||||
appContext.createDeviceProtectedStorageContext()
|
appContext.createDeviceProtectedStorageContext()
|
||||||
.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
.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 PREFS = "pluriwave_alarm_scheduler"
|
||||||
private const val KEY_IDS = "scheduled_alarm_ids"
|
private const val KEY_IDS = "scheduled_alarm_ids"
|
||||||
private const val KEY_ALARM_PREFIX = "scheduled_alarm_"
|
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 PRE_NOTICE_MILLIS = 30 * 60 * 1000L
|
||||||
private const val SCHEDULE_UNICA = "unica"
|
private const val SCHEDULE_UNICA = "unica"
|
||||||
private const val SCHEDULE_DIAS_SEMANA = "diasSemana"
|
private const val SCHEDULE_DIAS_SEMANA = "diasSemana"
|
||||||
|
|||||||
@@ -178,6 +178,10 @@ class MainActivity : AudioServiceActivity() {
|
|||||||
result.success(payload)
|
result.success(payload)
|
||||||
intent?.removeExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ACTION)
|
intent?.removeExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ACTION)
|
||||||
}
|
}
|
||||||
|
"getHandledAlarmOccurrences" -> {
|
||||||
|
Log.d(tag, "alarm.channel getHandledAlarmOccurrences")
|
||||||
|
result.success(alarmScheduler.handledOccurrences())
|
||||||
|
}
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,11 +112,13 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
.setContentText(title)
|
.setContentText(title)
|
||||||
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
||||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setAutoCancel(false)
|
.setAutoCancel(false)
|
||||||
.setContentIntent(fullScreenIntent)
|
.setContentIntent(fullScreenIntent)
|
||||||
.setFullScreenIntent(fullScreenIntent, true)
|
.setFullScreenIntent(fullScreenIntent, true)
|
||||||
.addAction(0, "Posponer $snoozeMinutes min", snoozePendingIntent(context, alarmId, snoozeMinutes))
|
.addAction(0, "Posponer $snoozeMinutes min", snoozePendingIntent(context, alarmId, snoozeMinutes))
|
||||||
|
.addAction(0, "Detener", stopPendingIntent(context, alarmId))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -250,6 +252,17 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
|||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
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 {
|
companion object {
|
||||||
const val TAG = "PluriWave"
|
const val TAG = "PluriWave"
|
||||||
const val CHANNEL_ID = "pluriwave_alarm_pre_notice"
|
const val CHANNEL_ID = "pluriwave_alarm_pre_notice"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
@@ -119,7 +120,11 @@ class PluriWaveAlarmService : Service() {
|
|||||||
setAudioAttributes(alarmAudioAttributes())
|
setAudioAttributes(alarmAudioAttributes())
|
||||||
isLooping = false
|
isLooping = false
|
||||||
setVolume(volume, volume)
|
setVolume(volume, volume)
|
||||||
setDataSource(stationUrl)
|
setDataSource(
|
||||||
|
this@PluriWaveAlarmService,
|
||||||
|
Uri.parse(stationUrl),
|
||||||
|
mapOf("User-Agent" to "PluriWave/0.1.0 (native alarm)")
|
||||||
|
)
|
||||||
setOnPreparedListener {
|
setOnPreparedListener {
|
||||||
if (activeAlarmId != alarmId) return@setOnPreparedListener
|
if (activeAlarmId != alarmId) return@setOnPreparedListener
|
||||||
cancelStationFallback()
|
cancelStationFallback()
|
||||||
@@ -256,6 +261,7 @@ class PluriWaveAlarmService : Service() {
|
|||||||
)
|
)
|
||||||
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
||||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setAutoCancel(false)
|
.setAutoCancel(false)
|
||||||
.setFullScreenIntent(openAlarmPendingIntent(alarmId, title, snoozeMinutes), true)
|
.setFullScreenIntent(openAlarmPendingIntent(alarmId, title, snoozeMinutes), true)
|
||||||
@@ -367,7 +373,7 @@ class PluriWaveAlarmService : Service() {
|
|||||||
private const val TAG = "PluriWave"
|
private const val TAG = "PluriWave"
|
||||||
private const val CHANNEL_ID = "pluriwave_alarm_native"
|
private const val CHANNEL_ID = "pluriwave_alarm_native"
|
||||||
private const val NOTIFICATION_ID = 92841
|
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 ACTION_SNOOZE = "es.freetimelab.pluriwave.alarm.SNOOZE_NATIVE"
|
||||||
const val EXTRA_SNOOZE_MINUTES = "snoozeMinutes"
|
const val EXTRA_SNOOZE_MINUTES = "snoozeMinutes"
|
||||||
private const val STATION_START_TIMEOUT_MILLIS = 15_000L
|
private const val STATION_START_TIMEOUT_MILLIS = 15_000L
|
||||||
@@ -392,10 +398,15 @@ class PluriWaveAlarmService : Service() {
|
|||||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, alarmId)
|
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, alarmId)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
context.stopService(intent)
|
context.startService(intent)
|
||||||
Log.d(TAG, "alarm.service stop requested id=$alarmId")
|
Log.d(TAG, "alarm.service stop action requested id=$alarmId")
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
Log.e(TAG, "alarm.service stop request failed id=$alarmId", error)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class EstadoAlarmas extends ChangeNotifier {
|
|||||||
_error = null;
|
_error = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
|
await _sincronizarEjecucionesGestionadasPorAndroid();
|
||||||
final config = await servicio.recalcularTodas();
|
final config = await servicio.recalcularTodas();
|
||||||
_aplicar(config);
|
_aplicar(config);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -83,6 +84,7 @@ class EstadoAlarmas extends ChangeNotifier {
|
|||||||
_aplicar(config);
|
_aplicar(config);
|
||||||
try {
|
try {
|
||||||
final guardada = _alarmas.firstWhere((a) => a.id == alarma.id);
|
final guardada = _alarmas.firstWhere((a) => a.id == alarma.id);
|
||||||
|
await _solicitarPermisosNecesariosParaAlarma();
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[PluriWave][alarmas] guardada id=${guardada.id} proxima=${guardada.proximaEjecucion?.toIso8601String()}',
|
'[PluriWave][alarmas] guardada id=${guardada.id} proxima=${guardada.proximaEjecucion?.toIso8601String()}',
|
||||||
);
|
);
|
||||||
@@ -246,10 +248,48 @@ class EstadoAlarmas extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _sincronizarEjecucionesGestionadasPorAndroid() async {
|
||||||
|
try {
|
||||||
|
final ejecuciones = await android.obtenerEjecucionesNativasGestionadas();
|
||||||
|
if (ejecuciones.isEmpty) return;
|
||||||
|
final config = await servicio.sincronizarEjecucionesNativas({
|
||||||
|
for (final ejecucion in ejecuciones)
|
||||||
|
ejecucion.alarmaId: ejecucion.gestionadaEn,
|
||||||
|
});
|
||||||
|
_aplicar(config);
|
||||||
|
debugPrint(
|
||||||
|
'[PluriWave][alarmas] sincronizadas ejecuciones nativas count=${ejecuciones.length}',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[PluriWave][alarmas] sincronizar nativas ERROR $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _solicitarPermisosNecesariosParaAlarma() async {
|
||||||
|
try {
|
||||||
|
final diag = await android.diagnostico();
|
||||||
|
_diagnostico = diag;
|
||||||
|
if (!diag.puedeProgramarExactas) {
|
||||||
|
await android.solicitarPermisoAlarmasExactas();
|
||||||
|
}
|
||||||
|
if (!diag.notificacionesPermitidas) {
|
||||||
|
await android.solicitarPermisoNotificaciones();
|
||||||
|
}
|
||||||
|
if (!diag.puedeUsarPantallaCompleta) {
|
||||||
|
await android.solicitarPermisoPantallaCompleta();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[PluriWave][alarmas] permisos android ERROR $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _sincronizarTodas() async {
|
Future<void> _sincronizarTodas() async {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'[PluriWave][alarmas] sincronizar todas count=${_alarmas.length}',
|
'[PluriWave][alarmas] sincronizar todas count=${_alarmas.length}',
|
||||||
);
|
);
|
||||||
|
if (_alarmas.any((alarma) => alarma.activa)) {
|
||||||
|
await _solicitarPermisosNecesariosParaAlarma();
|
||||||
|
}
|
||||||
for (final alarma in _alarmas) {
|
for (final alarma in _alarmas) {
|
||||||
await android.programar(alarma);
|
await android.programar(alarma);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,61 @@ class ServicioAlarmas {
|
|||||||
return nuevo;
|
return nuevo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ConfiguracionAlarmas> sincronizarEjecucionesNativas(
|
||||||
|
Map<String, DateTime> ejecuciones,
|
||||||
|
) async {
|
||||||
|
if (ejecuciones.isEmpty) return cargar();
|
||||||
|
|
||||||
|
final config = await cargar();
|
||||||
|
final ahora = _reloj();
|
||||||
|
var huboCambios = false;
|
||||||
|
final alarmas =
|
||||||
|
config.alarmas.map((alarma) {
|
||||||
|
final gestionadaEn = ejecuciones[alarma.id];
|
||||||
|
if (gestionadaEn == null) return alarma;
|
||||||
|
final ultima = alarma.ultimaEjecucionGestionada;
|
||||||
|
if (ultima != null && !gestionadaEn.isAfter(ultima)) return alarma;
|
||||||
|
|
||||||
|
final proxima = alarma.proximaProgramable;
|
||||||
|
if (proxima != null &&
|
||||||
|
proxima.isAfter(
|
||||||
|
gestionadaEn.add(
|
||||||
|
ServicioProgramacionAlarmas.toleranciaDisparoInminente,
|
||||||
|
),
|
||||||
|
)) {
|
||||||
|
return alarma;
|
||||||
|
}
|
||||||
|
|
||||||
|
final siguiente = _programacion.calcularSiguienteDespuesDeEjecucion(
|
||||||
|
alarma: alarma,
|
||||||
|
ejecucion: gestionadaEn,
|
||||||
|
vacaciones: config.vacaciones,
|
||||||
|
excepciones: config.excepciones,
|
||||||
|
);
|
||||||
|
huboCambios = true;
|
||||||
|
return alarma.copyWith(
|
||||||
|
activa:
|
||||||
|
alarma.tipoProgramacion == TipoProgramacionAlarma.unica
|
||||||
|
? false
|
||||||
|
: alarma.activa,
|
||||||
|
proximaEjecucion: siguiente,
|
||||||
|
limpiarProximaEjecucion: true,
|
||||||
|
limpiarSnooze: true,
|
||||||
|
ultimaEjecucionGestionada: gestionadaEn,
|
||||||
|
actualizadaEn: ahora,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
if (!huboCambios) return config;
|
||||||
|
final nuevo = ConfiguracionAlarmas(
|
||||||
|
alarmas: alarmas,
|
||||||
|
vacaciones: config.vacaciones,
|
||||||
|
excepciones: config.excepciones,
|
||||||
|
);
|
||||||
|
await _guardar(nuevo);
|
||||||
|
return nuevo;
|
||||||
|
}
|
||||||
|
|
||||||
Future<ConfiguracionAlarmas> saltarProxima(String alarmaId) async {
|
Future<ConfiguracionAlarmas> saltarProxima(String alarmaId) async {
|
||||||
final config = await cargar();
|
final config = await cargar();
|
||||||
final alarma = config.alarmas.firstWhere((a) => a.id == alarmaId);
|
final alarma = config.alarmas.firstWhere((a) => a.id == alarmaId);
|
||||||
|
|||||||
@@ -68,6 +68,25 @@ class DiagnosticoAlarmasAndroid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EjecucionAlarmaNativa {
|
||||||
|
const EjecucionAlarmaNativa({
|
||||||
|
required this.alarmaId,
|
||||||
|
required this.gestionadaEn,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String alarmaId;
|
||||||
|
final DateTime gestionadaEn;
|
||||||
|
|
||||||
|
factory EjecucionAlarmaNativa.fromMap(Map<Object?, Object?> map) {
|
||||||
|
return EjecucionAlarmaNativa(
|
||||||
|
alarmaId: map['alarmId'] as String? ?? '',
|
||||||
|
gestionadaEn: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
(map['handledAtMillis'] as num?)?.toInt() ?? 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract class PuertoAlarmasAndroid {
|
abstract class PuertoAlarmasAndroid {
|
||||||
Stream<EventoAlarmaAndroid> get eventosAlarma;
|
Stream<EventoAlarmaAndroid> get eventosAlarma;
|
||||||
|
|
||||||
@@ -81,6 +100,7 @@ abstract class PuertoAlarmasAndroid {
|
|||||||
Future<void> confirmarAudioFlutter(String alarmaId);
|
Future<void> confirmarAudioFlutter(String alarmaId);
|
||||||
Future<DiagnosticoAlarmasAndroid> diagnostico();
|
Future<DiagnosticoAlarmasAndroid> diagnostico();
|
||||||
Future<EventoAlarmaAndroid?> obtenerEventoInicial();
|
Future<EventoAlarmaAndroid?> obtenerEventoInicial();
|
||||||
|
Future<List<EjecucionAlarmaNativa>> obtenerEjecucionesNativasGestionadas();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||||
@@ -208,6 +228,24 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
|||||||
return evento.alarmaId.isEmpty ? null : evento;
|
return evento.alarmaId.isEmpty ? null : evento;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<EjecucionAlarmaNativa>>
|
||||||
|
obtenerEjecucionesNativasGestionadas() async {
|
||||||
|
final raw = await _channel.invokeMethod<List<Object?>>(
|
||||||
|
'getHandledAlarmOccurrences',
|
||||||
|
);
|
||||||
|
if (raw == null || raw.isEmpty) return const [];
|
||||||
|
return raw
|
||||||
|
.whereType<Map<Object?, Object?>>()
|
||||||
|
.map(EjecucionAlarmaNativa.fromMap)
|
||||||
|
.where(
|
||||||
|
(evento) =>
|
||||||
|
evento.alarmaId.isNotEmpty &&
|
||||||
|
evento.gestionadaEn.millisecondsSinceEpoch > 0,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _logAndInvokeVoid(String method, Map<String, Object?> args) {
|
Future<void> _logAndInvokeVoid(String method, Map<String, Object?> args) {
|
||||||
debugPrint('[PluriWave][alarmas] $method $args');
|
debugPrint('[PluriWave][alarmas] $method $args');
|
||||||
return _channel.invokeMethod<void>(method, args);
|
return _channel.invokeMethod<void>(method, args);
|
||||||
|
|||||||
@@ -143,6 +143,56 @@ void main() {
|
|||||||
expect(estado.alarmas.single.activa, isFalse);
|
expect(estado.alarmas.single.activa, isFalse);
|
||||||
expect(estado.alarmas.single.proximaEjecucion, isNull);
|
expect(estado.alarmas.single.proximaEjecucion, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'inicializar sincroniza ejecucion nativa y evita reprogramar al instante',
|
||||||
|
() async {
|
||||||
|
final android = FakePuertoAlarmasAndroid()
|
||||||
|
..ejecucionesNativas.add(
|
||||||
|
EjecucionAlarmaNativa(
|
||||||
|
alarmaId: 'native1',
|
||||||
|
gestionadaEn: DateTime(2026, 5, 25, 7, 30),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final servicio = ServicioAlarmas(
|
||||||
|
reloj: () => DateTime(2026, 5, 25, 7, 30, 20),
|
||||||
|
);
|
||||||
|
await servicio.guardarAlarma(
|
||||||
|
AlarmaMusical(
|
||||||
|
id: 'native1',
|
||||||
|
nombre: 'Nativa',
|
||||||
|
hora: 7,
|
||||||
|
minuto: 30,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: const [],
|
||||||
|
proximaEjecucion: DateTime(2026, 5, 25, 7, 30),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final estado = EstadoAlarmas(
|
||||||
|
servicio: servicio,
|
||||||
|
android: android,
|
||||||
|
iniciarAutomaticamente: false,
|
||||||
|
);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
addTearDown(android.dispose);
|
||||||
|
|
||||||
|
await estado.inicializar();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
estado.alarmas.single.ultimaEjecucionGestionada,
|
||||||
|
DateTime(2026, 5, 25, 7, 30),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
estado.alarmas.single.proximaEjecucion,
|
||||||
|
DateTime(2026, 5, 26, 7, 30),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
android.programadas.last.proximaProgramable,
|
||||||
|
DateTime(2026, 5, 26, 7, 30),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||||
@@ -150,6 +200,7 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
|||||||
final canceladas = <String>[];
|
final canceladas = <String>[];
|
||||||
final detenidas = <String>[];
|
final detenidas = <String>[];
|
||||||
final ocultadas = <String>[];
|
final ocultadas = <String>[];
|
||||||
|
final ejecucionesNativas = <EjecucionAlarmaNativa>[];
|
||||||
final _eventos = StreamController<EventoAlarmaAndroid>.broadcast();
|
final _eventos = StreamController<EventoAlarmaAndroid>.broadcast();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -195,6 +246,10 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
|||||||
@override
|
@override
|
||||||
Future<EventoAlarmaAndroid?> obtenerEventoInicial() async => null;
|
Future<EventoAlarmaAndroid?> obtenerEventoInicial() async => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<EjecucionAlarmaNativa>>
|
||||||
|
obtenerEjecucionesNativasGestionadas() async => ejecucionesNativas;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> solicitarPermisoAlarmasExactas() async => true;
|
Future<bool> solicitarPermisoAlarmasExactas() async => true;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user