fix(eq): re-apply equalizer when the native audio session rotates
Build & Deploy PluriWave / Análisis de código (push) Successful in 41s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m33s

ExoPlayer assigns a new audio session id after transient audio-focus
interruptions (navigation prompts, radar warnings), leaving the
AndroidEqualizer attached to the dead session so playback resumed
without equalization until the next station switch. The session-id
listener now detects genuine rotations through a dedicated guard and
re-activates the equalizer with the current preset, gated on EQ
availability to stay clear of player teardown/rebuild.
This commit is contained in:
2026-07-10 18:51:45 +02:00
parent a31dc07318
commit 0ab63731d0
11 changed files with 1098 additions and 0 deletions
@@ -0,0 +1,99 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pluriwave/servicios/servicio_audio.dart';
/// EQ audio-focus re-apply — pure decision predicate truth table.
///
/// Covers spec "EQ Audio-Focus Re-Apply Specification": whenever
/// `androidAudioSessionIdStream` emits a new native session id mid-playback
/// (audio-focus ducking by another app), the equalizer must be re-attached
/// and the current preset's gains re-pushed, gated on EQ availability so it
/// never races `_recrearPlayer()`'s teardown/rebuild.
///
/// [PluriWaveAudioHandler] cannot be instantiated in unit tests — its
/// constructor builds a real `just_audio.AudioPlayer` that requires platform
/// MethodChannels (confirmed by `servicio_audio_source_switch_test.dart`, "We
/// cannot instantiate the handler in unit tests"). The only unit-testable
/// surface is the pure, side-effect-free static predicate
/// `debeReaplicarEcualizador`, extracted per the design's "Interfaces /
/// Contracts" section. Listener wiring and the actual native re-attach are
/// covered by manual on-device QA (spec's Testability Matrix), not here.
void main() {
group('debeReaplicarEcualizador (EQ audio-focus re-apply predicate)', () {
test('rotation while playing returns true', () {
// Spec: Requirement "Session Id Rotation Triggers EQ Re-Apply" /
// Scenario "Session id rotates while playing".
expect(
PluriWaveAudioHandler.debeReaplicarEcualizador(
sessionId: 2,
ultimaSessionIdEq: 1,
eqDisponible: true,
),
isTrue,
reason: 'a genuine id rotation with EQ available must re-apply',
);
});
test('same id re-emitted returns false', () {
// Spec: Scenario "Same id re-emitted produces no redundant re-apply".
expect(
PluriWaveAudioHandler.debeReaplicarEcualizador(
sessionId: 1,
ultimaSessionIdEq: 1,
eqDisponible: true,
),
isFalse,
reason: 'a duplicate emission of the already-processed id is a no-op',
);
});
test('first activation / matching guard returns false', () {
// Spec: Scenario "First legitimate activation is not double-applied".
// Same shape as the duplicate-id case above, kept as a distinct named
// case per design's testing table to document intent: this represents
// the id emitted right after a station switch already set the guard
// (via _activarEcualizador() at the station-switch call site), not
// just an arbitrary repeated emission.
expect(
PluriWaveAudioHandler.debeReaplicarEcualizador(
sessionId: 1,
ultimaSessionIdEq: 1,
eqDisponible: true,
),
isFalse,
reason:
'the first post-station-switch emission must not double-apply '
'on top of the station-switch path\'s own _activarEcualizador()',
);
});
test('null id returns false', () {
// Spec: Requirement "Session Id Rotation Triggers EQ Re-Apply"
// (non-null precondition).
expect(
PluriWaveAudioHandler.debeReaplicarEcualizador(
sessionId: null,
ultimaSessionIdEq: 1,
eqDisponible: true,
),
isFalse,
reason: 'a null session id must never trigger a re-apply',
);
});
test('teardown gate returns false', () {
// Spec: Requirement "Re-Apply Is Gated On EQ Availability" / Scenario
// "Rotation during player teardown is safely skipped".
expect(
PluriWaveAudioHandler.debeReaplicarEcualizador(
sessionId: 9,
ultimaSessionIdEq: 1,
eqDisponible: false,
),
isFalse,
reason:
'while _eqDisponible is false (mid _recrearPlayer teardown), '
're-apply must be skipped entirely to avoid racing the rebuild',
);
});
});
}