refactor(auto): drop the in-car equalizer, keep EQ on the phone
The car tree carried a whole equalizer feature: an `Ecualizador` root folder with the six factory presets, a browsable `Personalizado` folder, five band folders and thirteen gain leaves each, plus the media-id namespaces, routing predicates, persistence-targeting and children-changed plumbing that fed them. Equalization is a phone task; the driver should not be tuning 5 bands from a car screen. Removed: the `eq_preset:`/`eq_banda:`/`eq_gain:` namespaces and their predicates and parsers, the `ecualizador`/`eq_custom` folder ids and their getChildren branches, itemPresetEq/presetsEq/itemEqPersonalizado/bandasEq/ gananciasBandaEq, resolverPresetEq, presetPersonalizadoEfectivo, dispositivoDestinoEq, debeAplicarPrincipalAhora/debeAplicarSeleccionAhora, aplicarPresetPorMediaId, aplicarGananciaPorMediaId, and in the handler the playFromMediaId branches, _presetPersonalizadoAuto, _dispositivoActivoAuto, _dispositivoDestinoEqAuto and the subscribeToChildren/_hijosSubjects notification machinery that existed only to refresh band titles after a gain tap. Deliberately KEPT: automatic per-device EQ. Reaching the car still applies that device's preset, because that lives in EstadoEcualizador and the output-device detection, not in this tree — it works with Android Auto or without it. Configuring is what moves to the phone; applying stays automatic. Also kept: the `eq_preset_*_v1` SharedPreferences keys in ServicioEcualizador, which share a name with the deleted media-id prefix by coincidence only and hold the phone's own presets. The root folder set goes from five entries to four (three without local music); its test now asserts no equalizer folder is offered at all, so a reintroduction has to be deliberate.
This commit is contained in:
@@ -4,13 +4,10 @@ import 'dart:ui' show Locale;
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/foundation.dart' show visibleForTesting;
|
||||
import 'package:flutter/services.dart' show MethodChannel;
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:rxdart/rxdart.dart' show BehaviorSubject, ValueStream;
|
||||
|
||||
import '../l10n/display_names.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../modelos/dispositivo_audio.dart';
|
||||
import '../modelos/emisora.dart';
|
||||
import '../modelos/pista_local.dart';
|
||||
import '../modelos/preset_ecualizador.dart';
|
||||
@@ -19,8 +16,6 @@ import 'controlador_reconexion.dart';
|
||||
import 'musica_local_auto.dart';
|
||||
import 'navegacion_auto.dart';
|
||||
import 'servicio_audio_session.dart';
|
||||
import 'servicio_dispositivo_audio.dart';
|
||||
import 'servicio_ecualizador.dart';
|
||||
|
||||
/// Estado de reproducción expuesto al UI.
|
||||
enum EstadoReproduccion {
|
||||
@@ -494,7 +489,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
}
|
||||
|
||||
/// Public entry point for EVERY external play (phone `reproducir`, car
|
||||
/// `emisora:`/`grupo:`/`pista:`/`eq_preset:` non-path). ALWAYS clears the
|
||||
/// `emisora:`/`grupo:`/`pista:` non-path). ALWAYS clears the
|
||||
/// local queue FIRST (Design ADR-2, the single load-bearing invariant:
|
||||
/// "external play = leave queue mode") so a stale auto-advance can never
|
||||
/// fire after an external source switch, then delegates to the private
|
||||
@@ -913,10 +908,6 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
await _androidAudioSessionIdSub?.cancel();
|
||||
await _player.dispose();
|
||||
await _androidAudioSessionIdController.close();
|
||||
for (final subject in _hijosSubjects.values) {
|
||||
await subject.close();
|
||||
}
|
||||
_hijosSubjects.clear();
|
||||
}
|
||||
|
||||
Emisora _emisoraDesdeMediaItem(MediaItem mediaItem) {
|
||||
@@ -937,88 +928,6 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
/// method channel `ServicioDispositivoAudioReal` talks to, but method
|
||||
/// calls only — opening a second EventChannel subscription here would
|
||||
/// steal the phone-side service's Dart stream handler.
|
||||
static const _canalDispositivos = MethodChannel('pluriwave/audio_devices');
|
||||
|
||||
/// Short timeout for the device query: on a headless Auto bind no
|
||||
/// Activity (and thus no channel handler) exists, and a car tap must fall
|
||||
/// back to global persistence instead of hanging.
|
||||
static const _timeoutConsultaDispositivo = Duration(seconds: 2);
|
||||
|
||||
/// Fresh active-output-device query for the car EQ paths. Returns `null`
|
||||
/// on ANY failure (missing handler while headless, timeout, malformed
|
||||
/// map) so callers degrade to global persistence — never a crash.
|
||||
Future<DispositivoAudio?> _dispositivoActivoAuto() async {
|
||||
try {
|
||||
final raw = await _canalDispositivos
|
||||
.invokeMethod<Map<dynamic, dynamic>>('getActiveDevice')
|
||||
.timeout(_timeoutConsultaDispositivo);
|
||||
if (raw == null) return null;
|
||||
return ServicioDispositivoAudioReal.dispositivoDesdeMapa(
|
||||
Map<String, dynamic>.from(raw),
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the persistence target for a car EQ action (feature
|
||||
/// auto-custom-eq): a deviceId for a DEVICE-level write, `null` for the
|
||||
/// global principal (toggle off, built-in speaker, placeholder id, or the
|
||||
/// headless error/timeout fallback).
|
||||
Future<String?> _dispositivoDestinoEqAuto(
|
||||
ServicioEcualizador servicio,
|
||||
) async {
|
||||
try {
|
||||
final config = await servicio.cargar();
|
||||
if (!config.eqMultiDeviceEnabled) return null;
|
||||
return dispositivoDestinoEq(
|
||||
multiDeviceEnabled: config.eqMultiDeviceEnabled,
|
||||
dispositivo: await _dispositivoActivoAuto(),
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// The custom preset the Auto tree shows and edits right now (feature
|
||||
/// auto-custom-eq): the device-level entry for the current output device
|
||||
/// when multi-device is on, the global principal otherwise — resolved
|
||||
/// from persistence so a headless bind (no `EstadoEcualizador`) still
|
||||
/// reports honest gains.
|
||||
Future<PresetEcualizador> _presetPersonalizadoAuto() async {
|
||||
final servicio = ServicioEcualizador();
|
||||
final config = await servicio.cargar();
|
||||
final destino =
|
||||
config.eqMultiDeviceEnabled
|
||||
? dispositivoDestinoEq(
|
||||
multiDeviceEnabled: true,
|
||||
dispositivo: await _dispositivoActivoAuto(),
|
||||
)
|
||||
: null;
|
||||
return presetPersonalizadoEfectivo(config: config, deviceId: destino);
|
||||
}
|
||||
|
||||
/// Per-parent children-changed subjects (feature auto-custom-eq):
|
||||
/// audio_service subscribes to [subscribeToChildren]'s stream the first
|
||||
/// time the platform loads a parent's children and translates every later
|
||||
/// emission into a native `notifyChildrenChanged`, making the car
|
||||
/// re-request `getChildren` so band titles and the selection mark refresh
|
||||
/// right after a gain tap.
|
||||
final Map<String, BehaviorSubject<Map<String, dynamic>>> _hijosSubjects = {};
|
||||
|
||||
@override
|
||||
ValueStream<Map<String, dynamic>> subscribeToChildren(String parentMediaId) =>
|
||||
_hijosSubjects.putIfAbsent(
|
||||
parentMediaId,
|
||||
() => BehaviorSubject.seeded(<String, dynamic>{}),
|
||||
);
|
||||
|
||||
/// Emits a children-changed notification for [parentMediaId] — a no-op
|
||||
/// until the platform has browsed that parent at least once.
|
||||
void _notificarHijosCambiados(String parentMediaId) {
|
||||
_hijosSubjects[parentMediaId]?.add(<String, dynamic>{});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MediaItem>> getChildren(
|
||||
String parentMediaId, [
|
||||
@@ -1032,23 +941,6 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
fuenteLocal != null && await fuenteLocal.hayCarpetaConfigurada();
|
||||
return constructor.raiz(incluirMusicaLocal: incluirMusicaLocal);
|
||||
}
|
||||
if (parentMediaId == ConstructorArbolAuto.idEcualizador) {
|
||||
return [
|
||||
...constructor.presetsEq(PresetEcualizador.presets),
|
||||
constructor.itemEqPersonalizado(),
|
||||
];
|
||||
}
|
||||
if (parentMediaId == ConstructorArbolAuto.idEqPersonalizado) {
|
||||
return constructor.bandasEq(await _presetPersonalizadoAuto());
|
||||
}
|
||||
if (esBandaEqMediaId(parentMediaId)) {
|
||||
final indice = indiceBandaEqDesde(parentMediaId);
|
||||
if (indice == null) return const [];
|
||||
return constructor.gananciasBandaEq(
|
||||
indice,
|
||||
await _presetPersonalizadoAuto(),
|
||||
);
|
||||
}
|
||||
final musicaLocal = await hijosMusicaLocal(
|
||||
parentMediaId,
|
||||
fuente: fuenteLocal,
|
||||
@@ -1097,65 +989,10 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
Map<String, dynamic>? extras,
|
||||
]) async {
|
||||
try {
|
||||
// EQ preset selection (Design ADR-3, Spec "EQ Preset Selection Applies
|
||||
// Without Disturbing Playback"): FIRST branch, unconditional `return`,
|
||||
// so an `eq_preset:` id can never fall through to the playback routing
|
||||
// below. `aplicarPresetPorMediaId`'s seams are EQ-only (persist +
|
||||
// apply) — there is no playback parameter to inject here.
|
||||
if (esPresetMediaId(mediaId)) {
|
||||
final servicio = ServicioEcualizador();
|
||||
// Persistence targeting (feature auto-custom-eq): with multi-device
|
||||
// EQ on and a non-builtin output device active, the tap persists a
|
||||
// DEVICE-level entry so the selection sticks for the car's device
|
||||
// instead of being shadowed by the hierarchy; otherwise (or on any
|
||||
// headless query failure) it persists the global principal as
|
||||
// before.
|
||||
await aplicarPresetPorMediaId(
|
||||
mediaId,
|
||||
presets: PresetEcualizador.presets,
|
||||
uuidActual: emisoraActual?.uuid,
|
||||
clavesPorEmisora: () async =>
|
||||
(await servicio.cargar()).porEmisora.keys.toSet(),
|
||||
clavesMatriz: () async =>
|
||||
(await servicio.cargar()).presetsMatriz.keys.toSet(),
|
||||
dispositivoDestino: () => _dispositivoDestinoEqAuto(servicio),
|
||||
persistirDispositivo: servicio.guardarPresetDispositivo,
|
||||
persistirPrincipal: servicio.guardarPrincipal,
|
||||
aplicar: aplicarPreset,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Custom-EQ gain selection (feature auto-custom-eq): same
|
||||
// unconditional-return shape as the eq_preset branch above — an
|
||||
// `eq_gain:` id can never fall through to playback routing.
|
||||
if (esGananciaEqMediaId(mediaId)) {
|
||||
final servicio = ServicioEcualizador();
|
||||
await aplicarGananciaPorMediaId(
|
||||
mediaId,
|
||||
cargarConfig: servicio.cargar,
|
||||
dispositivoDestino: () => _dispositivoDestinoEqAuto(servicio),
|
||||
persistirDispositivo: servicio.guardarPresetDispositivo,
|
||||
persistirPrincipal: servicio.guardarPrincipal,
|
||||
aplicarBanda: setBanda,
|
||||
uuidActual: emisoraActual?.uuid,
|
||||
clavesPorEmisora: () async =>
|
||||
(await servicio.cargar()).porEmisora.keys.toSet(),
|
||||
clavesMatriz: () async =>
|
||||
(await servicio.cargar()).presetsMatriz.keys.toSet(),
|
||||
);
|
||||
// Refresh the affected browse nodes so the band title under
|
||||
// `Personalizado` and the `● ` selection mark reflect the new gain.
|
||||
final ganancia = gananciaEqDesde(mediaId);
|
||||
if (ganancia != null) {
|
||||
_notificarHijosCambiados(ConstructorArbolAuto.idEqPersonalizado);
|
||||
_notificarHijosCambiados(idBandaEq(ganancia.$1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Local-track playback (Design "Local Track Playback Reuses Existing
|
||||
// Pipeline", Spec "User selects a local track"): SECOND branch,
|
||||
// unconditional `return`, mirroring the eq_preset branch above — a
|
||||
// `pista:` id never falls through to the station routing below.
|
||||
// Pipeline", Spec "User selects a local track"): FIRST branch,
|
||||
// unconditional `return` — a `pista:` id never falls through to the
|
||||
// station routing below.
|
||||
if (esPistaMediaId(mediaId)) {
|
||||
final fuenteLocal = _fuenteMusicaLocalGlobal;
|
||||
if (fuenteLocal == null) return;
|
||||
@@ -1166,10 +1003,10 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Folder-play actions (Design ADR-5, Phase 3 task 4.3): THIRD
|
||||
// branch, after eq_preset/pista, before the station fallthrough —
|
||||
// mirrors both branches above's unconditional-return shape so
|
||||
// neither new action id can fall through to station routing.
|
||||
// Folder-play actions (Design ADR-5, Phase 3 task 4.3): SECOND
|
||||
// branch, after `pista:`, before the station fallthrough — mirrors the
|
||||
// branch above's unconditional-return shape so neither new action id
|
||||
// can fall through to station routing.
|
||||
final constructorArbol = ConstructorArbolAuto();
|
||||
final esAccionCarpeta =
|
||||
constructorArbol.esCarpetaLocalReproducirMediaId(mediaId) ||
|
||||
|
||||
Reference in New Issue
Block a user