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:
@@ -9,6 +9,7 @@ import '../l10n/display_names.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../modelos/preset_ecualizador.dart';
|
||||
import 'servicio_audio_session.dart';
|
||||
|
||||
/// Estado de reproducción expuesto al UI.
|
||||
enum EstadoReproduccion { detenido, cargando, reproduciendo, pausado, error }
|
||||
@@ -110,9 +111,12 @@ class ServicioAudio {
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// AudioHandler
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
with SeekHandler
|
||||
implements ObjetivoAudioInterrumpible {
|
||||
static const _timeoutCambioFuente = Duration(seconds: 12);
|
||||
static const _timeoutCierrePlayer = Duration(seconds: 3);
|
||||
static const _factorAtenuacion = 0.3;
|
||||
|
||||
AndroidEqualizer _eq = AndroidEqualizer();
|
||||
late AudioPlayer _player = _crearPlayer();
|
||||
@@ -130,6 +134,15 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
double get volumen => _volumen;
|
||||
AppLocalizations? _l10n;
|
||||
|
||||
/// Intent-to-play flag (Designs 3.1/7.2): reflects the LAST explicit
|
||||
/// intent (play/pause/stop, including audio-session interruptions, which
|
||||
/// pause through [pausar]). The S7 reconnect state machine reads it to
|
||||
/// distinguish a network stall from an intentional pause.
|
||||
bool _intencionReproducir = false;
|
||||
|
||||
/// Ducked state requested by the audio session (transient focus loss).
|
||||
bool _atenuado = false;
|
||||
|
||||
AndroidEqualizer? get ecualizador => _eq;
|
||||
bool _eqDisponible = false;
|
||||
bool get ecualizadorDisponible => _eqDisponible;
|
||||
@@ -278,6 +291,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
|
||||
@override
|
||||
Future<void> playMediaItem(MediaItem mediaItem) async {
|
||||
_intencionReproducir = true;
|
||||
final revision = ++_revisionFuente;
|
||||
_colaCambioFuente = _colaCambioFuente
|
||||
.catchError((_) {})
|
||||
@@ -349,7 +363,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
_eqDisponible = false;
|
||||
_androidAudioSessionId = null;
|
||||
_player = _crearPlayer();
|
||||
await _player.setVolume(_volumen);
|
||||
await _player.setVolume(_volumenEfectivo);
|
||||
_conectarStreamsPlayer();
|
||||
}
|
||||
|
||||
@@ -450,17 +464,48 @@ class PluriWaveAudioHandler extends BaseAudioHandler with SeekHandler {
|
||||
|
||||
Future<void> setVolumen(double vol) async {
|
||||
_volumen = vol.clamp(0.0, 1.0);
|
||||
await _player.setVolume(_volumen);
|
||||
await _player.setVolume(_volumenEfectivo);
|
||||
}
|
||||
|
||||
double get _volumenEfectivo =>
|
||||
_atenuado ? _volumen * _factorAtenuacion : _volumen;
|
||||
|
||||
// ── ObjetivoAudioInterrumpible (audio-session seam, S3-R1) ───────────────
|
||||
|
||||
@override
|
||||
bool get intencionReproducir => _intencionReproducir;
|
||||
|
||||
@override
|
||||
bool get estaReproduciendo => playbackState.value.playing;
|
||||
|
||||
@override
|
||||
Future<void> pausar() => pause();
|
||||
|
||||
@override
|
||||
Future<void> reanudar() => play();
|
||||
|
||||
@override
|
||||
Future<void> setAtenuado(bool atenuado) async {
|
||||
if (_atenuado == atenuado) return;
|
||||
_atenuado = atenuado;
|
||||
await _player.setVolume(_volumenEfectivo);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() => _player.play();
|
||||
Future<void> play() {
|
||||
_intencionReproducir = true;
|
||||
return _player.play();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() => _player.pause();
|
||||
Future<void> pause() {
|
||||
_intencionReproducir = false;
|
||||
return _player.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
_intencionReproducir = false;
|
||||
_revisionFuente++;
|
||||
await _player.stop();
|
||||
emisoraActual = null;
|
||||
|
||||
Reference in New Issue
Block a user