feat(alarms): native reliability fixes and end-to-end snooze
- Use mediaPlayback|systemExempted FGS type with FOREGROUND_SERVICE_SYSTEM_EXEMPTED so alarms fire on Android 14+ (FOREGROUND_SERVICE_ALARM does not exist in the SDK) - Deduplicate fire notifications: the foreground service FSI notification is the single owner; receiver path removed - Notification channel v2 with alarm sound URI and USAGE_ALARM attributes, one-time guarded migration from legacy channels - Pass fallback station through the MethodChannel (NativeAlarmSpec schemaVersion 3) with a three-stage audio chain: primary -> fallback station -> bundled WAV - Native fade-in volume ramp honoring fadeInSegundos when the app is killed - Request battery-optimization exemption once, tracked with a persisted asked-once flag - Fix snooze end-to-end: native ACTION_SNOOZE now reports back to Flutter (snoozed event + cold-start sync), snooze anchor unified to occurrence+minutes on both sides, periodic recalc no longer erases an active snooze - Add snooze buttons (3/5/10/custom) to the ringing screen with shared audio teardown - Redesign ringing screen on PluriWaveScaffold with reduced-motion-aware entry animation (new PluriAnimate helper) - Alarm editor: live next-trigger preview, searchable station pickers (primary and fallback), configurable snooze duration, volume floor down to 0 - New alarm strings localized across all 13 locales - New unit/widget tests for the snooze flow, alarm bridge payloads, ringing screen and editor (77 tests green) - SDD artifacts for the app-quality-and-native-alarms change (explore, proposal, spec, design, tasks, apply progress)
This commit is contained in:
+19
-4
@@ -18,6 +18,8 @@ class FakeServicioAudio extends ServicioAudio {
|
||||
final List<PresetEcualizador> presetsAplicados = [];
|
||||
final List<Emisora> emisorasReproducidas = [];
|
||||
final List<bool> cambiosEcualizadorActivo = [];
|
||||
final List<double> volumenesAplicados = [];
|
||||
int pausas = 0;
|
||||
Emisora? _emisoraActual;
|
||||
EstadoReproduccion _estadoActual = EstadoReproduccion.detenido;
|
||||
|
||||
@@ -46,6 +48,17 @@ class FakeServicioAudio extends ServicioAudio {
|
||||
emitirEstado(EstadoReproduccion.detenido);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pausar() async {
|
||||
pausas++;
|
||||
emitirEstado(EstadoReproduccion.pausado);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setVolumen(double vol) async {
|
||||
volumenesAplicados.add(vol);
|
||||
}
|
||||
|
||||
void emitirEstado(EstadoReproduccion estado) {
|
||||
_estadoActual = estado;
|
||||
_estadoController.add(estado);
|
||||
@@ -116,7 +129,8 @@ class FakeServicioFavoritos extends ServicioFavoritos {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<GrupoFavoritos>> obtenerGrupos() async => List.unmodifiable(_grupos);
|
||||
Future<List<GrupoFavoritos>> obtenerGrupos() async =>
|
||||
List.unmodifiable(_grupos);
|
||||
|
||||
@override
|
||||
Future<GrupoFavoritos> crearGrupo(String nombre) async {
|
||||
@@ -151,9 +165,10 @@ class FakeServicioFavoritos extends ServicioFavoritos {
|
||||
|
||||
@override
|
||||
Future<void> asignarGrupo(String uuid, String grupoId) async {
|
||||
final destino = _grupos.any((g) => g.id == grupoId)
|
||||
? grupoId
|
||||
: GrupoFavoritos.sinAsignarId;
|
||||
final destino =
|
||||
_grupos.any((g) => g.id == grupoId)
|
||||
? grupoId
|
||||
: GrupoFavoritos.sinAsignarId;
|
||||
final index = _favoritos.indexWhere((e) => e.uuid == uuid);
|
||||
if (index != -1) {
|
||||
_favoritos[index] = _favoritos[index].copyWith(grupoFavoritosId: destino);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'dart:async';
|
||||
|
||||
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;
|
||||
|
||||
/// Simulates a native -> Flutter `alarmFired` MethodChannel event.
|
||||
void emitirEvento(EventoAlarmaAndroid evento) => _eventos.add(evento);
|
||||
|
||||
@override
|
||||
Stream<EventoAlarmaAndroid> get eventosAlarma => _eventos.stream;
|
||||
|
||||
@override
|
||||
Future<void> programar(AlarmaMusical alarma) async {
|
||||
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 {
|
||||
detenidas.add(alarmaId);
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
Reference in New Issue
Block a user