import 'package:audio_session/audio_session.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:pluriwave/servicios/servicio_audio_session.dart'; /// Reported from the car: PluriWave disappeared from the Android Auto pane /// mid-drive and another media app took its slot, the playback screen sat /// frozen, and the chosen equalizer was lost whenever navigation, a /// speed-camera app or the car's voice assistant spoke. /// /// One cause behind all three. `androidWillPauseWhenDucked: true` made /// `audio_session` translate Android's AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK /// into a full PAUSE. In a car that fires on every navigation instruction. A /// pause publishes `playing: false`, `AudioService.setState` turns that into /// `exitPlayingState()` and — with `androidStopForegroundOnPause` — into /// `stopForeground(...)`. The plugin's own doc for that flag: «while in this /// lower priority state, the operating system will also be able to kill your /// service at any time to reclaim resources». A killed service is a media /// session that vanishes from the car. /// /// Ducking keeps `playing: true` throughout, so session, notification and /// car pane all survive the interruption. class _ObjetivoFalso implements ObjetivoAudioInterrumpible { bool atenuado = false; int pausas = 0; int reanudaciones = 0; int reaplicacionesEq = 0; @override bool intencionReproducir = true; @override bool estaReproduciendo = true; @override Future pausar() async => pausas++; @override Future reanudar() async => reanudaciones++; @override Future setAtenuado(bool valor) async => atenuado = valor; @override Future reaplicarEcualizador() async => reaplicacionesEq++; } void main() { test('un aviso de navegación ATENÚA, nunca pausa', () async { final objetivo = _ObjetivoFalso(); final servicio = ServicioAudioSession(objetivo: objetivo); await servicio.manejarInterrupcion( AudioInterruptionEvent(true, AudioInterruptionType.duck), ); expect(objetivo.atenuado, isTrue); expect( objetivo.pausas, 0, reason: 'pausar publica playing:false -> exitPlayingState -> el servicio ' 'sale de primer plano y Android puede matarlo; ahí es donde la app ' 'desaparecía del panel del coche', ); }); test('al terminar el aviso se recupera el volumen Y se reasienta el ' 'ecualizador', () async { final objetivo = _ObjetivoFalso(); final servicio = ServicioAudioSession(objetivo: objetivo); await servicio.manejarInterrupcion( AudioInterruptionEvent(true, AudioInterruptionType.duck), ); await servicio.manejarInterrupcion( AudioInterruptionEvent(false, AudioInterruptionType.duck), ); expect(objetivo.atenuado, isFalse); expect( objetivo.reaplicacionesEq, 1, reason: 'Android puede desactivar en silencio el efecto de esta app cuando ' 'otro cliente de mayor prioridad toma el foco, y el id de sesión no ' 'cambia, así que el disparador por rotación de sesión no salta', ); expect(objetivo.pausas, 0); }); test('una llamada entrante SÍ pausa: no todo es un aviso corto', () async { // The duck change must not turn a real, non-duckable focus loss into a // station that keeps playing over a phone call. final objetivo = _ObjetivoFalso(); final servicio = ServicioAudioSession(objetivo: objetivo); await servicio.manejarInterrupcion( AudioInterruptionEvent(true, AudioInterruptionType.pause), ); expect(objetivo.pausas, 1); await servicio.manejarInterrupcion( AudioInterruptionEvent(false, AudioInterruptionType.pause), ); expect(objetivo.reanudaciones, 1); expect(objetivo.reaplicacionesEq, 1); }); test('desconectar los auriculares pausa y NO reanuda solo', () async { final objetivo = _ObjetivoFalso(); final servicio = ServicioAudioSession(objetivo: objetivo); await servicio.manejarDesconexionSalida(); expect(objetivo.pausas, 1); expect(objetivo.reanudaciones, 0); }); }