The native service and the Flutter player each ran their own 5%-to- target fade-in, and both could drive audible volume at the handoff, producing a jump or ramp reset. The Dart ramp now starts exactly once from the handoff-confirmation path: the player still pre-starts at 5%, and _confirmarAudioFlutterListo() starts the ramp in a finally block so it runs whether the native confirmation succeeds or fails — the alarm can never stay stuck at 5% if the native side is already gone. Work unit 3/3 of alarm-volume-ramp-restore (fade-in dedup).
155 lines
4.7 KiB
Dart
155 lines
4.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 ejecucionesNativas = <EjecucionAlarmaNativa>[];
|
|
final snoozesNativos = <EstadoSnoozeNativo>[];
|
|
final _eventos = StreamController<EventoAlarmaAndroid>.broadcast();
|
|
bool ignoraOptimizacionBateria = true;
|
|
int solicitudesExencionBateria = 0;
|
|
|
|
/// Records each [forzarVolumenMediaParaAlarma] call (Slice 2: ring-scoped
|
|
/// device-volume override).
|
|
final volumenForzado = <double>[];
|
|
|
|
/// Counts [restaurarVolumenMedia] calls (Slice 2). A plain counter, not a
|
|
/// list: widget tests only need to assert how many times restore ran.
|
|
int volumenRestaurado = 0;
|
|
|
|
/// 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 gate (Slice 3: fade-in dedup at handoff). When set,
|
|
/// [confirmarAudioFlutter] suspends on this completer before resolving,
|
|
/// letting a test observe the pre-confirm state deterministically instead
|
|
/// of racing real stream/timer scheduling. Complete it to let the call
|
|
/// proceed.
|
|
Completer<void>? puertaConfirmarAudioFlutter;
|
|
|
|
/// Test-only failure switch (Slice 3 edge case): when true,
|
|
/// [confirmarAudioFlutter] throws after the gate above (if any) resolves,
|
|
/// simulating a dead/never-there native channel at handoff.
|
|
bool fallaConfirmarAudioFlutter = false;
|
|
|
|
/// 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) {
|
|
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<void> ocultarNotificacionAlarma(String alarmaId) async {
|
|
ocultadas.add(alarmaId);
|
|
}
|
|
|
|
@override
|
|
Future<void> confirmarAudioFlutter(String alarmaId) async {
|
|
if (puertaConfirmarAudioFlutter != null) {
|
|
await puertaConfirmarAudioFlutter!.future;
|
|
}
|
|
if (fallaConfirmarAudioFlutter) {
|
|
throw StateError('fake confirmarAudioFlutter failure');
|
|
}
|
|
detenidas.add(alarmaId);
|
|
}
|
|
|
|
@override
|
|
Future<void> forzarVolumenMediaParaAlarma(double fraccion) async {
|
|
volumenForzado.add(fraccion);
|
|
}
|
|
|
|
@override
|
|
Future<void> restaurarVolumenMedia() async {
|
|
volumenRestaurado++;
|
|
}
|
|
|
|
@override
|
|
Future<DiagnosticoAlarmasAndroid> diagnostico() async =>
|
|
DiagnosticoAlarmasAndroid(
|
|
puedeProgramarExactas: true,
|
|
notificacionesPermitidas: true,
|
|
puedeUsarPantallaCompleta: true,
|
|
ignoraOptimizacionBateria: ignoraOptimizacionBateria,
|
|
alarmasNativasPendientes: 0,
|
|
fabricante: 'test',
|
|
versionSdk: 35,
|
|
);
|
|
|
|
@override
|
|
Future<bool> solicitarExencionBateria() async {
|
|
solicitudesExencionBateria++;
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<EventoAlarmaAndroid?> obtenerEventoInicial() async => null;
|
|
|
|
@override
|
|
Future<List<EjecucionAlarmaNativa>>
|
|
obtenerEjecucionesNativasGestionadas() async => ejecucionesNativas;
|
|
|
|
@override
|
|
Future<List<EstadoSnoozeNativo>> obtenerEstadoSnoozeNativo() async =>
|
|
List.of(snoozesNativos);
|
|
|
|
@override
|
|
Future<bool> solicitarPermisoAlarmasExactas() async => true;
|
|
|
|
@override
|
|
Future<bool> solicitarPermisoNotificaciones() async => true;
|
|
|
|
@override
|
|
Future<bool> solicitarPermisoPantallaCompleta() async => true;
|
|
|
|
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();
|
|
}
|