feat(auto): equalizer enable/disable and preset cycling from the car
Expose the equalizer's on/off toggle and preset choice as PlaybackStateCompat
custom actions on the now-playing screen. The redesign's removal of the
in-car equalizer FOLDER from the browse tree stays as-is (2403da3) -- this
is a different surface (playback screen custom actions, not a browse
folder) and does not reintroduce it.
Deliberately just 2 actions -- an on/off toggle plus a cycling preset
action, not one action per preset -- since Android Auto only surfaces a
limited number of custom actions. Both reuse the existing
setEcualizadorActivo/aplicarPreset entry points (the same ones
EstadoEcualizador's phone settings screen uses), so a car tap and a phone
tap behave identically and both keep the action labels in sync. Reuses the
bundled ic_stat_pluriwave drawable (the notification's own equalizer-bars
icon) -- zero new native assets. The 5-band constraint is untouched.
New pure, unit-tested functions in servicio_audio.dart: presetSiguiente,
nombrePresetVisible, controlesEcualizadorPersonalizados. New ARB keys
(eqCustomActionEnableLabel/DisableLabel/PresetLabel) across all 13 locales,
regenerated via flutter gen-l10n.
This commit is contained in:
@@ -100,6 +100,91 @@ Emisora emisoraDesdeMediaItem(MediaItem mediaItem) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Custom-action names for the equalizer's `PlaybackStateCompat` custom
|
||||
/// actions on the now-playing screen (Design "EQ custom actions", item 4).
|
||||
/// Public consts so tests and this file's own `customAction` dispatch share
|
||||
/// the exact same literals; distinct from every browse-tree media-id prefix
|
||||
/// in `navegacion_auto.dart` (they live in a completely different
|
||||
/// `MediaControl`/`customAction` namespace, never compared against a
|
||||
/// media id).
|
||||
const accionEqToggle = 'eq_toggle';
|
||||
const accionEqPresetSiguiente = 'eq_preset_siguiente';
|
||||
|
||||
/// Advances to the NEXT factory preset after [actual] in [presets] order
|
||||
/// (Design "EQ custom actions — cycling presets", item 4): wraps around
|
||||
/// after the last one. When [actual] is not found in [presets] (e.g. a
|
||||
/// user-tweaked "Personalizado" preset from `EstadoEcualizador.cambiarBanda`),
|
||||
/// starts from the FIRST preset rather than throwing — cycling from an
|
||||
/// unknown state always lands somewhere sane. Pure, no I/O.
|
||||
///
|
||||
/// [presets] defaults to [PresetEcualizador.presets] — not a literal default
|
||||
/// value, since that field is `static final` (not `const`) and Dart default
|
||||
/// parameter values must be compile-time constants.
|
||||
PresetEcualizador presetSiguiente(
|
||||
PresetEcualizador actual, {
|
||||
List<PresetEcualizador>? presets,
|
||||
}) {
|
||||
final lista = presets ?? PresetEcualizador.presets;
|
||||
final indice = lista.indexWhere((p) => p == actual);
|
||||
if (indice == -1) return lista.first;
|
||||
return lista[(indice + 1) % lista.length];
|
||||
}
|
||||
|
||||
/// Localizes a preset's raw `nombre` for the equalizer custom action's
|
||||
/// label (Design "EQ custom actions", item 4) — mirrors
|
||||
/// `ecualizador_widget.dart`'s private `_nombrePreset` mapping (duplicated
|
||||
/// rather than shared: that file is UI-widget layer, this one is the
|
||||
/// service/handler layer, and the mapping is a single small switch, not
|
||||
/// worth a cross-layer import for). An unrecognized name (e.g. a future
|
||||
/// user-named custom preset) falls through to the raw name verbatim.
|
||||
String nombrePresetVisible(AppLocalizations l10n, String nombre) {
|
||||
return switch (nombre) {
|
||||
'Flat' => l10n.equalizerPresetFlat,
|
||||
'Rock' => l10n.equalizerPresetRock,
|
||||
'Pop' => l10n.equalizerPresetPop,
|
||||
'Bass Boost' => l10n.equalizerPresetBassBoost,
|
||||
'Jazz' => l10n.equalizerPresetJazz,
|
||||
'Voz' => l10n.equalizerPresetVoice,
|
||||
'Personalizado' => l10n.equalizerPresetCustom,
|
||||
_ => nombre,
|
||||
};
|
||||
}
|
||||
|
||||
/// Builds the equalizer's custom-action `MediaControl`s for the now-playing
|
||||
/// screen (Design "EQ custom actions", item 4) — deliberately just 2: an
|
||||
/// on/off toggle plus a cycling-preset action, NOT one action per preset,
|
||||
/// since Android Auto only surfaces a limited number of custom actions.
|
||||
/// Empty when [disponible] is false (gate on EQ availability, mirrors the
|
||||
/// existing `debeReaplicarEcualizador`/`_eqDisponible` gate) — a device
|
||||
/// without the native Equalizer effect gets no EQ actions at all, not
|
||||
/// broken ones. Reuses the SAME bundled `ic_stat_pluriwave` drawable the
|
||||
/// notification's own status-bar icon already uses (an equalizer-bars
|
||||
/// glyph) — zero new native assets. Pure, no handler dependency.
|
||||
List<MediaControl> controlesEcualizadorPersonalizados({
|
||||
required bool disponible,
|
||||
required bool activo,
|
||||
required PresetEcualizador presetActual,
|
||||
required AppLocalizations l10n,
|
||||
}) {
|
||||
if (!disponible) return const [];
|
||||
return [
|
||||
MediaControl.custom(
|
||||
androidIcon: 'drawable/ic_stat_pluriwave',
|
||||
label: activo
|
||||
? l10n.eqCustomActionDisableLabel
|
||||
: l10n.eqCustomActionEnableLabel,
|
||||
name: accionEqToggle,
|
||||
),
|
||||
MediaControl.custom(
|
||||
androidIcon: 'drawable/ic_stat_pluriwave',
|
||||
label: l10n.eqCustomActionPresetLabel(
|
||||
nombrePresetVisible(l10n, presetActual.nombre),
|
||||
),
|
||||
name: accionEqPresetSiguiente,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Wrapper de alto nivel para el UI.
|
||||
class ServicioAudio {
|
||||
PluriWaveAudioHandler get _handler {
|
||||
@@ -319,12 +404,10 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
final colaActiva = _colaLocal != null;
|
||||
playbackState.add(
|
||||
playbackState.value.copyWith(
|
||||
controls: [
|
||||
if (colaActiva) MediaControl.skipToPrevious,
|
||||
if (playing) MediaControl.pause else MediaControl.play,
|
||||
MediaControl.stop,
|
||||
if (colaActiva) MediaControl.skipToNext,
|
||||
],
|
||||
controls: _controlesTransporte(
|
||||
colaActiva: colaActiva,
|
||||
playing: playing,
|
||||
),
|
||||
systemActions: {
|
||||
MediaAction.seek,
|
||||
MediaAction.stop,
|
||||
@@ -369,6 +452,49 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
});
|
||||
}
|
||||
|
||||
/// The full transport `controls` list for a `playbackState` push (item 4):
|
||||
/// the existing skip/play-pause/stop set, plus the equalizer's custom
|
||||
/// actions appended at the end. Appending (rather than interleaving) keeps
|
||||
/// [MediaControl.skipToPrevious]/play-pause/stop/[MediaControl.skipToNext]
|
||||
/// at their existing indices 0-3, so `androidCompactActionIndices`
|
||||
/// (`[colaActiva ? 1 : 0]`) stays correct unchanged.
|
||||
List<MediaControl> _controlesTransporte({
|
||||
required bool colaActiva,
|
||||
required bool playing,
|
||||
}) => [
|
||||
if (colaActiva) MediaControl.skipToPrevious,
|
||||
if (playing) MediaControl.pause else MediaControl.play,
|
||||
MediaControl.stop,
|
||||
if (colaActiva) MediaControl.skipToNext,
|
||||
..._controlesEqPersonalizados(),
|
||||
];
|
||||
|
||||
List<MediaControl> _controlesEqPersonalizados() =>
|
||||
controlesEcualizadorPersonalizados(
|
||||
disponible: _eqDisponible,
|
||||
activo: _ecualizadorActivo,
|
||||
presetActual: _presetActual,
|
||||
l10n: _textos,
|
||||
);
|
||||
|
||||
/// Re-pushes `playbackState` with a freshly built controls list (item 4):
|
||||
/// called whenever EQ availability/enabled/preset state changes outside a
|
||||
/// player-state transition (a custom-action tap, or a phone-side preset/
|
||||
/// toggle change), so the equalizer custom actions' label and current-
|
||||
/// preset name stay in sync on the now-playing screen without waiting for
|
||||
/// an unrelated player event. Idempotent and cheap (no native calls) —
|
||||
/// safe to call from any EQ state-changing path.
|
||||
void _actualizarControlesEq() {
|
||||
playbackState.add(
|
||||
playbackState.value.copyWith(
|
||||
controls: _controlesTransporte(
|
||||
colaActiva: _colaLocal != null,
|
||||
playing: playbackState.value.playing,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Gestiona cualquier error de reproducción de ExoPlayer.
|
||||
///
|
||||
/// Network-class failures while the user still intends to play enter the
|
||||
@@ -754,6 +880,11 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
} catch (_) {
|
||||
_eqDisponible = false;
|
||||
}
|
||||
// Item 4: an availability flip (e.g. a station switch that lands on a
|
||||
// device without the native Equalizer effect) must show/hide the EQ
|
||||
// custom actions immediately, not wait for a coincidental later
|
||||
// player-state event.
|
||||
_actualizarControlesEq();
|
||||
}
|
||||
|
||||
/// Pure re-apply decision for a native session-id emission. No side effects.
|
||||
@@ -772,25 +903,31 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
/// Aplica un preset al ecualizador nativo Android.
|
||||
Future<void> aplicarPreset(PresetEcualizador preset) async {
|
||||
_presetActual = preset;
|
||||
if (!_eqDisponible) return;
|
||||
try {
|
||||
await _eq.setEnabled(_ecualizadorActivo);
|
||||
if (!_ecualizadorActivo) return;
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
if (_eqDisponible) {
|
||||
try {
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
// 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
|
||||
// phone settings screen via EstadoEcualizador) — single chokepoint.
|
||||
_actualizarControlesEq();
|
||||
}
|
||||
|
||||
/// Ajusta una banda individual.
|
||||
@@ -826,13 +963,18 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
|
||||
Future<void> setEcualizadorActivo(bool activo) async {
|
||||
_ecualizadorActivo = activo;
|
||||
if (!_eqDisponible) return;
|
||||
try {
|
||||
await _eq.setEnabled(activo);
|
||||
if (activo) {
|
||||
await aplicarPreset(_presetActual);
|
||||
}
|
||||
} catch (_) {}
|
||||
if (_eqDisponible) {
|
||||
try {
|
||||
await _eq.setEnabled(activo);
|
||||
if (activo) {
|
||||
await aplicarPreset(_presetActual);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
// 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
|
||||
// screen via EstadoEcualizador).
|
||||
_actualizarControlesEq();
|
||||
}
|
||||
|
||||
Future<void> setVolumen(double vol) async {
|
||||
@@ -929,6 +1071,27 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
await _reproducirEntradaCola(anterior.actual);
|
||||
}
|
||||
|
||||
/// Dispatches the equalizer's 2 custom actions (item 4, Design "EQ custom
|
||||
/// actions"): `accionEqToggle` flips on/off, `accionEqPresetSiguiente`
|
||||
/// cycles to the next factory preset. Both delegate to the existing
|
||||
/// [setEcualizadorActivo]/[aplicarPreset] — the SAME entry points the
|
||||
/// phone settings screen uses via `EstadoEcualizador` — so a car tap and a
|
||||
/// phone tap have identical effects and both refresh the custom action's
|
||||
/// label via `_actualizarControlesEq()` (already wired into those two
|
||||
/// methods). Any other [name] is a no-op — never throws.
|
||||
@override
|
||||
Future<dynamic> customAction(
|
||||
String name, [
|
||||
Map<String, dynamic>? extras,
|
||||
]) async {
|
||||
switch (name) {
|
||||
case accionEqToggle:
|
||||
await setEcualizadorActivo(!_ecualizadorActivo);
|
||||
case accionEqPresetSiguiente:
|
||||
await aplicarPreset(presetSiguiente(_presetActual));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onTaskRemoved() async {
|
||||
await stop();
|
||||
|
||||
Reference in New Issue
Block a user