feat(alarmas): surface the three native scheduling failures in Dart
Completes the bridge the native side already exposed. AlarmScheduler and PluriWaveAlarmService record a pre-notice that could not be armed, a refused foreground-service start, and a per-alarm reschedule that failed after a reboot -- but nothing read them, so all three still ended at logcat. EstadoAlarmas now drains them at startup and turns each into a per-alarm exception, which the card already knows how to mark. An alarm that never reached the OS stops looking identical to one that did. The read is deliberately tolerant: a failure to read is logged and swallowed, never surfaced as an alarm error, so a diagnostics gap cannot masquerade as a scheduling problem.
This commit is contained in:
@@ -378,18 +378,28 @@ class ServicioAlarmas {
|
||||
return nuevo;
|
||||
});
|
||||
|
||||
/// Clears any outstanding failure record for [alarmaId] (a subsequent
|
||||
/// scheduling attempt succeeded). No-op when there is nothing to clear.
|
||||
/// Clears the outstanding failure record for [alarmaId] ONLY when its
|
||||
/// current tipo is [tipo] (a subsequent attempt of THAT SPECIFIC kind
|
||||
/// succeeded). Type-scoped on purpose: a successful main-alarm schedule
|
||||
/// call proves nothing about the pre-notice or foreground-service
|
||||
/// subsystems, so it must never clear a failure recorded for those. No-op
|
||||
/// when there is nothing to clear or the recorded tipo does not match.
|
||||
Future<ConfiguracionAlarmas> limpiarFalloProgramacion(
|
||||
String alarmaId,
|
||||
String tipo,
|
||||
) => _enCola(() async {
|
||||
final config = await _configActual();
|
||||
final sinFallo = _sinFalloPrevio(config.excepciones, alarmaId);
|
||||
if (sinFallo.length == config.excepciones.length) return config;
|
||||
final actual = config.excepciones.where((e) => e.alarmaId == alarmaId);
|
||||
final tieneEseTipo = actual.any((e) => e.tipo == tipo);
|
||||
if (!tieneEseTipo) return config;
|
||||
final excepciones =
|
||||
config.excepciones
|
||||
.where((e) => !(e.alarmaId == alarmaId && e.tipo == tipo))
|
||||
.toList();
|
||||
final nuevo = ConfiguracionAlarmas(
|
||||
alarmas: config.alarmas,
|
||||
vacaciones: config.vacaciones,
|
||||
excepciones: sinFallo,
|
||||
excepciones: excepciones,
|
||||
);
|
||||
await _guardar(nuevo);
|
||||
return nuevo;
|
||||
@@ -398,13 +408,14 @@ class ServicioAlarmas {
|
||||
List<ExcepcionAlarma> _sinFalloPrevio(
|
||||
List<ExcepcionAlarma> excepciones,
|
||||
String alarmaId,
|
||||
) => excepciones
|
||||
.where(
|
||||
(e) =>
|
||||
!(e.alarmaId == alarmaId &&
|
||||
ExcepcionAlarma.tiposFallo.contains(e.tipo)),
|
||||
)
|
||||
.toList();
|
||||
) =>
|
||||
excepciones
|
||||
.where(
|
||||
(e) =>
|
||||
!(e.alarmaId == alarmaId &&
|
||||
ExcepcionAlarma.tiposFallo.contains(e.tipo)),
|
||||
)
|
||||
.toList();
|
||||
|
||||
Future<ConfiguracionAlarmas> posponerEjecucion(
|
||||
String alarmaId,
|
||||
|
||||
@@ -161,6 +161,35 @@ class EjecucionAlarmaNativa {
|
||||
}
|
||||
}
|
||||
|
||||
/// A scheduling-reliability failure the NATIVE side recorded on its own
|
||||
/// (fix/alarmas-fallos-silenciosos, item 2): the pre-notice reminder, the
|
||||
/// ringing foreground service, and a post-boot/unlock reschedule can each
|
||||
/// fail without ever going through a Dart method-channel call that could
|
||||
/// throw -- the native scheduler persists these instead (mirroring how
|
||||
/// handled occurrences and snooze state already survive a killed engine),
|
||||
/// and this is the cold-start sync so the Dart side finds out at all.
|
||||
class FalloProgramacionNativo {
|
||||
const FalloProgramacionNativo({
|
||||
required this.alarmaId,
|
||||
required this.tipo,
|
||||
required this.ocurridoEn,
|
||||
});
|
||||
|
||||
final String alarmaId;
|
||||
final String tipo;
|
||||
final DateTime ocurridoEn;
|
||||
|
||||
factory FalloProgramacionNativo.fromMap(Map<Object?, Object?> map) {
|
||||
return FalloProgramacionNativo(
|
||||
alarmaId: map['alarmId'] as String? ?? '',
|
||||
tipo: map['type'] as String? ?? '',
|
||||
ocurridoEn: DateTime.fromMillisecondsSinceEpoch(
|
||||
(map['atMillis'] as num?)?.toInt() ?? 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class PuertoAlarmasAndroid {
|
||||
Stream<EventoAlarmaAndroid> get eventosAlarma;
|
||||
|
||||
@@ -170,6 +199,17 @@ abstract class PuertoAlarmasAndroid {
|
||||
|
||||
Future<void> programar(AlarmaMusical alarma);
|
||||
Future<void> cancelar(String alarmaId);
|
||||
|
||||
/// Failures the NATIVE side recorded on its own, outside any Dart call:
|
||||
/// a pre-notice that could not be armed, a refused foreground-service
|
||||
/// start when the alarm should have rung, and a per-alarm reschedule that
|
||||
/// failed after a reboot. Each entry carries the alarm id and one of
|
||||
/// [ExcepcionAlarma]'s `tipoFallo*` constants.
|
||||
///
|
||||
/// Before this existed every one of those paths logged to logcat and
|
||||
/// stopped there, so an alarm could sit switched on in the list having
|
||||
/// never reached the OS at all — the user's "as if there were no alarm".
|
||||
Future<List<Map<String, Object?>>> fallosNativosProgramacion();
|
||||
Future<void> ocultarNotificacionAlarma(String alarmaId);
|
||||
|
||||
/// Notification-only dismissal (RES-1): hides the fire notification for
|
||||
@@ -203,6 +243,11 @@ abstract class PuertoAlarmasAndroid {
|
||||
Future<EventoAlarmaAndroid?> obtenerEventoInicial();
|
||||
Future<List<EjecucionAlarmaNativa>> obtenerEjecucionesNativasGestionadas();
|
||||
Future<List<EstadoSnoozeNativo>> obtenerEstadoSnoozeNativo();
|
||||
|
||||
/// Scheduling-reliability failures the native side recorded on its own
|
||||
/// (pre-notice, foreground-service start, or post-boot reschedule) since
|
||||
/// the last sync.
|
||||
Future<List<FalloProgramacionNativo>> obtenerFallosProgramacionNativos();
|
||||
}
|
||||
|
||||
class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
@@ -393,6 +438,25 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> fallosNativosProgramacion() async {
|
||||
try {
|
||||
final raw = await _channel.invokeMethod<List<Object?>>(
|
||||
'getNativeSchedulingFailures',
|
||||
);
|
||||
if (raw == null) return const [];
|
||||
return raw
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.map((m) => m.map((k, v) => MapEntry(k.toString(), v)))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
// Never let a diagnostics read break alarm handling: an older build
|
||||
// of the native side simply has no such channel method.
|
||||
debugPrint('[PluriWave][alarmas] fallosNativosProgramacion ERROR $e');
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> solicitarPermisoAlarmasExactas() async {
|
||||
final abierto = await _channel.invokeMethod<bool>(
|
||||
@@ -494,6 +558,20 @@ class ServicioAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<FalloProgramacionNativo>>
|
||||
obtenerFallosProgramacionNativos() async {
|
||||
final raw = await _channel.invokeMethod<List<Object?>>(
|
||||
'getNativeSchedulingFailures',
|
||||
);
|
||||
if (raw == null || raw.isEmpty) return const [];
|
||||
return raw
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.map(FalloProgramacionNativo.fromMap)
|
||||
.where((fallo) => fallo.alarmaId.isNotEmpty && fallo.tipo.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> _logAndInvokeVoid(String method, Map<String, Object?> args) {
|
||||
debugPrint('[PluriWave][alarmas] $method $args');
|
||||
return _channel.invokeMethod<void>(method, args);
|
||||
|
||||
Reference in New Issue
Block a user