fix(auto): give the equalizer actions distinct, state-aware icons

On a car head unit the custom actions render icon-first, so two actions
sharing ic_stat_pluriwave were indistinguishable and the toggle gave no
sign of whether the equalizer was on.

Each action now has its own drawable, and the toggle swaps between
ic_auto_eq_on and ic_auto_eq_off so its state is legible at a glance.
This commit is contained in:
2026-07-31 18:11:57 +02:00
parent c8b2c4d2d6
commit 25405564ee
5 changed files with 107 additions and 18 deletions
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M3,5L21,5L21,7L3,7Z M14,3L17,3L17,9L14,9Z M3,11L21,11L21,13L3,13Z M7,9L10,9L10,15L7,15Z M3,17L21,17L21,19L3,19Z M17,15L20,15L20,21L17,21Z M2,20L4,22L22,4L20,2Z" />
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M3,5L21,5L21,7L3,7Z M14,3L17,3L17,9L14,9Z M3,11L21,11L21,13L3,13Z M7,9L10,9L10,15L7,15Z M3,17L21,17L21,19L3,19Z M17,15L20,15L20,21L17,21Z" />
</vector>
@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M2,14L4,14L4,18L2,18Z M6,6L8,6L8,18L6,18Z M10,10L12,10L12,18L10,18Z M14,6L14,18L22,12Z" />
</vector>
+18 -5
View File
@@ -154,12 +154,23 @@ String nombrePresetVisible(AppLocalizations l10n, String nombre) {
/// 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.
/// Folding "off" into the preset cycle (one button walking
/// off → preset 1 → preset 2 → ... → preset 6 → off) was considered and
/// REJECTED: with 6 presets that can take up to 6 taps to switch the EQ
/// off while driving, which is worse for a driver than a dedicated one-tap
/// toggle. Do not "simplify" this back down to a single action.
/// 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.
/// broken ones.
///
/// On-device feedback follow-up: both actions used to reuse the SAME
/// `ic_stat_pluriwave` drawable and were visually indistinguishable on a
/// car head unit, which foregrounds the icon over the label. Each action
/// now gets its own dedicated drawable (`ic_auto_eq_on`/`ic_auto_eq_off`/
/// `ic_auto_eq_preset`), and the toggle's icon itself reflects [activo]
/// (not just its label) so on/off is legible at a glance. Pure, no
/// handler dependency.
List<MediaControl> controlesEcualizadorPersonalizados({
required bool disponible,
required bool activo,
@@ -169,14 +180,16 @@ List<MediaControl> controlesEcualizadorPersonalizados({
if (!disponible) return const [];
return [
MediaControl.custom(
androidIcon: 'drawable/ic_stat_pluriwave',
androidIcon: activo
? 'drawable/ic_auto_eq_on'
: 'drawable/ic_auto_eq_off',
label: activo
? l10n.eqCustomActionDisableLabel
: l10n.eqCustomActionEnableLabel,
name: accionEqToggle,
),
MediaControl.custom(
androidIcon: 'drawable/ic_stat_pluriwave',
androidIcon: 'drawable/ic_auto_eq_preset',
label: l10n.eqCustomActionPresetLabel(
nombrePresetVisible(l10n, presetActual.nombre),
),
@@ -1,3 +1,4 @@
import 'dart:io';
import 'dart:ui' show Locale;
import 'package:audio_service/audio_service.dart';
@@ -162,20 +163,53 @@ void main() {
);
});
test('both actions reuse the bundled notification drawable (zero new '
'native assets)', () {
final controles = controlesEcualizadorPersonalizados(
disponible: true,
activo: true,
presetActual: PresetEcualizador.flat,
l10n: l10n,
);
test(
'toggle icon reflects EQ state: ON uses ic_auto_eq_on, OFF uses '
'ic_auto_eq_off -- a car head unit foregrounds the icon over the '
'label, so the icon itself must change, not just the text',
() {
final activado = controlesEcualizadorPersonalizados(
disponible: true,
activo: true,
presetActual: PresetEcualizador.flat,
l10n: l10n,
).firstWhere((c) => c.customAction?.name == accionEqToggle);
final desactivado = controlesEcualizadorPersonalizados(
disponible: true,
activo: false,
presetActual: PresetEcualizador.flat,
l10n: l10n,
).firstWhere((c) => c.customAction?.name == accionEqToggle);
expect(
controles.every((c) => c.androidIcon == 'drawable/ic_stat_pluriwave'),
isTrue,
);
});
expect(activado.androidIcon, 'drawable/ic_auto_eq_on');
expect(desactivado.androidIcon, 'drawable/ic_auto_eq_off');
},
);
test(
'the toggle and the preset-cycle action never share an androidIcon, '
'in either EQ state -- this was the on-device bug: both buttons used '
'the same drawable and were visually indistinguishable',
() {
for (final activo in [true, false]) {
final controles = controlesEcualizadorPersonalizados(
disponible: true,
activo: activo,
presetActual: PresetEcualizador.flat,
l10n: l10n,
);
final iconos = controles.map((c) => c.androidIcon).toSet();
expect(
iconos.length,
controles.length,
reason:
'every custom action must have a distinct androidIcon '
'(activo=$activo)',
);
}
},
);
});
group('action name constants (item 4 — collision-free with car-tree ids)', () {
@@ -186,4 +220,37 @@ void main() {
expect(accionEqToggle, isNot(equals(accionEqPresetSiguiente)));
});
});
group(
'equalizer drawable assets on disk (on-device feedback follow-up: the '
'two custom actions used to share one drawable and were visually '
'indistinguishable)',
() {
test(
'ic_auto_eq_on, ic_auto_eq_off and ic_auto_eq_preset all exist under '
'android/app/src/main/res/drawable/ -- a missing drawable is not a '
'build error, it silently renders blank/default on the head unit, '
'so this is the only safety net that would have caught the original '
'duplication',
() {
for (final nombre in [
'ic_auto_eq_on',
'ic_auto_eq_off',
'ic_auto_eq_preset',
]) {
final archivo = File(
'android/app/src/main/res/drawable/$nombre.xml',
);
expect(
archivo.existsSync(),
isTrue,
reason:
'$nombre.xml must exist under '
'android/app/src/main/res/drawable/',
);
}
},
);
},
);
}