feat(alarmas): verify native registration after a successful save
android.programar() returning without throwing was treated as proof the OS registered the alarm -- this is exactly the gap the reported case fell through. guardarAlarma now cross-checks a fresh native pending-alarm count against how many alarms Dart believes are active-with-a-next-run right after a successful schedule call, and records a failure for the just-saved alarm when the native count falls short. FakePuertoAlarmasAndroid.alarmasNativasPendientes now defaults to a count derived from programar()/cancelar() calls (mirroring the real native scheduler's own registry) instead of a frozen 0, while any test that explicitly assigns the field keeps getting exactly that value regardless of what programar/cancelar do afterward -- verified against the full suite, no regressions.
This commit is contained in:
@@ -118,6 +118,7 @@ class EstadoAlarmas extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
await android.programar(guardada);
|
await android.programar(guardada);
|
||||||
await _limpiarFalloProgramacion(guardada.id);
|
await _limpiarFalloProgramacion(guardada.id);
|
||||||
|
await _verificarRegistroNativo(guardada.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_error = 'Alarma guardada, pero Android no pudo programarla todavía: $e';
|
_error = 'Alarma guardada, pero Android no pudo programarla todavía: $e';
|
||||||
await _registrarFalloProgramacion(alarma.id);
|
await _registrarFalloProgramacion(alarma.id);
|
||||||
@@ -221,9 +222,7 @@ class EstadoAlarmas extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
_aplicar(config);
|
_aplicar(config);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint(
|
debugPrint('[PluriWave][alarmas] registrar fallo programacion ERROR $e');
|
||||||
'[PluriWave][alarmas] registrar fallo programacion ERROR $e',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,6 +238,40 @@ class EstadoAlarmas extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verifies the OS genuinely registered [alarmaId] after a successful
|
||||||
|
/// `android.programar` call (fix/alarmas-fallos-silenciosos, item 3): a
|
||||||
|
/// scheduling call that returns without throwing is not proof enough by
|
||||||
|
/// itself -- this cross-check against the native pending-alarm count is
|
||||||
|
/// exactly what would have caught the reported "alarm never rings, no
|
||||||
|
/// exception anywhere" case. Compares a FRESH native count against how
|
||||||
|
/// many alarms Dart believes are currently active-with-a-next-run; a
|
||||||
|
/// native count that falls short is recorded as a failure for the alarm
|
||||||
|
/// the user just interacted with. Never overrides an already-caught
|
||||||
|
/// programar() exception (this only runs on ITS success path).
|
||||||
|
Future<void> _verificarRegistroNativo(String alarmaId) async {
|
||||||
|
try {
|
||||||
|
final alarma = _buscarAlarma(alarmaId);
|
||||||
|
if (alarma == null ||
|
||||||
|
!alarma.activa ||
|
||||||
|
alarma.proximaProgramable == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final diag = await android.diagnostico();
|
||||||
|
_diagnostico = diag;
|
||||||
|
final esperadas =
|
||||||
|
_alarmas
|
||||||
|
.where((a) => a.activa && a.proximaProgramable != null)
|
||||||
|
.length;
|
||||||
|
if (diag.alarmasNativasPendientes < esperadas) {
|
||||||
|
_error =
|
||||||
|
'Alarma guardada, pero el sistema no confirma que quedó registrada.';
|
||||||
|
await _registrarFalloProgramacion(alarmaId);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[PluriWave][alarmas] verificar registro nativo ERROR $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> cambiarActiva(AlarmaMusical alarma, bool activa) async {
|
Future<void> cambiarActiva(AlarmaMusical alarma, bool activa) async {
|
||||||
await guardarAlarma(alarma.copyWith(activa: activa));
|
await guardarAlarma(alarma.copyWith(activa: activa));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pluriwave/estado/estado_alarmas.dart';
|
||||||
|
import 'package:pluriwave/modelos/alarma_musical.dart';
|
||||||
|
import 'package:pluriwave/servicios/servicio_alarmas.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../helpers/fakes_alarmas.dart';
|
||||||
|
|
||||||
|
/// fix/alarmas-fallos-silenciosos, item 3: "verify the alarm is actually
|
||||||
|
/// registered, and say so if it is not". `android.programar` returning
|
||||||
|
/// without throwing is not proof enough by itself -- this is the check that
|
||||||
|
/// would have caught the reported case immediately (the native side can
|
||||||
|
/// silently fail to persist the registration even when the channel call
|
||||||
|
/// itself reports success).
|
||||||
|
void main() {
|
||||||
|
setUp(() {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('guardarAlarma detecta que el conteo nativo no refleja la alarma '
|
||||||
|
'guardada, aunque android.programar no haya lanzado', () async {
|
||||||
|
// Fixed at 0 regardless of what programar() does internally --
|
||||||
|
// simulates the native side accepting the channel call but never
|
||||||
|
// actually persisting the registration.
|
||||||
|
final android = FakePuertoAlarmasAndroid()..alarmasNativasPendientes = 0;
|
||||||
|
final estado = EstadoAlarmas(
|
||||||
|
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
|
||||||
|
android: android,
|
||||||
|
iniciarAutomaticamente: false,
|
||||||
|
);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
addTearDown(android.dispose);
|
||||||
|
|
||||||
|
await estado.guardarAlarma(
|
||||||
|
const AlarmaMusical(
|
||||||
|
id: 'silenciosa1',
|
||||||
|
nombre: 'Diaria',
|
||||||
|
hora: 7,
|
||||||
|
minuto: 30,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final excepcion = estado.ultimaExcepcionPara('silenciosa1');
|
||||||
|
expect(excepcion, isNotNull);
|
||||||
|
expect(excepcion!.tipo, ExcepcionAlarma.tipoFalloProgramacion);
|
||||||
|
expect(estado.error, isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('guardarAlarma en el camino feliz (conteo nativo coincide) no registra '
|
||||||
|
'fallo alguno', () async {
|
||||||
|
final android = FakePuertoAlarmasAndroid();
|
||||||
|
final estado = EstadoAlarmas(
|
||||||
|
servicio: ServicioAlarmas(reloj: () => DateTime(2026, 5, 25, 7)),
|
||||||
|
android: android,
|
||||||
|
iniciarAutomaticamente: false,
|
||||||
|
);
|
||||||
|
addTearDown(estado.dispose);
|
||||||
|
addTearDown(android.dispose);
|
||||||
|
|
||||||
|
await estado.guardarAlarma(
|
||||||
|
const AlarmaMusical(
|
||||||
|
id: 'sana1',
|
||||||
|
nombre: 'Diaria',
|
||||||
|
hora: 7,
|
||||||
|
minuto: 30,
|
||||||
|
tipoProgramacion: TipoProgramacionAlarma.diaria,
|
||||||
|
diasSemana: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(estado.ultimaExcepcionPara('sana1'), isNull);
|
||||||
|
expect(estado.error, isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -26,10 +26,27 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
|||||||
bool puedeProgramarExactas = true;
|
bool puedeProgramarExactas = true;
|
||||||
bool notificacionesPermitidas = true;
|
bool notificacionesPermitidas = true;
|
||||||
bool puedeUsarPantallaCompleta = true;
|
bool puedeUsarPantallaCompleta = true;
|
||||||
int alarmasNativasPendientes = 0;
|
|
||||||
String fabricante = 'test';
|
String fabricante = 'test';
|
||||||
int versionSdk = 35;
|
int versionSdk = 35;
|
||||||
|
|
||||||
|
/// Ids [programar] most recently scheduled as active-with-a-next-run (kept
|
||||||
|
/// in sync with [cancelar] too), mirroring the real native scheduler's own
|
||||||
|
/// pending-alarm registry (fix/alarmas-fallos-silenciosos, item 3: "verify
|
||||||
|
/// the alarm is actually registered"). Backs [alarmasNativasPendientes]'s
|
||||||
|
/// DEFAULT so a test that never touches that field gets a value that
|
||||||
|
/// tracks reality instead of a frozen `0` -- a test that explicitly
|
||||||
|
/// assigns the field (many `pantalla_diagnostico_alarmas_test.dart` cases
|
||||||
|
/// do, to model a stale/corrupt native count on purpose) keeps getting
|
||||||
|
/// EXACTLY that value regardless of what programar/cancelar do afterward.
|
||||||
|
final _idsRegistradosNativamente = <String>{};
|
||||||
|
int? _alarmasNativasPendientesFijado;
|
||||||
|
|
||||||
|
int get alarmasNativasPendientes =>
|
||||||
|
_alarmasNativasPendientesFijado ?? _idsRegistradosNativamente.length;
|
||||||
|
|
||||||
|
set alarmasNativasPendientes(int valor) =>
|
||||||
|
_alarmasNativasPendientesFijado = valor;
|
||||||
|
|
||||||
/// Test-only failure switch (diagnostics screen, "intent not resolving"
|
/// Test-only failure switch (diagnostics screen, "intent not resolving"
|
||||||
/// coverage): when true, every `abrir*`/`solicitar*` system-screen action
|
/// coverage): when true, every `abrir*`/`solicitar*` system-screen action
|
||||||
/// below reports failure (as a real device does when a ROM lacks that
|
/// below reports failure (as a real device does when a ROM lacks that
|
||||||
@@ -83,11 +100,17 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
|||||||
throw StateError('fake programar failure');
|
throw StateError('fake programar failure');
|
||||||
}
|
}
|
||||||
programadas.add(alarma);
|
programadas.add(alarma);
|
||||||
|
if (alarma.activa && alarma.proximaProgramable != null) {
|
||||||
|
_idsRegistradosNativamente.add(alarma.id);
|
||||||
|
} else {
|
||||||
|
_idsRegistradosNativamente.remove(alarma.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> cancelar(String alarmaId) async {
|
Future<void> cancelar(String alarmaId) async {
|
||||||
canceladas.add(alarmaId);
|
canceladas.add(alarmaId);
|
||||||
|
_idsRegistradosNativamente.remove(alarmaId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user