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.
251 lines
8.7 KiB
Dart
251 lines
8.7 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:pluriwave/l10n/gen/app_localizations.dart';
|
|
import 'package:pluriwave/modelos/alarma_musical.dart';
|
|
import 'package:pluriwave/servicios/servicio_alarmas_android.dart';
|
|
import 'package:pluriwave/servicios/servicio_grabacion_radio.dart';
|
|
|
|
/// Shared fake of the Android alarm bridge for alarm-related tests.
|
|
class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
|
final programadas = <AlarmaMusical>[];
|
|
final canceladas = <String>[];
|
|
final detenidas = <String>[];
|
|
final ocultadas = <String>[];
|
|
final soloOcultadas = <String>[];
|
|
final ejecucionesNativas = <EjecucionAlarmaNativa>[];
|
|
final snoozesNativos = <EstadoSnoozeNativo>[];
|
|
final fallosProgramacionNativos = <FalloProgramacionNativo>[];
|
|
final _eventos = StreamController<EventoAlarmaAndroid>.broadcast();
|
|
bool ignoraOptimizacionBateria = true;
|
|
int solicitudesExencionBateria = 0;
|
|
int aperturasConfiguracionNotificaciones = 0;
|
|
|
|
/// Extra diagnostico() fields (fix/alarmas-fiabilidad diagnostics screen).
|
|
/// Default values mirror the previous hardcoded literals in [diagnostico],
|
|
/// so every existing test that never sets these keeps seeing the exact
|
|
/// same snapshot as before.
|
|
bool puedeProgramarExactas = true;
|
|
bool notificacionesPermitidas = true;
|
|
bool puedeUsarPantallaCompleta = true;
|
|
String fabricante = 'test';
|
|
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"
|
|
/// coverage): when true, every `abrir*`/`solicitar*` system-screen action
|
|
/// below reports failure (as a real device does when a ROM lacks that
|
|
/// settings screen), while still recording the attempt via its counter.
|
|
bool fallaAccionSistema = false;
|
|
|
|
/// Test-only failure switch (Design D7): when true, [programar] throws
|
|
/// instead of scheduling, enabling failure-path coverage that the fake
|
|
/// could not otherwise produce.
|
|
bool fallaProgramar = false;
|
|
|
|
/// Test-only PER-ALARM failure switch (fix/alarmas-fallos-silenciosos):
|
|
/// [programar] throws only for ids in this set, letting a test simulate
|
|
/// one alarm failing to schedule while its siblings succeed -- the global
|
|
/// [fallaProgramar] switch cannot express that (it fails everything).
|
|
final Set<String> idsFallanProgramar = {};
|
|
|
|
/// Test-only failure switch: when true, [detenerSonidoActivo] reports an
|
|
/// unconfirmed/failed stop instead of a confirmed one.
|
|
bool fallaDetener = false;
|
|
|
|
/// Simulates [PluriWaveAlarmService.activeRingingId]: the id the fake
|
|
/// reports as currently ringing, or null if nothing rings.
|
|
String? alarmaSonandoIdValor;
|
|
|
|
/// Test-only failure switch (Finding 2, fail-toward-silence): when true,
|
|
/// [alarmaSonandoId] throws instead of returning a value, exercising the
|
|
/// [detenerSonidoNativo] fallback in `EstadoAlarmas._detenerSiEstaSonando`.
|
|
bool fallaConsultaSonando = false;
|
|
|
|
/// Every alarm id [detenerSonidoActivo] was invoked for, in call order.
|
|
final detencionesActivas = <String>[];
|
|
|
|
/// Test-only gate (RES-2 guard test): when set, [detenerSonidoActivo]
|
|
/// awaits it before resolving, letting tests exercise an overlapping
|
|
/// in-flight call.
|
|
Completer<void>? detenerActivoGate;
|
|
|
|
/// Simulates a native -> Flutter `alarmFired` MethodChannel event.
|
|
void emitirEvento(EventoAlarmaAndroid evento) => _eventos.add(evento);
|
|
|
|
@override
|
|
Stream<EventoAlarmaAndroid> get eventosAlarma => _eventos.stream;
|
|
|
|
@override
|
|
void configurarLocalizaciones(AppLocalizations l10n) {}
|
|
|
|
@override
|
|
Future<void> programar(AlarmaMusical alarma) async {
|
|
if (fallaProgramar || idsFallanProgramar.contains(alarma.id)) {
|
|
throw StateError('fake programar failure');
|
|
}
|
|
programadas.add(alarma);
|
|
if (alarma.activa && alarma.proximaProgramable != null) {
|
|
_idsRegistradosNativamente.add(alarma.id);
|
|
} else {
|
|
_idsRegistradosNativamente.remove(alarma.id);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> cancelar(String alarmaId) async {
|
|
canceladas.add(alarmaId);
|
|
_idsRegistradosNativamente.remove(alarmaId);
|
|
}
|
|
|
|
@override
|
|
Future<void> detenerSonidoNativo(String alarmaId) async {
|
|
detenidas.add(alarmaId);
|
|
}
|
|
|
|
@override
|
|
Future<String?> alarmaSonandoId() async {
|
|
if (fallaConsultaSonando) {
|
|
throw StateError('fake alarmaSonandoId failure');
|
|
}
|
|
return alarmaSonandoIdValor;
|
|
}
|
|
|
|
@override
|
|
Future<ResultadoDetencion> detenerSonidoActivo() async {
|
|
detencionesActivas.add(alarmaSonandoIdValor ?? '');
|
|
final gate = detenerActivoGate;
|
|
if (gate != null) await gate.future;
|
|
if (fallaDetener) {
|
|
return const ResultadoDetencion(detenido: false, estabaSonando: true);
|
|
}
|
|
return ResultadoDetencion(
|
|
detenido: true,
|
|
estabaSonando: alarmaSonandoIdValor != null,
|
|
alarmaId: alarmaSonandoIdValor,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<void> ocultarNotificacionAlarma(String alarmaId) async {
|
|
ocultadas.add(alarmaId);
|
|
}
|
|
|
|
@override
|
|
Future<void> ocultarSoloNotificacion(String alarmaId) async {
|
|
soloOcultadas.add(alarmaId);
|
|
}
|
|
|
|
@override
|
|
Future<DiagnosticoAlarmasAndroid> diagnostico() async =>
|
|
DiagnosticoAlarmasAndroid(
|
|
puedeProgramarExactas: puedeProgramarExactas,
|
|
notificacionesPermitidas: notificacionesPermitidas,
|
|
puedeUsarPantallaCompleta: puedeUsarPantallaCompleta,
|
|
ignoraOptimizacionBateria: ignoraOptimizacionBateria,
|
|
alarmasNativasPendientes: alarmasNativasPendientes,
|
|
fabricante: fabricante,
|
|
versionSdk: versionSdk,
|
|
);
|
|
|
|
@override
|
|
Future<bool> solicitarExencionBateria() async {
|
|
solicitudesExencionBateria++;
|
|
return !fallaAccionSistema;
|
|
}
|
|
|
|
@override
|
|
Future<bool> abrirConfiguracionNotificaciones() async {
|
|
aperturasConfiguracionNotificaciones++;
|
|
return !fallaAccionSistema;
|
|
}
|
|
|
|
@override
|
|
Future<EventoAlarmaAndroid?> obtenerEventoInicial() async => null;
|
|
|
|
@override
|
|
Future<List<EjecucionAlarmaNativa>>
|
|
obtenerEjecucionesNativasGestionadas() async => ejecucionesNativas;
|
|
|
|
@override
|
|
Future<List<EstadoSnoozeNativo>> obtenerEstadoSnoozeNativo() async =>
|
|
List.of(snoozesNativos);
|
|
|
|
@override
|
|
Future<List<FalloProgramacionNativo>>
|
|
obtenerFallosProgramacionNativos() async =>
|
|
List.of(fallosProgramacionNativos);
|
|
|
|
int solicitudesPermisoAlarmasExactas = 0;
|
|
int solicitudesPermisoPantallaCompleta = 0;
|
|
|
|
/// Native-recorded failures the next read should return. Tests seed this
|
|
/// to simulate a pre-notice that never armed, a refused foreground-service
|
|
/// start, or a per-alarm reschedule that failed after a reboot.
|
|
List<Map<String, Object?>> fallosNativos = const [];
|
|
|
|
int lecturasFallosNativos = 0;
|
|
|
|
/// Simulates an older native build with no such channel method.
|
|
bool fallaLecturaFallosNativos = false;
|
|
|
|
@override
|
|
Future<List<Map<String, Object?>>> fallosNativosProgramacion() async {
|
|
lecturasFallosNativos++;
|
|
if (fallaLecturaFallosNativos) {
|
|
throw StateError('canal no disponible');
|
|
}
|
|
return fallosNativos;
|
|
}
|
|
|
|
@override
|
|
Future<bool> solicitarPermisoAlarmasExactas() async {
|
|
solicitudesPermisoAlarmasExactas++;
|
|
return !fallaAccionSistema;
|
|
}
|
|
|
|
@override
|
|
Future<bool> solicitarPermisoNotificaciones() async => true;
|
|
|
|
@override
|
|
Future<bool> solicitarPermisoPantallaCompleta() async {
|
|
solicitudesPermisoPantallaCompleta++;
|
|
return !fallaAccionSistema;
|
|
}
|
|
|
|
Future<void> dispose() => _eventos.close();
|
|
}
|
|
|
|
/// Inactive recording service fake, safe for widget tests.
|
|
class FakeServicioGrabacionRadioInactiva extends ServicioGrabacionRadio {
|
|
final _controller = StreamController<EstadoGrabacionRadio>.broadcast();
|
|
|
|
@override
|
|
EstadoGrabacionRadio get estado => const EstadoGrabacionRadio.inactiva();
|
|
|
|
@override
|
|
Stream<EstadoGrabacionRadio> get estadoStream => _controller.stream;
|
|
|
|
@override
|
|
Future<void> inicializar() async {}
|
|
|
|
@override
|
|
Future<void> dispose() => _controller.close();
|
|
}
|