|
|
|
@@ -56,6 +56,11 @@ typedef GuardarEqActivoPersistido = Future<void> Function(bool activo);
|
|
|
|
|
/// tests, fakes), which simply falls back to deriving the context on the spot.
|
|
|
|
|
typedef LeerContextoSaltoPersistido = Future<ContextoSalto?> Function();
|
|
|
|
|
|
|
|
|
|
/// Read port for the equalizer's persisted PRESET, the exact sibling of
|
|
|
|
|
/// [LeerEqActivoPersistido]. Bound to `ServicioEcualizador.leerPresetPrincipal`
|
|
|
|
|
/// in `main.dart`; `null` for any caller with no disk (widget tests, fakes).
|
|
|
|
|
typedef LeerPresetPersistido = Future<PresetEcualizador?> Function();
|
|
|
|
|
|
|
|
|
|
/// Write port for the same context. Bound to `guardarContextoSalto`.
|
|
|
|
|
typedef GuardarContextoSaltoPersistido =
|
|
|
|
|
Future<void> Function(ContextoSalto contexto);
|
|
|
|
@@ -86,6 +91,22 @@ bool? _eqActivoPersistido;
|
|
|
|
|
/// equalizer on, and the app has always behaved that way.
|
|
|
|
|
bool estadoEqInicial({required bool? persistido}) => persistido ?? true;
|
|
|
|
|
|
|
|
|
|
/// The two native operations an equalizer on/off transition is made of, as
|
|
|
|
|
/// values so their ORDER is a testable fact rather than the incidental shape
|
|
|
|
|
/// of a method body.
|
|
|
|
|
///
|
|
|
|
|
/// Off-device neither operation is observable (`_eqDisponible` is `false`, and
|
|
|
|
|
/// `AndroidEqualizer.parameters` never completes without an attached player),
|
|
|
|
|
/// so before this enum the sequence could only be asserted by reading the
|
|
|
|
|
/// source — which is how the wrong one shipped.
|
|
|
|
|
enum PasoEcualizador {
|
|
|
|
|
/// Write the current preset's band levels into the native effect.
|
|
|
|
|
ganancias,
|
|
|
|
|
|
|
|
|
|
/// Flip the native effect on or off (`AudioEffect.setEnabled`).
|
|
|
|
|
habilitacion,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Reads the persisted equalizer flag through [leer] exactly once and seeds
|
|
|
|
|
/// [handler] with it, without ever writing back.
|
|
|
|
|
///
|
|
|
|
@@ -110,6 +131,37 @@ Future<void> _sembrarEcualizadorDesdeDisco(
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Reads the persisted equalizer PRESET through [leer] exactly once and seeds
|
|
|
|
|
/// [handler] with it.
|
|
|
|
|
///
|
|
|
|
|
/// The exact sibling of [_sembrarEcualizadorDesdeDisco], and it exists for the
|
|
|
|
|
/// exact same reason. eq-estado-unico gave the on/off FLAG a UI-independent
|
|
|
|
|
/// link to disk; the preset never got one, so `_presetActual` stayed on its
|
|
|
|
|
/// hardcoded `PresetEcualizador.flat`. On a phone that is invisible —
|
|
|
|
|
/// `EstadoEcualizador` owns the real preset and pushes it into the handler as
|
|
|
|
|
/// soon as the widget tree exists. On the headless engine Android Auto starts
|
|
|
|
|
/// there is no widget tree and no `EstadoEcualizador`, so a car toggle
|
|
|
|
|
/// enabled the equalizer and applied FLAT.
|
|
|
|
|
///
|
|
|
|
|
/// Never throws: an unreadable preference store leaves the handler on the
|
|
|
|
|
/// historical default rather than taking down the audio bootstrap.
|
|
|
|
|
Future<void> _sembrarPresetDesdeDisco(
|
|
|
|
|
PluriWaveAudioHandler handler,
|
|
|
|
|
LeerPresetPersistido leer,
|
|
|
|
|
) async {
|
|
|
|
|
PresetEcualizador? persistido;
|
|
|
|
|
try {
|
|
|
|
|
persistido = await leer();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint(
|
|
|
|
|
'[PluriWave][ServicioAudio] no se pudo leer el preset EQ persistido: $e',
|
|
|
|
|
);
|
|
|
|
|
persistido = null;
|
|
|
|
|
}
|
|
|
|
|
if (persistido == null) return;
|
|
|
|
|
await handler.sembrarPresetEcualizador(persistido);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Wires the freshly built handler into the module-level seams.
|
|
|
|
|
///
|
|
|
|
|
/// [leerEqActivoPersistido] and [guardarEqActivoPersistido] give the handler
|
|
|
|
@@ -123,6 +175,7 @@ void registrarHandler(
|
|
|
|
|
PluriWaveAudioHandler handler, {
|
|
|
|
|
LeerEqActivoPersistido? leerEqActivoPersistido,
|
|
|
|
|
GuardarEqActivoPersistido? guardarEqActivoPersistido,
|
|
|
|
|
LeerPresetPersistido? leerPresetPersistido,
|
|
|
|
|
LeerContextoSaltoPersistido? leerContextoSalto,
|
|
|
|
|
GuardarContextoSaltoPersistido? guardarContextoSalto,
|
|
|
|
|
}) {
|
|
|
|
@@ -142,6 +195,9 @@ void registrarHandler(
|
|
|
|
|
if (leerEqActivoPersistido != null) {
|
|
|
|
|
unawaited(_sembrarEcualizadorDesdeDisco(handler, leerEqActivoPersistido));
|
|
|
|
|
}
|
|
|
|
|
if (leerPresetPersistido != null) {
|
|
|
|
|
unawaited(_sembrarPresetDesdeDisco(handler, leerPresetPersistido));
|
|
|
|
|
}
|
|
|
|
|
// iap-freemium-unlock (design.md Open Questions, orchestrator-resolved),
|
|
|
|
|
// generalizado en fix/android-auto-musica-local item 4: invalida
|
|
|
|
|
// activamente todo id de nivel raíz que un head unit pueda tener cacheado
|
|
|
|
@@ -1057,7 +1113,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
AndroidEqualizer _eq = AndroidEqualizer();
|
|
|
|
|
AndroidEqualizer _eq = _crearEq();
|
|
|
|
|
late AudioPlayer _player = _crearPlayer();
|
|
|
|
|
StreamSubscription<PlayerState>? _estadoPlayerSub;
|
|
|
|
|
StreamSubscription<Duration>? _bufferedSub;
|
|
|
|
@@ -1222,6 +1278,39 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|
|
|
|
bool _eqDisponible = false;
|
|
|
|
|
bool get ecualizadorDisponible => _eqDisponible;
|
|
|
|
|
|
|
|
|
|
/// Last [AndroidEqualizerParameters] resolved by [_activarEcualizador].
|
|
|
|
|
///
|
|
|
|
|
/// Cached rather than re-awaited because `AndroidEqualizer.parameters` is a
|
|
|
|
|
/// `Completer` future that only completes when the platform player attaches
|
|
|
|
|
/// (`just_audio.dart` `AndroidEqualizer._activate`). Awaiting it from a
|
|
|
|
|
/// toggle path therefore does not "read the device", it BLOCKS until the
|
|
|
|
|
/// next successful load — potentially forever if that load fails — which
|
|
|
|
|
/// would leave the car's equalizer button pending and its icon stale.
|
|
|
|
|
/// `null` means "not resolved yet on this player": the gains are skipped and
|
|
|
|
|
/// [_activarEcualizador] pushes them as soon as the player attaches.
|
|
|
|
|
AndroidEqualizerParameters? _paramsEq;
|
|
|
|
|
|
|
|
|
|
/// The [PasoEcualizador]s the LAST on/off transition actually executed, in
|
|
|
|
|
/// execution order. Reset at the start of every transition, so it stays
|
|
|
|
|
/// bounded and says exactly what the most recent toggle did.
|
|
|
|
|
///
|
|
|
|
|
/// This is the only way a test can see the order: both operations are
|
|
|
|
|
/// invisible off-device. Asserting "both happened" would have stayed green
|
|
|
|
|
/// against the very bug this exists for.
|
|
|
|
|
@visibleForTesting
|
|
|
|
|
List<PasoEcualizador> get pasosEcualizadorEjecutados =>
|
|
|
|
|
List.unmodifiable(_pasosEqEjecutados);
|
|
|
|
|
final _pasosEqEjecutados = <PasoEcualizador>[];
|
|
|
|
|
|
|
|
|
|
/// How many native equalizer calls have thrown.
|
|
|
|
|
///
|
|
|
|
|
/// The native effect is write-only (`just_audio` exposes no
|
|
|
|
|
/// `Equalizer.getEnabled()`), so a failure used to be indistinguishable
|
|
|
|
|
/// from success both in a logcat and in a test.
|
|
|
|
|
@visibleForTesting
|
|
|
|
|
int get fallosNativosEcualizador => _fallosNativosEq;
|
|
|
|
|
int _fallosNativosEq = 0;
|
|
|
|
|
|
|
|
|
|
/// The equalizer's on/off state — and, since eq-estado-unico, its SINGLE
|
|
|
|
|
/// in-memory owner. `EstadoEcualizador._activo` is now a pure display
|
|
|
|
|
/// mirror of this field, and `ServicioEcualizador` is its durable copy.
|
|
|
|
@@ -1401,6 +1490,36 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|
|
|
|
|
|
|
|
|
PresetEcualizador _presetActual = PresetEcualizador.flat;
|
|
|
|
|
PresetEcualizador get presetActual => _presetActual;
|
|
|
|
|
|
|
|
|
|
/// True once anybody has chosen a preset on this handler. Guards the disk
|
|
|
|
|
/// seed against clobbering a live choice — see [_sembrarPresetDesdeDisco].
|
|
|
|
|
bool _presetElegido = false;
|
|
|
|
|
|
|
|
|
|
/// The ordered native steps an on/off transition performs.
|
|
|
|
|
///
|
|
|
|
|
/// Pure and public so the ORDER is asserted directly.
|
|
|
|
|
@visibleForTesting
|
|
|
|
|
static List<PasoEcualizador> pasosEcualizador({required bool activo}) =>
|
|
|
|
|
activo
|
|
|
|
|
// GAINS FIRST. `AudioEffect.setEnabled(true)` re-activates the
|
|
|
|
|
// native `android.media.audiofx.Equalizer`, which still holds the
|
|
|
|
|
// band levels the PREVIOUS preset left in it — so enabling first
|
|
|
|
|
// means the driver hears the old equalization and then, one native
|
|
|
|
|
// round trip per band, the new one sliding in over it. That is the
|
|
|
|
|
// «doubled equalization» the owner reports from the car. Writing
|
|
|
|
|
// the levels while the effect is still bypassed makes the
|
|
|
|
|
// transition a single audible step.
|
|
|
|
|
? const [PasoEcualizador.ganancias, PasoEcualizador.habilitacion]
|
|
|
|
|
// DISABLING DOES NOT RESET THE GAINS, on purpose.
|
|
|
|
|
// `AudioEffect.setEnabled(false)` (just_audio's
|
|
|
|
|
// `AudioPlayer.java:820-822` → `AudioEffect.setEnabled`) BYPASSES
|
|
|
|
|
// the effect; it neither releases it nor clears its band levels,
|
|
|
|
|
// and a bypassed effect is inaudible whatever they hold. Zeroing
|
|
|
|
|
// them would be one `setBandLevel` IPC per band for no audible
|
|
|
|
|
// difference, and the enable path above rewrites them all before
|
|
|
|
|
// re-enabling anyway — so there is no stale-gain window left for a
|
|
|
|
|
// reset to close.
|
|
|
|
|
: const [PasoEcualizador.habilitacion];
|
|
|
|
|
int? get androidAudioSessionId => _androidAudioSessionId;
|
|
|
|
|
Stream<int?> get androidAudioSessionIdStream =>
|
|
|
|
|
_androidAudioSessionIdController.stream;
|
|
|
|
@@ -1575,6 +1694,21 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|
|
|
|
)?
|
|
|
|
|
fabricaReproductorPrueba;
|
|
|
|
|
|
|
|
|
|
/// Same seam as [fabricaReproductorPrueba], for the native equalizer effect.
|
|
|
|
|
///
|
|
|
|
|
/// `AudioEffect.setEnabled` is a silent no-op while the player is detached
|
|
|
|
|
/// (`just_audio.dart` gates it on `_player._active`), so off-device a
|
|
|
|
|
/// failing native equalizer cannot otherwise be simulated at all — which is
|
|
|
|
|
/// why the silent `catch (_) {}` on that path shipped with zero coverage.
|
|
|
|
|
/// Static for the same reason as [fabricaReproductorPrueba]: `_eq` is a
|
|
|
|
|
/// field initializer, so the factory must already be installed before
|
|
|
|
|
/// `PluriWaveAudioHandler()` runs. Tests clear it in `tearDown`.
|
|
|
|
|
@visibleForTesting
|
|
|
|
|
static AndroidEqualizer Function()? fabricaEcualizadorPrueba;
|
|
|
|
|
|
|
|
|
|
static AndroidEqualizer _crearEq() =>
|
|
|
|
|
fabricaEcualizadorPrueba?.call() ?? AndroidEqualizer();
|
|
|
|
|
|
|
|
|
|
AudioPlayer _crearPlayer() {
|
|
|
|
|
final pipeline = AudioPipeline(androidAudioEffects: [_eq]);
|
|
|
|
|
final fabrica = fabricaReproductorPrueba;
|
|
|
|
@@ -2330,8 +2464,31 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|
|
|
|
await anterior.dispose().timeout(_timeoutCierrePlayer);
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
|
|
|
|
|
_eq = AndroidEqualizer();
|
|
|
|
|
_eqDisponible = false;
|
|
|
|
|
_eq = _crearEq();
|
|
|
|
|
// `_eqDisponible` is deliberately NOT reset here. It answers "does this
|
|
|
|
|
// DEVICE have a usable native Equalizer effect", which no station change
|
|
|
|
|
// can alter — and resetting it on every source change is what made a car
|
|
|
|
|
// toggle land in a window where every native EQ path was gated off (the
|
|
|
|
|
// reported «does nothing») and made the EQ custom action disappear from
|
|
|
|
|
// the now-playing screen and come back seconds later
|
|
|
|
|
// (`controlesEcualizadorPersonalizados` returns `const []` when
|
|
|
|
|
// unavailable). [_activarEcualizador] is the only writer now: it sets it
|
|
|
|
|
// true when the fresh effect reports bands, false when it throws.
|
|
|
|
|
//
|
|
|
|
|
// Keeping it true across the rebuild cannot lie or throw, and that was
|
|
|
|
|
// verified against just_audio 0.9.46 rather than assumed:
|
|
|
|
|
// - `AudioEffect.setEnabled` short-circuits on `_player._active`, so on
|
|
|
|
|
// the detached fresh player it only records the Dart-side intent and
|
|
|
|
|
// never reaches the platform — no throw, no native call.
|
|
|
|
|
// - that recorded intent is NOT lost: the effect's `_toMessage()` is
|
|
|
|
|
// only read when the player attaches (`AudioPlayer._setPlatformActive`
|
|
|
|
|
// → `InitRequest.androidAudioEffects`), so a toggle made inside this
|
|
|
|
|
// window is carried into the new native pipeline verbatim.
|
|
|
|
|
// - the one call that WOULD hang is `await AndroidEqualizer.parameters`:
|
|
|
|
|
// its `Completer` only completes in `_activate`, i.e. when the player
|
|
|
|
|
// attaches. No toggle path awaits it any more — they read the
|
|
|
|
|
// [_paramsEq] cache cleared just below and skip while it is null.
|
|
|
|
|
_paramsEq = null;
|
|
|
|
|
// Resets alongside its siblings above: the fresh player starts detached,
|
|
|
|
|
// so the next non-idle event is a genuine idle -> active edge that
|
|
|
|
|
// [debeReasertarEcualizadorNativo] must see. A value stuck at `true`
|
|
|
|
@@ -2403,8 +2560,8 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|
|
|
|
'activo=$_ecualizadorActivo preset=${_presetActual.nombre}',
|
|
|
|
|
);
|
|
|
|
|
if (_eqDisponible) {
|
|
|
|
|
await _eq.setEnabled(_ecualizadorActivo);
|
|
|
|
|
await aplicarPreset(_presetActual);
|
|
|
|
|
_paramsEq = params;
|
|
|
|
|
await _conmutarEcualizadorNativo(_ecualizadorActivo);
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {
|
|
|
|
|
_eqDisponible = false;
|
|
|
|
@@ -2484,26 +2641,27 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|
|
|
|
/// Aplica un preset al ecualizador nativo Android.
|
|
|
|
|
Future<void> aplicarPreset(PresetEcualizador preset) async {
|
|
|
|
|
_presetActual = preset;
|
|
|
|
|
// A preset chosen by anyone (car folder, phone screen) claims ownership:
|
|
|
|
|
// a disk seed still in flight must not overwrite it. See
|
|
|
|
|
// [_sembrarPresetDesdeDisco].
|
|
|
|
|
_presetElegido = true;
|
|
|
|
|
if (_eqDisponible) {
|
|
|
|
|
try {
|
|
|
|
|
// Enable-then-gains here does NOT contradict [pasosEcualizador]'s
|
|
|
|
|
// gains-then-enable. That order matters only on an on/off TRANSITION,
|
|
|
|
|
// where enabling first un-bypasses an effect still holding the
|
|
|
|
|
// previous preset. Choosing a preset is not a transition: the effect
|
|
|
|
|
// is already in its final on/off state, so this `setEnabled` is the
|
|
|
|
|
// idempotent re-assert that keeps the native effect honest after a
|
|
|
|
|
// `stop()` (see [debeReasertarEcualizadorNativo]) and opens no
|
|
|
|
|
// stale-gain window of its own.
|
|
|
|
|
await _eq.setEnabled(_ecualizadorActivo);
|
|
|
|
|
if (_ecualizadorActivo) {
|
|
|
|
|
final params = await _eq.parameters;
|
|
|
|
|
for (
|
|
|
|
|
int i = 0;
|
|
|
|
|
i < params.bands.length && i < preset.bandas.length;
|
|
|
|
|
i++
|
|
|
|
|
) {
|
|
|
|
|
await params.bands[i].setGain(
|
|
|
|
|
mapearGananciaNativa(
|
|
|
|
|
preset.bandas[i],
|
|
|
|
|
minDecibels: params.minDecibels,
|
|
|
|
|
maxDecibels: params.maxDecibels,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
await _empujarGananciasNativas(preset);
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
_registrarFalloEq('aplicarPreset(${preset.nombre})', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Item 4: keeps the EQ custom action's preset-cycle label in sync
|
|
|
|
|
// regardless of WHO changed the preset (a car customAction tap or the
|
|
|
|
@@ -2518,9 +2676,11 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|
|
|
|
bandas[index] = db;
|
|
|
|
|
_presetActual = _presetActual.copyWithBandas(bandas);
|
|
|
|
|
}
|
|
|
|
|
_presetElegido = true;
|
|
|
|
|
if (!_eqDisponible || !_ecualizadorActivo) return;
|
|
|
|
|
final params = _paramsEq;
|
|
|
|
|
if (params == null) return;
|
|
|
|
|
try {
|
|
|
|
|
final params = await _eq.parameters;
|
|
|
|
|
if (index < params.bands.length) {
|
|
|
|
|
await params.bands[index].setGain(
|
|
|
|
|
mapearGananciaNativa(
|
|
|
|
@@ -2530,7 +2690,68 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
_registrarFalloEq('setBanda($index)', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Writes [preset]'s band levels into the native effect.
|
|
|
|
|
///
|
|
|
|
|
/// Skips silently while [_paramsEq] is `null` (the player has not attached
|
|
|
|
|
/// since the last rebuild): the gains have nowhere to go yet and
|
|
|
|
|
/// [_activarEcualizador] pushes them the moment it does.
|
|
|
|
|
Future<void> _empujarGananciasNativas(PresetEcualizador preset) async {
|
|
|
|
|
final params = _paramsEq;
|
|
|
|
|
if (params == null) return;
|
|
|
|
|
for (int i = 0; i < params.bands.length && i < preset.bandas.length; i++) {
|
|
|
|
|
await params.bands[i].setGain(
|
|
|
|
|
mapearGananciaNativa(
|
|
|
|
|
preset.bandas[i],
|
|
|
|
|
minDecibels: params.minDecibels,
|
|
|
|
|
maxDecibels: params.maxDecibels,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The native operations an on/off transition performs, in
|
|
|
|
|
/// [pasosEcualizador] order.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `false` when the [PasoEcualizador.habilitacion] step itself
|
|
|
|
|
/// threw, i.e. when the device did NOT adopt [activo]. A failed gains step
|
|
|
|
|
/// does not make the transition dishonest: the effect really is in the
|
|
|
|
|
/// requested on/off state, just carrying stale band levels.
|
|
|
|
|
Future<bool> _conmutarEcualizadorNativo(bool activo) async {
|
|
|
|
|
_pasosEqEjecutados.clear();
|
|
|
|
|
var conmutado = true;
|
|
|
|
|
for (final paso in pasosEcualizador(activo: activo)) {
|
|
|
|
|
try {
|
|
|
|
|
switch (paso) {
|
|
|
|
|
case PasoEcualizador.ganancias:
|
|
|
|
|
await _empujarGananciasNativas(_presetActual);
|
|
|
|
|
case PasoEcualizador.habilitacion:
|
|
|
|
|
await _eq.setEnabled(activo);
|
|
|
|
|
}
|
|
|
|
|
_pasosEqEjecutados.add(paso);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
_registrarFalloEq('$paso(activo=$activo)', e);
|
|
|
|
|
if (paso == PasoEcualizador.habilitacion) conmutado = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return conmutado;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Single trace/count point for every native equalizer failure.
|
|
|
|
|
///
|
|
|
|
|
/// [debugPrint] and never `dart:developer`'s `log`, for the same reason as
|
|
|
|
|
/// the rest of this file: `log()` writes to the VM service, which the
|
|
|
|
|
/// RELEASE build a car runs does not have.
|
|
|
|
|
void _registrarFalloEq(String operacion, Object error) {
|
|
|
|
|
_fallosNativosEq++;
|
|
|
|
|
debugPrint(
|
|
|
|
|
'[PluriWave][ServicioAudio] fallo nativo del ecualizador en '
|
|
|
|
|
'$operacion: $error',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Sets the equalizer on/off state AND persists it — the single entry
|
|
|
|
@@ -2539,6 +2760,24 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|
|
|
|
Future<void> setEcualizadorActivo(bool activo) =>
|
|
|
|
|
_aplicarEcualizadorActivo(activo, persistir: true);
|
|
|
|
|
|
|
|
|
|
/// Adopts a PRESET that came from disk, the sibling of
|
|
|
|
|
/// [sembrarEcualizadorActivo]. Bound through
|
|
|
|
|
/// `registrarHandler(leerPresetPersistido: ...)`.
|
|
|
|
|
///
|
|
|
|
|
/// Unlike the on/off flag's seed this one YIELDS to a live choice. The flag
|
|
|
|
|
/// has exactly one persisted value and the handler owns writing it, so
|
|
|
|
|
/// seeding it can never contradict anybody. The preset does not: the phone
|
|
|
|
|
/// UI resolves a richer value (per-station, and per-Bluetooth-device when
|
|
|
|
|
/// the multi-device toggle is on) that this narrow "principal preset" read
|
|
|
|
|
/// knows nothing about. The seed's disk read is `unawaited`, so without the
|
|
|
|
|
/// [_presetElegido] guard a slow read could land after `EstadoEcualizador`
|
|
|
|
|
/// had already pushed the right preset and silently replace it with the
|
|
|
|
|
/// principal one. The seed exists to fill a VOID, never to overrule.
|
|
|
|
|
Future<void> sembrarPresetEcualizador(PresetEcualizador preset) async {
|
|
|
|
|
if (_presetElegido) return;
|
|
|
|
|
await aplicarPreset(preset);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Adopts a value that came FROM disk (eq-estado-unico item A). Identical
|
|
|
|
|
/// to [setEcualizadorActivo] except that it does not write back — seeding
|
|
|
|
|
/// is a read, and echoing it to disk would only add a pointless write on
|
|
|
|
@@ -2550,14 +2789,19 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|
|
|
|
bool activo, {
|
|
|
|
|
required bool persistir,
|
|
|
|
|
}) async {
|
|
|
|
|
final anterior = _ecualizadorActivo;
|
|
|
|
|
_ecualizadorActivo = activo;
|
|
|
|
|
if (_eqDisponible) {
|
|
|
|
|
try {
|
|
|
|
|
await _eq.setEnabled(activo);
|
|
|
|
|
if (activo) {
|
|
|
|
|
await aplicarPreset(_presetActual);
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
if (_eqDisponible && !await _conmutarEcualizadorNativo(activo)) {
|
|
|
|
|
// The device REFUSED the on/off call. Publishing `activo` anyway would
|
|
|
|
|
// put an icon on the car's now-playing screen claiming a state the
|
|
|
|
|
// audio does not have — and persisting it would resurrect that lie on
|
|
|
|
|
// the next engine start. Rolling back is cheap here because
|
|
|
|
|
// `_ecualizadorActivo` is the single in-memory owner (eq-estado-unico)
|
|
|
|
|
// and the controls are rebuilt from it one line below; the toggle then
|
|
|
|
|
// honestly reads "unchanged" and the failure is in the logcat.
|
|
|
|
|
_ecualizadorActivo = anterior;
|
|
|
|
|
_actualizarControlesEq();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Item 4: keeps the EQ custom action's on/off label in sync regardless
|
|
|
|
|
// of WHO toggled it (a car customAction tap or the phone settings
|
|
|
|
|