feat(audio): audio session integration and runtime robustness
- Integrate audio_session (new servicio_audio_session.dart): incoming calls pause the radio and resume on end, headphone unplug pauses without auto-resume, permanent focus loss never auto-resumes, duck lowers volume - Add play-intent flag to ServicioAudio so interruption handling and future reconnect logic can distinguish user pause from system-driven stops - Eliminate read-modify-write race in ServicioAlarmas with an in-memory cache and single-writer queue across all mutations; recalcularTodas persists only when state actually changed - Convert ServicioAlarmasAndroid static StreamController/handler to injectable instance fields, restoring test isolation - Inject a single cached SharedPreferences from main.dart across services and state (removes 23 inline getInstance() calls) - Move configurarLocalizaciones out of MiniReproductor.build() (was running on every rebuild during playback) - Bound the alarm fire-dedup set (cap 200 entries, 24h pruning) - 12 new tests (89 total green), flutter analyze clean
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/servicios/servicio_audio_session.dart';
|
||||
|
||||
class _ObjetivoFake implements ObjetivoAudioInterrumpible {
|
||||
bool intencion = false;
|
||||
bool reproduciendo = false;
|
||||
int pausas = 0;
|
||||
int reanudaciones = 0;
|
||||
final List<bool> atenuaciones = [];
|
||||
|
||||
@override
|
||||
bool get intencionReproducir => intencion;
|
||||
|
||||
@override
|
||||
bool get estaReproduciendo => reproduciendo;
|
||||
|
||||
@override
|
||||
Future<void> pausar() async {
|
||||
pausas++;
|
||||
reproduciendo = false;
|
||||
intencion = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> reanudar() async {
|
||||
reanudaciones++;
|
||||
reproduciendo = true;
|
||||
intencion = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setAtenuado(bool atenuado) async {
|
||||
atenuaciones.add(atenuado);
|
||||
}
|
||||
}
|
||||
|
||||
/// S3-R1: audio-session interruptions (phone call, transient loss, duck) and
|
||||
/// becoming-noisy (headphones unplugged) must pause/duck and auto-resume.
|
||||
void main() {
|
||||
test('interrupcion begin/pause pausa y baja la intencion (S3-R1)', () async {
|
||||
final objetivo =
|
||||
_ObjetivoFake()
|
||||
..reproduciendo = true
|
||||
..intencion = true;
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(true, AudioInterruptionType.pause),
|
||||
);
|
||||
|
||||
expect(objetivo.pausas, 1);
|
||||
expect(
|
||||
objetivo.intencionReproducir,
|
||||
isFalse,
|
||||
reason: 'el reconnect de S7 no debe pelear con la llamada en curso',
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'interrupcion end/shouldResume reanuda la reproduccion (S3-R1)',
|
||||
() async {
|
||||
final objetivo =
|
||||
_ObjetivoFake()
|
||||
..reproduciendo = true
|
||||
..intencion = true;
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(true, AudioInterruptionType.pause),
|
||||
);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(false, AudioInterruptionType.pause),
|
||||
);
|
||||
|
||||
expect(objetivo.reanudaciones, 1);
|
||||
expect(objetivo.intencionReproducir, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test('end sin pausa previa por interrupcion NO reanuda', () async {
|
||||
final objetivo = _ObjetivoFake();
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(false, AudioInterruptionType.pause),
|
||||
);
|
||||
|
||||
expect(
|
||||
objetivo.reanudaciones,
|
||||
0,
|
||||
reason:
|
||||
'si el usuario ya estaba en pausa, el fin de llamada no arranca audio',
|
||||
);
|
||||
});
|
||||
|
||||
test('duck atenua al comenzar y restaura al terminar', () async {
|
||||
final objetivo =
|
||||
_ObjetivoFake()
|
||||
..reproduciendo = true
|
||||
..intencion = true;
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(true, AudioInterruptionType.duck),
|
||||
);
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(false, AudioInterruptionType.duck),
|
||||
);
|
||||
|
||||
expect(objetivo.atenuaciones, [true, false]);
|
||||
expect(objetivo.pausas, 0);
|
||||
});
|
||||
|
||||
test('becoming-noisy (auriculares desconectados) pausa (S3-R1)', () async {
|
||||
final objetivo =
|
||||
_ObjetivoFake()
|
||||
..reproduciendo = true
|
||||
..intencion = true;
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarDesconexionSalida();
|
||||
|
||||
expect(objetivo.pausas, 1);
|
||||
|
||||
// A later interruption end must NOT resume: unplugging is a hard pause.
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(false, AudioInterruptionType.pause),
|
||||
);
|
||||
expect(objetivo.reanudaciones, 0);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user