fix(auto): restore the equalizer toggle and list the user's own presets

Two Android Auto regressions reported from the car.

1. The on/off equalizer action disappeared from the playback screen.

That was self-inflicted: commit cacd3ec removed it on the theory that a
custom action in `controls` aborts `AudioService.setState` and kills the
media notification. Reading the plugin source refutes it. setState
(AudioService.java:513-520) SPLITS the list -- a control carrying a
customAction goes to `customActions` (PlaybackStateCompat, i.e. the car),
everything else becomes a NotificationCompat.Action in `nativeActions`
(the phone notification). The two never mix. And the throw the theory
depended on cannot happen here: ic_auto_eq_on/ic_auto_eq_off both exist
under res/drawable, and the labels are non-empty in all 13 locales.

The notification outage was already fixed by abc6b47 (transient idle on
a source change, which setState turns into a full stop() at :557).

The action is back, with both state-aware icons. The real invariant --
a custom action's icon must resolve and its label must be non-empty --
is now a test that reads res/drawable and fails on a missing file,
instead of a comment claiming custom actions are forbidden outright.

2. The Ecualizador folder never listed the user's saved presets.

itemsEcualizadorAuto iterated PresetEcualizador.presets, so only the six
factory presets appeared -- the user's own were unreachable from the
car, the surface where a preset picker matters most. They now arrive
through a registered read function (same seam as stations and local
music, re-read per browse so a preset saved on the phone shows up
without an app restart).

presetsEcualizadorAuto is the single source of truth for the ordered
universe, used to BUILD the items and to RESOLVE a tap, so the folder
cannot show an item that resolution then refuses -- which is what the
factory-only default in seleccionarPresetEqPorMediaId would have caused.
A custom preset whose name collides with a factory one is dropped: the
media id is the raw name, so it could only ever resolve to the factory
entry, and an item that applies a preset other than the one it names is
worse than an absent one.

Tests: 1108 -> 1120.
This commit is contained in:
2026-08-03 21:32:09 +02:00
parent d3999a20fb
commit f2f706b342
5 changed files with 430 additions and 98 deletions
@@ -120,4 +120,135 @@ void main() {
expect(desactivar.title, contains(l10n.autoEqDisableOption));
});
});
/// On-device feedback: the folder only ever listed the six FACTORY
/// presets, so a user's own saved presets were unreachable from the car —
/// the surface where a preset picker matters most, since you cannot pull
/// out the phone and drag five band sliders while driving.
group('presets personalizados del usuario en la carpeta', () {
final mio = PresetEcualizador(
nombre: 'Mi coche',
bandas: [4.0, 2.0, 0.0, 1.0, 3.0],
);
final otro = PresetEcualizador(
nombre: 'Podcast noche',
bandas: [-3.0, 0.0, 3.0, 2.0, -1.0],
);
test('se listan tras los 6 de fábrica, en orden de guardado', () {
final items = itemsEcualizadorAuto(
activo: true,
presetActual: PresetEcualizador.flat,
l10n: l10n,
presetsPersonalizados: [mio, otro],
);
expect(items, hasLength(9));
expect(items[7].title, contains('Mi coche'));
expect(items[8].title, contains('Podcast noche'));
});
test('su id sigue el mismo formato eq_preset:<nombre> y resuelve al '
'preset correcto con la lista combinada', () {
final constructor = ConstructorArbolAuto();
final universo = presetsEcualizadorAuto(personalizados: [mio]);
final items = itemsEcualizadorAuto(
activo: true,
presetActual: PresetEcualizador.flat,
l10n: l10n,
presetsPersonalizados: [mio],
);
final item = items.last;
expect(item.id, constructor.idPresetEq('Mi coche'));
expect(item.playable, isTrue);
expect(constructor.resolverPresetEq(item.id, presets: universo), mio);
});
test('sin la lista combinada NO resuelve: es exactamente el fallo que se '
'reportó, un item visible que al pulsarlo no hacía nada', () {
final constructor = ConstructorArbolAuto();
// Default `presets` == the factory six only. This is what the tap
// dispatch used to do, and why a listed custom preset was a no-op.
expect(
constructor.resolverPresetEq(constructor.idPresetEq('Mi coche')),
isNull,
);
});
test('pulsar un preset personalizado lo aplica de verdad', () async {
final aplicados = <PresetEcualizador>[];
var encendido = false;
await seleccionarPresetEqPorMediaId(
ConstructorArbolAuto().idPresetEq('Mi coche'),
activo: false,
aplicarPreset: (p) async => aplicados.add(p),
activarEcualizador: (v) async => encendido = v,
presets: presetsEcualizadorAuto(personalizados: [mio]),
);
expect(aplicados, [mio]);
// Tapping any preset while the EQ is off must also switch it on,
// exactly as it already does for a factory preset.
expect(encendido, isTrue);
});
test('el preset personalizado activo se marca con ✓, y solo él', () {
final items = itemsEcualizadorAuto(
activo: true,
presetActual: mio,
l10n: l10n,
presetsPersonalizados: [mio, otro],
);
final marcados = items.where((i) => i.title.contains('')).toList();
expect(marcados, hasLength(1));
expect(marcados.single.id, ConstructorArbolAuto().idPresetEq('Mi coche'));
});
test('un personalizado que repite el nombre de uno de fábrica se DESCARTA '
'— su id solo podría resolver al de fábrica, así que mostrarlo '
'aplicaría un preset distinto del que anuncia', () {
final impostor = PresetEcualizador(
nombre: 'Rock',
bandas: [9.0, 9.0, 9.0, 9.0, 9.0],
);
final universo = presetsEcualizadorAuto(personalizados: [impostor, mio]);
expect(universo.where((p) => p.nombre == 'Rock'), hasLength(1));
expect(
universo.firstWhere((p) => p.nombre == 'Rock'),
PresetEcualizador.rock,
);
expect(universo, contains(mio));
expect(
itemsEcualizadorAuto(
activo: true,
presetActual: PresetEcualizador.flat,
l10n: l10n,
presetsPersonalizados: [impostor, mio],
),
hasLength(8),
);
});
test('sin presets guardados la carpeta queda exactamente como antes', () {
expect(
itemsEcualizadorAuto(
activo: true,
presetActual: PresetEcualizador.flat,
l10n: l10n,
).map((i) => i.id),
itemsEcualizadorAuto(
activo: true,
presetActual: PresetEcualizador.flat,
l10n: l10n,
presetsPersonalizados: const [],
).map((i) => i.id),
);
});
});
}