guardarAlarma/posponerAlarma/posponerProximaDesdePreaviso now record a scheduling failure via ServicioAlarmas.registrarFalloProgramacion on catch and clear it on a successful (re)schedule, in addition to the existing transient EstadoAlarmas.error string. This makes the failure visible per-alarm via ultimaExcepcionPara instead of only a generic app-wide message. Also fixes _sincronizarTodas: a single alarm's android.programar throw used to abort the whole loop, silently skipping every sibling alarm scheduled AFTER it on that pass (including on every app launch, via inicializar). Each alarm's outcome is now independent.
203 lines
6.7 KiB
Dart
203 lines
6.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 _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;
|
|
int alarmasNativasPendientes = 0;
|
|
String fabricante = 'test';
|
|
int versionSdk = 35;
|
|
|
|
/// 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);
|
|
}
|
|
|
|
@override
|
|
Future<void> cancelar(String alarmaId) async {
|
|
canceladas.add(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);
|
|
|
|
int solicitudesPermisoAlarmasExactas = 0;
|
|
int solicitudesPermisoPantallaCompleta = 0;
|
|
|
|
@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();
|
|
}
|