From 25405564ee7c087e36c3bbf8db4a816ae16ebc77 Mon Sep 17 00:00:00 2001 From: freetlab Date: Fri, 31 Jul 2026 18:11:57 +0200 Subject: [PATCH] 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. --- .../src/main/res/drawable/ic_auto_eq_off.xml | 3 + .../src/main/res/drawable/ic_auto_eq_on.xml | 3 + .../main/res/drawable/ic_auto_eq_preset.xml | 3 + lib/servicios/servicio_audio.dart | 23 ++++- ...servicio_audio_eq_custom_actions_test.dart | 93 ++++++++++++++++--- 5 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 android/app/src/main/res/drawable/ic_auto_eq_off.xml create mode 100644 android/app/src/main/res/drawable/ic_auto_eq_on.xml create mode 100644 android/app/src/main/res/drawable/ic_auto_eq_preset.xml diff --git a/android/app/src/main/res/drawable/ic_auto_eq_off.xml b/android/app/src/main/res/drawable/ic_auto_eq_off.xml new file mode 100644 index 0000000..34416fa --- /dev/null +++ b/android/app/src/main/res/drawable/ic_auto_eq_off.xml @@ -0,0 +1,3 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_auto_eq_on.xml b/android/app/src/main/res/drawable/ic_auto_eq_on.xml new file mode 100644 index 0000000..1584ea4 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_auto_eq_on.xml @@ -0,0 +1,3 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_auto_eq_preset.xml b/android/app/src/main/res/drawable/ic_auto_eq_preset.xml new file mode 100644 index 0000000..0ca5263 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_auto_eq_preset.xml @@ -0,0 +1,3 @@ + + + diff --git a/lib/servicios/servicio_audio.dart b/lib/servicios/servicio_audio.dart index 7f53582..8dd49ee 100644 --- a/lib/servicios/servicio_audio.dart +++ b/lib/servicios/servicio_audio.dart @@ -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 controlesEcualizadorPersonalizados({ required bool disponible, required bool activo, @@ -169,14 +180,16 @@ List 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), ), diff --git a/test/servicios/servicio_audio_eq_custom_actions_test.dart b/test/servicios/servicio_audio_eq_custom_actions_test.dart index 2de6d45..7c25a57 100644 --- a/test/servicios/servicio_audio_eq_custom_actions_test.dart +++ b/test/servicios/servicio_audio_eq_custom_actions_test.dart @@ -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/', + ); + } + }, + ); + }, + ); }