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:
@@ -90,6 +90,7 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
);
|
||||
await _sincronizarTodas();
|
||||
await cargarDiagnostico();
|
||||
await cargarFallosNativos();
|
||||
_activarRefresco();
|
||||
} catch (e) {
|
||||
_error = 'No se pudieron cargar las alarmas: $e';
|
||||
@@ -228,10 +229,22 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
|
||||
/// Clears a previously recorded scheduling failure once a later attempt
|
||||
/// for the same alarm succeeds (D5-style recovery, mirroring how [_error]
|
||||
/// itself already clears on a successful retry).
|
||||
/// itself already clears on a successful retry). Type-scoped: a
|
||||
/// successful `android.programar` call only proves the MAIN alarm
|
||||
/// registration (and, transitively, that any stale post-boot reschedule
|
||||
/// failure no longer applies) -- it says nothing about the pre-notice or
|
||||
/// foreground-service subsystems, so those are left untouched here.
|
||||
Future<void> _limpiarFalloProgramacion(String alarmaId) async {
|
||||
try {
|
||||
final config = await servicio.limpiarFalloProgramacion(alarmaId);
|
||||
var config = await servicio.limpiarFalloProgramacion(
|
||||
alarmaId,
|
||||
ExcepcionAlarma.tipoFalloProgramacion,
|
||||
);
|
||||
_aplicar(config);
|
||||
config = await servicio.limpiarFalloProgramacion(
|
||||
alarmaId,
|
||||
ExcepcionAlarma.tipoFalloReprogramacionArranque,
|
||||
);
|
||||
_aplicar(config);
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] limpiar fallo programacion ERROR $e');
|
||||
@@ -525,6 +538,39 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Drains the failures the NATIVE side recorded on its own and turns each
|
||||
/// into a per-alarm exception, so the card can mark it.
|
||||
///
|
||||
/// These three paths used to log to logcat and stop there: 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. None of them run inside a Dart call, so nothing on this side
|
||||
/// ever learned they happened — an alarm could sit switched on in the
|
||||
/// list having never reached the OS. Reading them at startup is what
|
||||
/// makes the reported "as if there were no alarm" visible.
|
||||
///
|
||||
/// Deliberately tolerant: a failed read is logged and swallowed, never
|
||||
/// surfaced as an alarm error, because a diagnostics gap must not look
|
||||
/// like a scheduling problem.
|
||||
Future<void> cargarFallosNativos() async {
|
||||
try {
|
||||
final fallos = await android.fallosNativosProgramacion();
|
||||
for (final fallo in fallos) {
|
||||
final alarmaId = fallo['alarmaId'] as String?;
|
||||
final tipo = fallo['tipo'] as String?;
|
||||
if (alarmaId == null || tipo == null) continue;
|
||||
await _registrarFalloProgramacion(alarmaId, tipo: tipo);
|
||||
}
|
||||
if (fallos.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[PluriWave][alarmas] fallos nativos recogidos=${fallos.length}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] cargar fallos nativos ERROR $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Records a snooze the native layer performed by itself (Decision 2.1).
|
||||
/// The native scheduler already re-registered setAlarmClock, so this only
|
||||
/// persists the canonical state — it MUST NOT call android.programar again.
|
||||
@@ -624,6 +670,60 @@ class EstadoAlarmas extends ChangeNotifier {
|
||||
debugPrint('[PluriWave][alarmas] sincronizar nativas ERROR $e');
|
||||
}
|
||||
await _importarSnoozesNativosActivos();
|
||||
await _importarFallosProgramacionNativos();
|
||||
}
|
||||
|
||||
/// Cold-start sync (fix/alarmas-fallos-silenciosos, item 2): imports
|
||||
/// scheduling-reliability failures the NATIVE side recorded on its own --
|
||||
/// a pre-notice `SecurityException`, a refused foreground-service start,
|
||||
/// or a per-alarm reschedule failure after boot/unlock -- none of which
|
||||
/// ever go through a Dart method-channel call that could throw. Without
|
||||
/// this sync, these three failures stayed invisible forever (only
|
||||
/// logcat), even after this app-launch fix reads them.
|
||||
Future<void> _importarFallosProgramacionNativos() async {
|
||||
try {
|
||||
final fallos = await android.obtenerFallosProgramacionNativos();
|
||||
final reportadoPorAlarma = {
|
||||
for (final fallo in fallos) fallo.alarmaId: fallo,
|
||||
};
|
||||
// Reconcile stale copies: the native side clears its OWN record the
|
||||
// next time that specific subsystem succeeds (pre-notice/foreground-
|
||||
// service), so an alarm previously imported with one of those tipos
|
||||
// that is no longer reported here means it already recovered --
|
||||
// without this, the card would keep showing a problem that fixed
|
||||
// itself. `tipoFalloProgramacion`/`tipoFalloReprogramacionArranque`
|
||||
// are NOT reconciled here -- those already clear on the Dart side's
|
||||
// own successful `android.programar` calls.
|
||||
for (final alarma in _alarmas) {
|
||||
final actual = ultimaExcepcionPara(alarma.id);
|
||||
final esTipoReconciliable =
|
||||
actual != null &&
|
||||
(actual.tipo == ExcepcionAlarma.tipoFalloPreaviso ||
|
||||
actual.tipo == ExcepcionAlarma.tipoFalloServicioSonido);
|
||||
if (esTipoReconciliable && !reportadoPorAlarma.containsKey(alarma.id)) {
|
||||
final config = await servicio.limpiarFalloProgramacion(
|
||||
alarma.id,
|
||||
actual.tipo,
|
||||
);
|
||||
_aplicar(config);
|
||||
}
|
||||
}
|
||||
for (final fallo in fallos) {
|
||||
final config = await servicio.registrarFalloProgramacion(
|
||||
fallo.alarmaId,
|
||||
fallo.ocurridoEn,
|
||||
fallo.tipo,
|
||||
);
|
||||
_aplicar(config);
|
||||
}
|
||||
if (fallos.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[PluriWave][alarmas] fallos nativos importados count=${fallos.length}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[PluriWave][alarmas] importar fallos nativos ERROR $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Cold-start half of Decision 2.1: imports snoozes the native scheduler
|
||||
|
||||
@@ -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