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
+55
View File
@@ -171,6 +171,61 @@ class ServicioAlarmas {
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 {
final config = await cargar();
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 {
Stream<EventoAlarmaAndroid> get eventosAlarma;
@@ -81,6 +100,7 @@ abstract class PuertoAlarmasAndroid {
Future<void> confirmarAudioFlutter(String alarmaId);
Future<DiagnosticoAlarmasAndroid> diagnostico();
Future<EventoAlarmaAndroid?> obtenerEventoInicial();
Future<List<EjecucionAlarmaNativa>> obtenerEjecucionesNativasGestionadas();
}
class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
@@ -208,6 +228,24 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
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) {
debugPrint('[PluriWave][alarmas] $method $args');
return _channel.invokeMethod<void>(method, args);