feat(auto): add an Ecualizador browsable folder with preset selection
On-device feedback showed the equalizer's preset-cycling custom action
looked dead: many head units render custom actions icon-first, and a
monochrome icon cannot legibly encode "which of six presets" the way a
browsable list's text rows can.
This adds an "Ecualizador" folder to the car's browse tree, listing
"Desactivar" first, then the six factory presets by name, with the
currently-active one marked. Selecting a preset routes through the same
playFromMediaId seam every other browse-tree leaf already uses; picking
a preset while the equalizer is off turns it on and applies that preset.
Supersedes the earlier "no equalizer folder" rule (commit 2403da3),
which predated this feedback -- see decision auto/ecualizador-diseno.
The preset-cycling custom action still coexists with the folder in this
commit; it is removed in the next one.
This commit is contained in:
@@ -66,7 +66,10 @@ void registrarFuenteMusicaLocal(FuenteMusicaLocalAuto fuente) {
|
||||
/// the browse tree and the car-tap path already show, instead of a blank
|
||||
/// tile on the car/lockscreen/notification. Pure — no [PluriWaveAudioHandler]
|
||||
/// dependency — so it is unit-testable without instantiating the handler.
|
||||
MediaItem mediaItemParaEmisora(Emisora emisora, {required AppLocalizations l10n}) {
|
||||
MediaItem mediaItemParaEmisora(
|
||||
Emisora emisora, {
|
||||
required AppLocalizations l10n,
|
||||
}) {
|
||||
return MediaItem(
|
||||
id: emisora.url,
|
||||
title: localizedStationName(l10n, emisora.nombre),
|
||||
@@ -180,12 +183,12 @@ List<MediaControl> controlesEcualizadorPersonalizados({
|
||||
if (!disponible) return const [];
|
||||
return [
|
||||
MediaControl.custom(
|
||||
androidIcon: activo
|
||||
? 'drawable/ic_auto_eq_on'
|
||||
: 'drawable/ic_auto_eq_off',
|
||||
label: activo
|
||||
? l10n.eqCustomActionDisableLabel
|
||||
: l10n.eqCustomActionEnableLabel,
|
||||
androidIcon:
|
||||
activo ? 'drawable/ic_auto_eq_on' : 'drawable/ic_auto_eq_off',
|
||||
label:
|
||||
activo
|
||||
? l10n.eqCustomActionDisableLabel
|
||||
: l10n.eqCustomActionEnableLabel,
|
||||
name: accionEqToggle,
|
||||
),
|
||||
MediaControl.custom(
|
||||
@@ -198,6 +201,85 @@ List<MediaControl> controlesEcualizadorPersonalizados({
|
||||
];
|
||||
}
|
||||
|
||||
/// Content-style extras for the Ecualizador folder's items (decision
|
||||
/// `auto/ecualizador-diseno`), mirrors `ConstructorArbolAuto
|
||||
/// ._contentStyleLista` in `navegacion_auto.dart` — duplicated rather than
|
||||
/// exposed publicly (see [nombrePresetVisible]'s doc for why small pieces
|
||||
/// are deliberately duplicated across this handler/service layer and the
|
||||
/// pure browse-tree builder layer rather than cross-layer-shared). List
|
||||
/// style, not grid: these items are short text options with no artwork of
|
||||
/// their own, unlike a station or local-track tile.
|
||||
const _contentStyleListaEq = {
|
||||
'android.media.browse.CONTENT_STYLE_BROWSABLE_HINT': 1,
|
||||
'android.media.browse.CONTENT_STYLE_PLAYABLE_HINT': 1,
|
||||
};
|
||||
|
||||
/// Marks the active Ecualizador-folder item by prefixing [titulo] with a
|
||||
/// checkmark glyph (decision `auto/ecualizador-diseno`, spec "the active
|
||||
/// preset must be visibly marked").
|
||||
///
|
||||
/// A `MediaItem.extras` completion-status flag (`androidx.media.utils.
|
||||
/// MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS`) was considered
|
||||
/// and REJECTED as the marking mechanism: this project's `audio_service`
|
||||
/// version (0.18.18) has no Dart wrapper for it — only `AndroidContentStyle`
|
||||
/// 's list/grid hints are exposed — and the raw platform key itself is
|
||||
/// designed for playback-COMPLETION tracking (e.g. "this podcast episode
|
||||
/// was already listened to"), not item SELECTION; repurposing it here could
|
||||
/// render as "already played" on some head units, which would be actively
|
||||
/// misleading for a preset picker, and there is no way to verify its actual
|
||||
/// rendering on a real head unit from this environment. A plain-text
|
||||
/// marker renders identically and unambiguously on every head unit, which
|
||||
/// an unverifiable, semantically-mismatched extras key cannot guarantee.
|
||||
String _marcarActivoEq(String titulo, {required bool activo}) =>
|
||||
activo ? '✓ $titulo' : titulo;
|
||||
|
||||
/// Builds the "Ecualizador" folder's children for the Android Auto browse
|
||||
/// tree (decision `auto/ecualizador-diseno`): "Desactivar" FIRST, then the
|
||||
/// six factory presets in [PresetEcualizador.presets] order, each localized
|
||||
/// via [nombrePresetVisible] — the SAME helper the toggle's custom-action
|
||||
/// label already uses, so a preset's name reads identically whether the
|
||||
/// driver sees it in the now-playing screen's tooltip or in this folder.
|
||||
/// All items are playable: tapping one is dispatched through
|
||||
/// `playFromMediaId` -> `seleccionarPresetEqPorMediaId` (`navegacion_auto.
|
||||
/// dart`), the same seam every other browse-tree leaf already uses; this
|
||||
/// folder has no sub-browsing. Exactly one item is marked active via
|
||||
/// [_marcarActivoEq]: "Desactivar" when [activo] is `false`, otherwise
|
||||
/// whichever preset equals [presetActual] — never both at once, and never
|
||||
/// zero once this function is reached (an unresolvable [presetActual] with
|
||||
/// [activo] `true` simply marks nothing, which cannot happen in practice
|
||||
/// since [presetActual] always originates from [PresetEcualizador.presets]
|
||||
/// or a "Personalizado" tweak that would just leave every item unmarked
|
||||
/// rather than mis-marking one).
|
||||
List<MediaItem> itemsEcualizadorAuto({
|
||||
required bool activo,
|
||||
required PresetEcualizador presetActual,
|
||||
required AppLocalizations l10n,
|
||||
}) {
|
||||
final constructor = ConstructorArbolAuto();
|
||||
final items = <MediaItem>[
|
||||
MediaItem(
|
||||
id: ConstructorArbolAuto.idDesactivarEq,
|
||||
title: _marcarActivoEq(l10n.autoEqDisableOption, activo: !activo),
|
||||
playable: true,
|
||||
extras: _contentStyleListaEq,
|
||||
),
|
||||
];
|
||||
for (final preset in PresetEcualizador.presets) {
|
||||
items.add(
|
||||
MediaItem(
|
||||
id: constructor.idPresetEq(preset.nombre),
|
||||
title: _marcarActivoEq(
|
||||
nombrePresetVisible(l10n, preset.nombre),
|
||||
activo: activo && preset == presetActual,
|
||||
),
|
||||
playable: true,
|
||||
extras: _contentStyleListaEq,
|
||||
),
|
||||
);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/// Wrapper de alto nivel para el UI.
|
||||
class ServicioAudio {
|
||||
PluriWaveAudioHandler get _handler {
|
||||
@@ -1158,6 +1240,17 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
fuente: fuenteLocal,
|
||||
);
|
||||
if (musicaLocal != null) return musicaLocal;
|
||||
// Ecualizador folder (decision `auto/ecualizador-diseno`): needs no
|
||||
// external data source, unlike every branch below it -- checked
|
||||
// before the `_fuenteNavegacionGlobal` gate, mirroring how the
|
||||
// local-music branch above is also resolved before that gate.
|
||||
if (parentMediaId == ConstructorArbolAuto.idEcualizador) {
|
||||
return itemsEcualizadorAuto(
|
||||
activo: _ecualizadorActivo,
|
||||
presetActual: _presetActual,
|
||||
l10n: _textos,
|
||||
);
|
||||
}
|
||||
final fuente = _fuenteNavegacionGlobal;
|
||||
if (fuente == null) return const [];
|
||||
if (parentMediaId == ConstructorArbolAuto.idFavoritos) {
|
||||
@@ -1228,14 +1321,24 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
if (fuenteLocal == null) return;
|
||||
await reproducirCarpetaLocal(
|
||||
mediaId,
|
||||
aleatorio: constructorArbol.esCarpetaLocalAleatorioMediaId(
|
||||
mediaId,
|
||||
),
|
||||
aleatorio: constructorArbol.esCarpetaLocalAleatorioMediaId(mediaId),
|
||||
fuente: fuenteLocal,
|
||||
iniciarCola: _iniciarColaLocal,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Equalizer preset selection (decision `auto/ecualizador-diseno`):
|
||||
// THIRD branch, same unconditional-return shape as the two above --
|
||||
// an `eq_preset:` id never falls through to station routing.
|
||||
if (constructorArbol.esPresetEqMediaId(mediaId)) {
|
||||
await seleccionarPresetEqPorMediaId(
|
||||
mediaId,
|
||||
activo: _ecualizadorActivo,
|
||||
aplicarPreset: aplicarPreset,
|
||||
activarEcualizador: setEcualizadorActivo,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final fuente = _fuenteNavegacionGlobal;
|
||||
if (fuente == null) return;
|
||||
await reproducirPorMediaId(
|
||||
|
||||
Reference in New Issue
Block a user