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.
Reported: the media notification vanished entirely -- no shade widget, no
lock-screen controls, not even the small icon beside the clock -- while
audio kept playing and nothing was logged. Working three days earlier.
The equalizer toggle added on 30-31 July was appended to the transport
controls list. That list feeds BOTH the phone notification and the car
playback screen, and AudioService.setState walks every control through
createCustomAction (AudioService.java:513-520) BEFORE it reaches
mediaSession.setPlaybackState (:552) and enterPlayingState (:559), which
is the only place the notification is ever posted.
createCustomAction resolves the icon by name through getIdentifier
(:415-420) -- 0 on a miss -- and hands it to
PlaybackStateCompat.CustomAction.Builder, which throws on a 0 icon or an
empty label. A throw there aborts setState before the session is ever
published. ExoPlayer is independent, so audio continues; and until
asyncError got its first subscriber the exception was dropped silently.
That accounts for every detail of the report.
The car keeps its equalizer: the Ecualizador browse folder already lists
Desactivar plus every preset by name.
Tests: 1103 -> 1108.
The equalizer toggle appended to the transport controls was aborting the
whole notification. controls feeds BOTH the phone notification and the
car playback screen, and AudioService.setState walks every control
through createCustomAction (AudioService.java:513-520) BEFORE reaching
mediaSession.setPlaybackState (:552) and enterPlayingState (:559) -- the
only place the notification is ever posted.
createCustomAction resolves the icon by name via getIdentifier (:415-420),
which returns 0 on a miss, and passes it to
PlaybackStateCompat.CustomAction.Builder, which throws on a 0 icon or an
empty label. That throw aborts setState, so the media session is never
published: no shade widget, no lock-screen controls, not even the small
status-bar icon. ExoPlayer runs independently so audio keeps playing, and
until asyncError got a subscriber the exception was dropped silently.
Nothing is lost in the car: the Ecualizador browse folder already lists
Desactivar plus every preset by name, which is Auto's own idiom for
choosing among options.
The app was cancelling its own foreground service each time the source
changed. _recrearPlayer builds a fresh AudioPlayer, which emits idle
first; audio_service treats any non-idle to idle transition as a stop
and cancels the notification. Recovery then depends on
startForegroundService, which throws on API 31+ when the process is not
foreground -- screen off, lock screen, or an Android Auto start.
- Suppress the transient idle only while a source change is in flight,
via a pure mapearEstadoProceso seam so both directions are unit-tested
- Publish idle explicitly from stop(): just_audio's playerStateStream is
.distinct() over a value-equal PlayerState, so stopping an
already-idle player emits nothing, which would have left the state
stuck at loading and the notification unkillable
- Subscribe to AudioService.asyncError, which had zero listeners and was
silently swallowing the exception that identifies this class of failure
This removes a real self-inflicted teardown on every API level. It does
NOT prove the reported symptom is fixed: the audio path is byte-identical
across the releases where the symptom appeared, so the trigger is
environmental and still unidentified.
Tests: 1084 -> 1103.
just_audio's playerStateStream is .distinct() over a value-equal
PlayerState, so stopping an already-idle player emits nothing. Paired
with the source-change mask -- which writes loading into playbackState
rather than filtering at read time -- a stop landing before native init
completed would leave the state at loading forever.
audio_service only tears the foreground service down on a non-idle to
idle transition, so that window produced an unkillable notification
stuck on "cargando" with a dead Stop button: strictly worse than the
teardown this branch removes.
Additive and idempotent -- when the player does emit its own idle, this
just lands first.
Root cause of the disappearing media notification, and it is self-inflicted
on EVERY Android version — no plugin patch involved.
`audio_service`'s `_observePlaybackState` (audio_service.dart:1131-1136) calls
`AudioService._stop()` on ANY transition into `idle` from a non-idle state.
That reaches `stopService()` -> `deactivateMediaSession()` ->
`notificationManager.cancel(NOTIFICATION_ID)`. The notification is re-posted
at exactly one place, `internalStartForeground()`, reachable only from the
`!wasPlaying && playing` edge in `setState()`, and its FIRST statement is
`ContextCompat.startForegroundService(...)` — which on API 31+ throws
`ForegroundServiceStartNotAllowedException` whenever the process is not in a
foreground state.
Every station change walked straight into that. `_cambiarFuente` pushes
`loading`, then `_recrearPlayer` disposes the old `AudioPlayer` and builds a
FRESH one; a fresh player's first `playerStateStream` event is always `idle`,
and the listener forwarded it verbatim. So `loading -> idle` tore the
foreground service down mid-source-change, and recovery depended on the
following `playing: true` edge restarting it. Screen off, lock screen, or an
Android Auto / Bluetooth-initiated start is precisely where the platform
refuses that restart: audio keeps playing, the notification never returns.
That is exactly what the user reports.
The mapping decision moves out of the private `_mapProcState` into a pure
top-level `mapearEstadoProceso(proc, {required bool cambiandoFuente})`, so the
one line that decides whether the foreground service dies is unit-testable
without instantiating the handler (which needs MethodChannels). It is
byte-for-byte identical to the old switch in every case except `idle` while a
source change is in flight, which now maps to `loading`. The test asserts the
full ProcessingState x cambiandoFuente matrix against a literal transcription
of the previous mapping, and asserts both directions explicitly: a real stop
still yields `idle`, a source-change idle yields `loading`, and `idle` is the
only case where the two branches differ at all.
The only risk this introduces is a `_cambiandoFuente` stuck at `true`: a real
user stop would be masked away from `idle`, the service would never stop, and
the notification would become unkillable. So the flag is cleared by four
independent mechanisms rather than one audited path:
- a `finally` around the whole body of `_cambiarFuente`, which covers normal
completion, BOTH `revision != _revisionFuente` early returns, every
`rethrow` out of a catch clause, and any non-`Exception` `Error` that none
of the three clauses matches;
- eagerly at the top of each of the three catch clauses — needed on top of
the `finally` because `_gestionarErrorReproduccion` calls `_player.stop()`
WITHOUT awaiting it, so that `idle` could otherwise land while the mask
was still up;
- right after `setUrl` resolves, before anything below can await, since the
fresh player's transient `idle` is already behind us at that point;
- at the start of `stop()` — before `_player.stop()` — and at the start of
`_gestionarErrorReproduccion`, which makes the invariant total: the flag
is `false` before every single `_player.stop()` call in this class.
`stop()` matters most: `BaseAudioHandler.stop()` is empty, so the handler
never pushes `idle` itself — teardown is driven entirely by the player's
emission. A stop landing while a station change was still in flight would
otherwise be masked and the notification would survive the stop.
Audited: two `_player.stop()` call sites exist and both are preceded by a
clear; `_recrearPlayer` has exactly one caller and it is guarded; the old
player cannot emit during `_recrearPlayer` because its subscriptions are
cancelled first.
`AudioService.asyncError` had ZERO subscribers app-wide. The plugin funnels
every asynchronous failure of its own observers into that stream and nowhere
else — `_observePlaybackState`, `_observeMediaItem` and `_observeQueue` each
wrap their whole body in `catch (e) { _asyncError.add(e); }`, and the artwork
path uses `.catchError(_asyncError.add)` — and a `PublishSubject` with no
listeners simply drops what it is given. The platform-side exception behind
"the media playback notification disappeared" was therefore being discarded
without a single log line, which is why that report arrives with no evidence
attached.
`observarErroresAudio` is a pure, injectable seam in `arranque_audio.dart`
(stream in, logger callback out), matching the seam convention this codebase
already uses for `esperarArranqueAudio`, `decidirAvanceCola` and
`debeReaplicarEcualizador`: the unit tests exercise the wiring with a plain
`StreamController`, never the real plugin. The default logger emits one
`[PluriWave]`-prefixed `developer.log` line at `level: 900`, the same level
and prefix `servicio_audio.dart` already uses, so one logcat filter catches
both.
Wired from `lib/main.dart`, not from `arranque_audio.dart`: main.dart is the
module that genuinely owns handler lifecycle — it is the only caller of
`AudioService.init`, `registrarHandler` and `ServicioAudioSession`, and both
the on-time and the degraded/timeout startup branches converge on its
`conectarHandler` closure. `arranque_audio.dart` owns only the timeout race
and the degraded loading shell; it never creates or registers a handler
(`alListo` is injected into it from main.dart), so it has no lifecycle to
hang a subscription on. Subscribing happens before `AudioService.init` — the
getter only touches a static subject — so nothing reported during the
MediaBrowser handshake is missed, and one subscription covers both paths.
The subscription is cancellable and its `cancel` is registered into the
handler via `registrarLimpiezaArranque`, mirroring the existing
`registrarHandler` / `registrarFuenteNavegacion` / `registrarFuenteMusicaLocal`
registration convention. `onTaskRemoved` — the only handler teardown in this
app — runs it, so the subscription cannot outlive what it instruments. The
dependency points bootstrap -> service, so `servicio_audio.dart` never has to
import the bootstrap module or the plugin's static stream.
Zero behaviour change: nothing but log output is added.
Reusable first-launch onboarding and manual reference under
Ajustes > Información > Ayuda y tutorial.
- 9-page PageView walking favorites/groups, the two-level equalizer,
live recording, adaptive alarms, Android Auto, auto-reconnect, snooze
duration and custom stations, closing with where to find it again.
- Shown once via a plain persisted flag, so it fires on a fresh install
AND on the first launch after an existing install updates to this
version -- inserted between the welcome screen and the unrelated
what's-new dialog in the boot sequence.
- The existing 'Ayuda y tutorial' Settings tile now opens this carousel
instead of the what's-new dialog, which loses its only manual entry
point but keeps its own auto-show cadence unchanged.
Monetization-free, matching the welcome screen's binding constraint.
Tests: 1064 -> 1084.
Three unrelated reports fixed together.
- PluriRootHeader ignored the top system inset, so every root screen's
own title row sat under the status bar / notch. Now pads for
MediaQuery top inset without touching app.dart's deliberate edge-to-edge
SafeArea(top:false) background bleed.
- Android Auto pushed the global-sort-derived favorites list instead of
the phone's own manual order, and the tree builder then force-sorted
everything by a hardcoded criterion regardless of what arrived --
incoming order is now preserved, and Todas/Mis emisoras follow the
same ordenListas setting the phone itself uses.
- The vacation range edit sheet could save but not delete; it now offers
both, reusing the existing confirm dialog and delete path.
Tests: 1051 -> 1064.
The vacation edit sheet could save changes to an existing range but had
no way to remove it, forcing users back to the swipe-to-delete gesture
on the list. When editing (not creating) a range, the sheet now shows
an outlined delete action next to Save; it reuses the existing
confirmation dialog and EstadoAlarmas.eliminarRangoVacaciones exactly
as the swipe gesture already does, then pops on success.
PluriRootHeader rendered its 56px title/actions row flush at y=0 on
every device, since app.dart's root SafeArea(top: false) deliberately
excludes the top inset (so each root's full-bleed background paints
edge-to-edge behind the status bar) but the header itself never added
MediaQuery.paddingOf(context).top anywhere. The header now wraps its
existing 56px content row in an outer top padding equal to that inset,
so total rendered height is height + topInset while `height` keeps
meaning the content row's own height (verified no call site did
total-height math against the old fixed constant).
Point the existing Info tile at PantallaTutorialAyuda (with
primerArranque: false, so its last page reads "Close") instead of
PluriOnboardingDialog's "what's new" modal.
Trade-off: PluriOnboardingDialog loses its only manual entry point --
it keeps auto-showing on its own existing cadence from app.dart, but
is no longer reachable by tapping this tile. This matches the mockup's
Info screen, which has no separate "what's new" row.
Insert PantallaTutorialAyuda.mostrarSiProcede between the welcome
screen and the recurring what's-new dialog in
_mostrarFlujoPrimerLanzamiento, so the carousel shows once on every
install -- fresh AND existing installs upgrading to this version --
via its own independent one-time flag, without racing either
surface.
Add PantallaTutorialAyuda, a PageView-based carousel covering saved
stations/groups, per-station equalizer, recording, adaptive alarms,
Android Auto favorites, auto-reconnect, snooze duration, custom
stations, and a closing summary with a "watch it again" reminder.
ServicioTutorialAyuda persists a one-time seen flag so the carousel
shows once via mostrarSiProcede, independent of entry point; the
final page's CTA label depends on the primerArranque constructor
parameter ("Empezar a escuchar" vs "Cerrar").
Translate the new copy into all 13 supported locales and update
helpSubtitle to describe the new entry point.
Android Auto's Favoritos/Todas/Mis emisoras folders always re-sorted by
a hardcoded quality criterion in ConstructorArbolAuto.hijos/hijosGrupo,
discarding whatever order the caller passed in. EstadoRadio now pushes
already-ordered snapshots (listaFavoritosManual for Favoritos, and the
ordenListas-sorted populares/emisorasCustom getters for Todas/Mis
emisoras, re-pushed immediately on cambiarOrdenListas), and hijos/
hijosGrupo stop re-sorting so that order survives into the car.
Reported: on a Redmi C55 the alarm never rang, no full-screen window, no
pre-notice -- "as if there were no alarm at all". Same build works on a
Poco X7 Pro.
Not device-specific. EstadoAlarmas already recorded per-alarm scheduling
failures and exposed ultimaExcepcionPara, but no screen ever called it,
so a failed alarm rendered identically to a working one.
- Scheduling failures now mark their own card
- The three native paths that only logged -- an unarmed pre-notice, a
refused foreground-service start, a per-alarm reschedule failing after
boot -- report to Dart and become per-alarm exceptions
- After a save, the native pending-alarm count is cross-checked, so an
alarm that never reached the OS is caught immediately
Additive throughout: successful scheduling behaves identically and no
logic branches on manufacturer.
Tests: 1029 -> 1051.
The first pass read 'alarmaId'/'tipo' from the channel payload while the
native side sends 'alarmId'/'type'/'atMillis' (AlarmScheduler.kt:1389).
Every entry would have been dropped silently in production.
The tests passed because the fake was seeded with the same guessed keys,
so they confirmed the mistake instead of catching it. Decoding now goes
through FalloProgramacionNativo.fromMap -- the single place native key
names appear -- and the fixtures build through that same constructor.
Completes the bridge the native side already exposed. AlarmScheduler and
PluriWaveAlarmService record a pre-notice that could not be armed, a
refused foreground-service start, and a per-alarm reschedule that failed
after a reboot -- but nothing read them, so all three still ended at
logcat.
EstadoAlarmas now drains them at startup and turns each into a per-alarm
exception, which the card already knows how to mark. An alarm that never
reached the OS stops looking identical to one that did.
The read is deliberately tolerant: a failure to read is logged and
swallowed, never surfaced as an alarm error, so a diagnostics gap cannot
masquerade as a scheduling problem.
android.programar() returning without throwing was treated as proof
the OS registered the alarm -- this is exactly the gap the reported
case fell through. guardarAlarma now cross-checks a fresh native
pending-alarm count against how many alarms Dart believes are
active-with-a-next-run right after a successful schedule call, and
records a failure for the just-saved alarm when the native count
falls short.
FakePuertoAlarmasAndroid.alarmasNativasPendientes now defaults to a
count derived from programar()/cancelar() calls (mirroring the real
native scheduler's own registry) instead of a frozen 0, while any
test that explicitly assigns the field keeps getting exactly that
value regardless of what programar/cancelar do afterward -- verified
against the full suite, no regressions.
Wires EstadoAlarmas.ultimaExcepcionPara into PantallaAlarmas: an
alarm with an outstanding scheduling-failure exception now shows a
calm warning line (distinguishing a pre-notice-only failure from the
alarm itself not being registered) with a tap target into the
reliability diagnostics screen. The warning is its own small tap
target nested inside the existing card InkWell, so tap-to-edit,
swipe-to-delete and the hero "Saltar" chip are untouched.
Adds alarmCardSchedulingFailedMessage/alarmCardPreNoticeFailedMessage
to all 13 ARB locales with real per-language translations (verified
against arb_parity_test and arb_anti_copy_test).
guardarAlarma/posponerAlarma/posponerProximaDesdePreaviso now record
a scheduling failure via ServicioAlarmas.registrarFalloProgramacion
on catch and clear it on a successful (re)schedule, in addition to
the existing transient EstadoAlarmas.error string. This makes the
failure visible per-alarm via ultimaExcepcionPara instead of only a
generic app-wide message.
Also fixes _sincronizarTodas: a single alarm's android.programar
throw used to abort the whole loop, silently skipping every sibling
alarm scheduled AFTER it on that pass (including on every app launch,
via inicializar). Each alarm's outcome is now independent.
Adds ServicioAlarmas.registrarFalloProgramacion/limpiarFalloProgramacion,
persisting a scheduling-reliability failure through the same
ExcepcionAlarma model saltarProxima already uses. Only one failure
record is kept per alarm (latest attempt wins) and skipNext entries
for any alarm are never touched. EstadoAlarmas wiring follows next.
ExcepcionAlarma._esValida matched ANY exception tipo against an
occurrence, treating it as a user skip. Only the 'skipNext' tipo
existed until now, but the next commits reuse the same model to
record scheduling-reliability failures per alarm (so the alarms list
can surface them via ultimaExcepcionPara) -- without this guard, a
recorded failure would be silently treated as if the user asked to
skip that occurrence, corrupting scheduling. Adds tipo constants to
ExcepcionAlarma for the upcoming failure kinds.
On-device feedback: two identical icons on the car's now-playing screen,
one of which looked dead. It worked -- but head units render custom
actions icon-first, so cycling six presets behind one static glyph was
invisible.
A monochrome icon cannot encode which of six presets is active. Android
Auto separates the idioms deliberately: custom actions for stateless
toggles, browsable lists for choosing among options.
- Playback screen keeps one action: equalizer on/off, state-aware icons
- New Ecualizador folder lists Desactivar plus the six presets by name,
active one marked
- The preset-cycling action and its drawable are removed
Supersedes the redesign's no-equalizer-folder rule, which predated
knowing custom actions do not surface state in a car.
# Conflicts:
# lib/l10n/app_ar.arb
# lib/l10n/app_bn.arb
# lib/l10n/app_de.arb
# lib/l10n/app_en.arb
# lib/l10n/app_es.arb
# lib/l10n/app_fr.arb
# lib/l10n/app_hi.arb
# lib/l10n/app_id.arb
# lib/l10n/app_it.arb
# lib/l10n/app_ja.arb
# lib/l10n/app_pt.arb
# lib/l10n/app_ru.arb
# lib/l10n/app_zh.arb
Reported: on a Redmi C55 the alarm never rang and the pre-notice never
appeared, while the same build works on a Poco X7 Pro.
The app was never device-specific -- every permission is declared. The
gap was visibility: six diagnostic signals were collected and only three
shown. Battery-optimisation exemption and the count of alarms actually
registered with Android, the two most diagnostic for this failure, were
gathered and discarded.
- Full diagnostics screen, one row per signal, each with a Fix button
wired to the right system settings intent and guarded by SDK level
- Manufacturer guidance for Xiaomi/Huawei/Oppo/Vivo/OnePlus/Samsung
explaining Autostart must be enabled by hand -- there is no API for it
- Unresolvable intents surface a message instead of a dead tap
Tests: 993 -> 1014.
The equalizer's preset-cycling custom action (eq_preset_siguiente) and
its ic_auto_eq_preset drawable are no longer needed now that the
"Ecualizador" folder lists all six presets directly: the folder replaces
what the cycle action did, and this frees a scarce Android Auto custom
action slot.
The on/off toggle is now the equalizer's only custom action.
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.
Surface all six DiagnosticoAlarmasAndroid fields instead of three: the
battery-optimization exemption and native pending-alarm count were
already collected but silently dropped by the old widget. Each failing
signal now offers a "Fix this" action that opens the right system
settings screen (exact alarms, notifications, full-screen intent,
battery optimization), guarded by SDK level and never crashing when a
ROM lacks that screen. Manufacturers known for aggressive background
killing (Xiaomi/Redmi/POCO, Huawei, Oppo, Vivo, OnePlus, Samsung) get
an honest explanation that Autostart must be enabled manually, since
there is no API to detect or grant it.
Notifications now deep-links straight to
ACTION_APP_NOTIFICATION_SETTINGS via a new openNotificationSettings
native method, instead of reusing the runtime permission popup meant
for first-time alarm creation.
New copy is added to all 13 ARB locales with real per-language
translations (not Spanish copies), verified by the ARB parity and
anti-copy tests plus the corruption scanner.
DiagnosticoAlarmasAndroid already collected six raw reliability fields
but only three ever reached the UI. Add a pure-Dart mapping that turns
the raw snapshot into five ordered signals with a clear ok/needs-
attention state (exact alarms, notifications, full-screen intent,
battery-optimization exemption, native pending-alarm count), plus a
manufacturer check for vendors known to require manually enabling
Autostart (Xiaomi/Redmi/POCO, Huawei, Oppo, Vivo, OnePlus, Samsung),
since there is no public API to detect or grant that setting.
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.
- Local-music subfolders sort before files, so they no longer fall past
the 50-item page boundary and vanish from the car
- Playing a folder now plays its subfolders too, bounded at depth 4 and
500 tracks to cap native SAF round-trips
- Stations and tracks with no artwork fall back to on-brand art instead
of an empty tile
- Equalizer on/off and preset cycling are reachable from the car's
now-playing screen as two custom actions
- The equalizer is re-applied after an audio-focus interruption, not
only when the audio session id changes -- a nav-app prompt keeps the
same session, which is why the earlier fix missed this case
- The alarm list shows which days an alarm actually fires on
Tests: 933 -> 991.
The alarms list showed a generic "Días" label for a diasSemana alarm
instead of its actual configured days. Render the real recurrence (e.g.
"Lun, Mié, Vie") by reusing the SAME per-day abbreviation the editor's own
day-picker circles already use -- no new formatting scheme, no new ARB
keys for the days themselves.
Also surface fade/volume/vacation-pause state on the card, each only when
it is a genuinely useful deviation from the common case: a fade badge when
fadeInSegundos > 0 (reusing the existing alarmFadeInLabel key), a volume
percentage when it differs from the 85% default, and a vacation-paused
badge when the alarm is both configured to pause and a vacation range is
currently active (mirrors the exact predicate ServicioProgramacionAlarmas
already uses). One compact line, not a badge per field.
Fixes a text-collision regression in pantalla_alarmas_editor_test.dart:
opening the editor for an alarm whose own day now renders on its card
(e.g. "Lun") made a bare find.text(weekday) ambiguous against the editor's
day-picker circle with the same label -- scoped that finder to the
BottomSheet subtree.
The equalizer stopped applying after another app interrupted audio (e.g. a
navigation app's voice prompt): play a station with EQ working, let the
prompt speak, resume -- the audio sounds flat until the station is
re-tapped.
debeReaplicarEcualizador only re-attaches the equalizer when the native
player session id actually changes. A short transient interruption keeps
the SAME session (no id rotation), so that trigger never fires, while
Android's AudioEffect framework can let a higher-priority client silently
disable this app's effect instance in the meantime.
Add reaplicarEcualizador() to ObjetivoAudioInterrumpible, implemented as a
thin delegate to the existing _activarEcualizador() (already the correct
idempotent setEnabled + re-push-gains path). ServicioAudioSession calls it
on resume-from-pause (after reanudar()) and on un-duck (after
setAtenuado(false)) -- additive to the existing session-id trigger, not a
replacement. The method takes no argument, so it can only re-assert
whatever enabled/disabled state the handler already holds -- an
interruption cycle with the equalizer OFF stays OFF.