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
+40
View File
@@ -58,6 +58,7 @@ class EstadoAlarmas extends ChangeNotifier {
_error = null;
notifyListeners();
try {
await _sincronizarEjecucionesGestionadasPorAndroid();
final config = await servicio.recalcularTodas();
_aplicar(config);
debugPrint(
@@ -83,6 +84,7 @@ class EstadoAlarmas extends ChangeNotifier {
_aplicar(config);
try {
final guardada = _alarmas.firstWhere((a) => a.id == alarma.id);
await _solicitarPermisosNecesariosParaAlarma();
debugPrint(
'[PluriWave][alarmas] guardada id=${guardada.id} proxima=${guardada.proximaEjecucion?.toIso8601String()}',
);
@@ -246,10 +248,48 @@ class EstadoAlarmas extends ChangeNotifier {
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 {
debugPrint(
'[PluriWave][alarmas] sincronizar todas count=${_alarmas.length}',
);
if (_alarmas.any((alarma) => alarma.activa)) {
await _solicitarPermisosNecesariosParaAlarma();
}
for (final alarma in _alarmas) {
await android.programar(alarma);
}
+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);