Commit Graph
100 Commits
Author SHA1 Message Date
FreeTLab 1bfd5a2348 Merge branch 'main' of https://git.freetimelab.es/FreeTLab/pluriwave
Build & Deploy PluriWave / Análisis de código (push) Successful in 25s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m54s
2026-08-28 20:06:20 +02:00
FreeTLab 4ea5d2056c test(alarmas): anchor the vacation pill test on a relative future range
The test hardcoded 4-18 August 2026, which was in the future when it was
written and is now in the past. The pill only renders for the active or
next range, so the assertion started failing purely because the calendar
moved on — the production code was never wrong.

Anchor the range on next month (days 4-18, so it never straddles a month
boundary) and assert against rangoFechasCorto, the same pure formatter the
widget uses, so the test checks that the pill is rendered rather than
restating the formatter's own output.
2026-08-28 20:06:10 +02:00
FreeTLab 9efa6d8937 merge: bring the freemium/IAP release line into main
Build & Deploy PluriWave / Análisis de código (push) Successful in 25s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m13s
main had drifted behind PRO by the whole 1.3.0 line: the freemium/IAP
feature, the code-review fixes, the real AdMob ids, the equalizer
cross-surface resync and the closed-testing ad switch all shipped through
PRO only. This reconciles main so day-to-day work no longer branches from
a stale base.

# Conflicts:
#	pubspec.yaml
2026-08-28 19:59:12 +02:00
FreeTLab 55fe50d07d merge: equalizer cross-surface sync + test ads for the closed-testing phase [version set]
Build & Deploy PluriWave / Análisis de código (push) Successful in 24s
Build & Deploy PluriWave / Build APK + AAB release (push) Failing after 3m9s
Brings in two independent fixes that both need to reach testers:
- e9f47d4 resyncs EstadoEcualizador with car/notification-initiated changes
  and closes the persistence gap that lost them on restart.
- 2e15d05 forces Google test ad units in release builds while
  usarAnunciosDePruebaEnRelease is true, so no tester can generate invalid
  traffic against the AdMob account during closed testing.

[version set] keeps the 1.3.0 name; CI advances the build number.
2026-08-28 19:56:26 +02:00
FreeTLab 2e15d05431 fix(ads): force test ad units in release during closed testing
Closed-testing human testers cannot be registered as AdMob test
devices, so release builds serving real ad units risked invalid
traffic against an AdMob account that currently earns essentially
nothing. Add usarAnunciosDePruebaEnRelease, a single boolean switch
defaulted to true, that keeps bannerAdUnitId/interstitialAdUnitId on
Google's official test ids even in kReleaseMode. Flipping it to false
is the only change needed to go live. AndroidManifest's AdMob
application id is untouched, as it only initializes the SDK.
2026-08-28 19:49:12 +02:00
FreeTLab e9f47d47c2 fix(eq): resync EstadoEcualizador with car/notification-initiated changes
A toggle from the Android Auto notification or a preset picked from the
car's EQ folder mutated PluriWaveAudioHandler state directly, leaving
EstadoEcualizador (and therefore the phone UI) unaware and never
persisting the change, so it was lost on the next app restart.

Forward the handler's ecualizadorActivo flag through ServicioAudio and,
mirroring EstadoRadio's existing playFromMediaId resync, diff it plus
presetActual against the cached values on every estadoStream tick,
adopting and persisting a divergence via ServicioEcualizador.
2026-08-28 17:48:52 +02:00
FreeTLab 689f3e8123 chore(release): rebuild to get a fresh version code [version set]
Build & Deploy PluriWave / Análisis de código (push) Successful in 27s
Build & Deploy PluriWave / Build APK + AAB release (push) Failing after 4m4s
Play Console already holds version code 152 (name 1.3.0, uploaded 16 Aug),
so the bundle this pipeline produced was rejected as a duplicate code.

This empty commit re-triggers the build. The bump step advances the code to
153 (the first free one) while [version set] keeps the 1.3.0 version name,
which is the release name this freemium/IAP work is shipping under.
2026-08-28 15:23:13 +02:00
FreeTLab 9cfa5ac17d fix(iap): address code review defects in freemium/IAP change
Build & Deploy PluriWave / Análisis de código (push) Successful in 42s
Build & Deploy PluriWave / Build APK + AAB release (push) Failing after 4m23s
Fixes 9 of 10 review findings (10th requires a manual Play Console
step, no code change):

1. app.dart/banner_anuncio_superior.dart: move the top SafeArea inside
   BannerAnuncioSuperior so it only reserves status-bar height when an
   ad actually renders, restoring edge-to-edge layout for premium and
   free-unloaded users.
2. servicio_anuncios.dart: bound every interstitial await (load,
   presentation, and the injected implementation itself) with
   injectable timeouts so a callback that never fires can no longer
   hang a caller.
3. estado_entitlement.dart/hoja_premium.dart: expose a typed
   resultadoUsuario signal for purchase/restore failures and
   restore-found-nothing, with dedicated localized messages
   (compraError, restauracionSinCompras) across all 13 locales --
   never the raw developer/exception string.
4. main.dart/servicio_consentimiento.dart: add a GDPR/UMP consent flow
   (ConsentInformation/ConsentForm) that gates Mobile Ads SDK init on
   canRequestAds(); premium users never see a consent form; failures
   degrade to no ads instead of crashing or blocking startup.
6. servicio_anuncios.dart: track real ad presentation
   (onAdShowedFullScreenContent) so a failed-to-show interstitial no
   longer consumes a session cap slot.
7. banner_anuncio_superior.dart: add an explicit load-attempted guard
   so repeated didChangeDependencies (e.g. entitlement notifyListeners
   during a purchase) can only ever trigger one banner load attempt.
8. servicio_anuncios.dart: make esPremium a required constructor
   parameter, matching the hardened contract already applied to
   EstadoAlarmas/EstadoGrabacion/EstadoRadio.
9. hoja_premium.dart: add a dedicated premiumActivo localized string
   instead of reusing the equalizer's equalizerActive translation,
   across all 13 locales.

All fixes implemented RED-first (failing test before production
code). Full suite: 1261 passed, 2 pre-existing skips, 0 failures.
flutter analyze: 5 pre-existing issues only, 0 new.

[version set]
2026-08-12 16:10:46 +02:00
FreeTLab 94f354a7c1 feat(iap): wire real AdMob app id, banner and interstitial units
App id always uses the real value (SDK init only, no ad-serving risk).
Banner/interstitial pick the real unit id in release builds and Google's
test unit id everywhere else, so debug/profile builds can never serve
(or accidentally tap) a real ad.
2026-08-12 12:53:34 +02:00
FreeTLab d81fabbe27 refactor(iap): make esPremium a required constructor parameter
EstadoAlarmas, EstadoGrabacion and EstadoRadio defaulted `esPremium` to
`() => true`, so any construction site that forgot to wire entitlement
compiled fine and silently ran ungated — failing OPEN to premium and
disabling the paywall with no test able to catch it.

The parameter is now required with no default. Production wiring in
app.dart was already correct and is unchanged; the 184 pre-existing test
call sites now pass `() => true` explicitly, which is exactly the old
implicit default, so every assertion is untouched.

EstadoRadio has no gate of its own but constructs EstadoGrabacion, so it
inherits the same contract.

The one test that existed to pin the old default is renamed to describe
what it still covers (the premium path through iniciar() with no
duracion); its assertions are unchanged.
2026-08-10 22:06:36 +02:00
FreeTLab aa0b242374 feat(iap): add freemium unlock via one-time in-app purchase
Adds a permanent, non-consumable premium unlock (EstadoEntitlement +
PuertoCompras/ServicioComprasPlayBilling) that removes ads and unlocks
alarm vacations, alarms past a 5-alarm free cap, recording start, and
full Android Auto browsing. The phone equalizer stays free for everyone.

- Entitlement is prefs-backed (compra_premium_v1), fail-open, and
  resolvable headlessly via esPremiumPersistido() for the Android Auto
  audio handler, which registers before runApp.
- Android Auto reduced mode keeps the real root folder labels for free
  users; browsing into any of them (and playFromMediaId/playFromSearch/
  skipToNext/skipToPrevious) is blocked at the getChildren/servicio_audio
  choke points, with a locked "Función Premium" item as the backstop.
  Current-station play/pause/stop stays untouched. A free -> premium
  transition actively invalidates the head unit's cached browse tree.
- Ads (top banner + capped interstitial before adding a station or an
  alarm) are gated behind entitlement via ServicioAnuncios, using
  official Google test ad unit IDs pending AdMob provisioning.
- Alarm cap UX shows an explanatory message with a secondary unlock
  action rather than a bare paywall jump; existing data is grandfathered.
- 4 new localization keys translated across all 13 supported locales.

Co-located tests use strict TDD (RED test before implementation) for
every new pure-logic unit; full existing suite passes unchanged.
2026-08-10 20:37:07 +02:00
FreeTLab f4a1fac45a merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 25s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m30s
2026-08-07 17:17:49 +02:00
FreeTLab ea005434d2 merge: local skips stay local, failed stations keep their metadata 2026-08-07 17:17:48 +02:00
FreeTLab d754e28ddf fix(audio): keep local skips local, and stop a failed station blanking Auto
Three reported suspicions. Two confirmed by reading, one not.

1. CONFIRMED, self-inflicted. Playing a song from the phone and pressing
NEXT jumped to a radio station.

3398d02 taught skipToNext/skipToPrevious to fall back to station skipping
when there is no local queue, so the car's buttons would not be dead for
radio. But queue-less does not mean radio: tapping ONE track goes through
reproducirPistaLocal, which never builds a queue -- only folder playback
sets _colaLocal. That is exactly why the report said "at least the first
time".

emisoraActual cannot tell them apart either: _cambiarFuente fills it in for
every source, so a local MP3 arrives as an Emisora whose url is its
content:// document URI. The media id's scheme is the real discriminator,
the same test that already keeps the recorder off local files. A local
track now skips nowhere, which is the correct behaviour for a single item.

2. CONFIRMED mechanism. A failed station made the app disappear from the
Android Auto pane.

The error path published STATE_ERROR and then cleared everything:
`emisoraActual = null; mediaItem.add(null)`. That leaves the session in an
error state with no metadata at all, and Auto drops a session with nothing
to show -- reported as "if a station fails it seems to crash, and going to
1/3 it fails".

Both are kept now. Nothing outside servicio_audio.dart consumes mediaItem
(verified), so the phone is unaffected, and the car gains two things: the
screen can still name the station that failed instead of going blank, and
previous/next stay usable, so a driver can skip out of a dead station
instead of being stranded -- _saltarEmisora needs emisoraActual to know
where it is in the list. The error state itself is unchanged.

3. NOT CONFIRMED. A local track occasionally jumping to another one mid-play.

An advance requires a genuine `completed` from just_audio, so either the
player reports the end early -- plausible for a content:// SAF source,
whose duration is not always exact -- or something else moved the track.
Reading the code cannot separate those, so nothing was changed on a guess.
The advance now logs the decision with the processing state, position and
duration that caused it, so the next occurrence arrives with its reason
attached.

Tests: 1192 -> 1195.
2026-08-07 17:17:48 +02:00
FreeTLab 950c9fda58 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 28s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m41s
2026-08-07 13:06:01 +02:00
FreeTLab 0949525859 merge: ship the onboarding and release-notes content 2026-08-07 13:06:00 +02:00
FreeTLab 0ef6ce35b4 fix(assets): declare the content subdirectories so onboarding ships
Audit of the same failure family as the shrunk drawables: references by
NAME that nothing validates at compile time.

The whole onboarding and release-notes feature had never shipped. Reading
the installed APK: ZERO entries under assets/content/, while
assets/icons/alarmas/* was present. pubspec declared `assets/content/`, and
Flutter does not recurse -- naming a directory includes the files sitting
directly in it, never its subdirectories. Every content file lives in one
(onboarding/, updates/<locale>/), so none of them were packaged.

On the device that surfaced on every single launch:

  Unable to load asset: "assets/content/onboarding/en.md"

with the file plainly present on disk. That is why it never looked like a
packaging problem. The tell was already in the pubspec: assets/icons/alarmas/
is listed explicitly, so the rule was known once and not applied here.

All 14 content directories are now declared: onboarding/ plus updates/ for
each of the 13 locales.

The guard is a test that loads every file under assets/content/ through
rootBundle, because that is the only thing that proves an asset is declared
and will ship. A test asserting File.existsSync would have stayed green
through all of this -- the files were never missing, only unpackaged. Run
against the unfixed pubspec it fails 26 of 27; with the fix it passes.

Tests: 1165 -> 1192.
2026-08-07 13:05:59 +02:00
FreeTLab 28f47d6340 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 26s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m39s
2026-08-07 12:52:56 +02:00
FreeTLab ea0c6c8a9c merge: keep Dart-named drawables from the resource shrinker 2026-08-07 12:52:55 +02:00
FreeTLab c3cc4120c0 fix(android): stop the resource shrinker from deleting Dart-named drawables
Root cause found, and it is not the stale build cache I claimed earlier.
flutter clean was good hygiene and changed nothing here, because nothing
was cached: the resources were being deliberately removed.

Flutter's own Gradle plugin enables shrinking on every release build --
FlutterPlugin.kt, `releaseBuildType.isMinifyEnabled = true` and
`isShrinkResources = true` -- no matter what app/build.gradle.kts says. The
shrinker keeps what it can see referenced, and it cannot see
`MediaControl.custom(androidIcon: 'drawable/ic_auto_eq_on')`: that is a
string inside Dart, resolved at runtime through getIdentifier. So both
equalizer icons were stripped from every release APK ever built.

The evidence that pins it, from the APK pulled off the device:

  ic_stat_pluriwave   present   <- referenced as R.drawable from Kotlin,
                                   4 call sites in the alarm notifications
  ic_auto_eq_on       absent    <- named only in a Dart string
  ic_auto_eq_off      absent    <- named only in a Dart string

Same folder, same file shape, same commit range. The only difference is
whether a real R.drawable reference exists, which is exactly what the
shrinker looks for.

The consequence was never a blank button. getResourceId returns 0 for an
unresolvable name, PlaybackStateCompat.CustomAction.Builder throws on a 0
icon, and that throw aborts AudioService.setState before the media session
is activated -- so Android Auto held a frozen, inactive session. Dead
playback screen, play that never became pause, the app losing its pane to
any app with a live session, audio playing "as if it were not the app".
One shrunk file, four symptoms, since 31 July (2540556).

Two protections, because they fail differently:
- res/raw/keep.xml with tools:keep is the official mechanism for
  dynamically resolved resources and is what actually binds the shrinker;
- RecursosResueltosPorNombre.kt gives them genuine R.drawable references,
  the same thing that kept ic_stat_pluriwave alive all along.

station_art_* are kept too. They are reached the same way, through
android.resource:// URIs built in Dart, and survived only by luck.

Tests: 1165, unchanged -- this is a build-configuration fix, and no Dart
test can see it. The CI resource guard is what verifies it now.
2026-08-07 12:52:55 +02:00
FreeTLab 9914aced92 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 27s
Build & Deploy PluriWave / Build APK + AAB release (push) Failing after 1m19s
2026-08-07 12:40:26 +02:00
FreeTLab 4cc42af9d1 merge: resource guard inspects the resource table and self-checks 2026-08-07 12:40:25 +02:00
FreeTLab e0fa2d695a fix(ci): inspect the resource table, not zip paths, and self-check first
The previous guard reported "Drawables en el APK: (ninguno)" for a 105MB
release APK. Zero drawables is impossible -- AndroidX alone contributes
dozens -- so the check was wrong, not the build. Release APKs shorten and
rename resource file paths, so `res/drawable/...` simply is not how they
are stored there. The 45MB base.apk taken off the device kept readable
paths because it came from an AAB through bundletool; the CI builds a fat
APK through a different pipeline. Same app, different layout.

Resource NAMES survive in resources.arsc regardless of path shortening, so
that is what gets inspected now.

And the guard checks itself before judging. It looks for a sentinel
resource known to be present (station_art_nova); if the sentinel is not
found, the inspection method is unreliable and the step says so instead of
declaring anything absent. This guard has already lied once, reporting
ic_stat_pluriwave missing when it was verified present, and that lie was
about to send us hunting a build problem that did not exist. A check with
no way to detect its own failure has no business failing a build.

Verified before pushing, all three extracted verbatim from the parsed YAML
and run against real inputs:
  1. real 45MB base.apk    -> sentinel found, ic_stat_pluriwave OK,
                              ic_auto_eq_on/off missing, exit 1
  2. APK absent            -> reports the path and lists what is there,
                              exit 1, no resource accusations
  3. zip without arsc      -> "inspection impossible", exit 1 (checked
                              without a pipe, so the code is the script's)

Scenario 3 is the one the old guard got wrong: it turned an inspection
failure into three false "FALTA" lines.
2026-08-07 12:40:25 +02:00
FreeTLab ca5f243524 merge: repair the workflow YAML
Build & Deploy PluriWave / Análisis de código (push) Successful in 26s
Build & Deploy PluriWave / Build APK + AAB release (push) Failing after 1m29s
2026-08-07 12:35:02 +02:00
FreeTLab 1e97a94602 fix(ci): repair the workflow YAML broken by an unindented heredoc
The previous commit made build.yml unparseable and no job ran at all:

  yaml: line 183: could not find expected ':'

A `run: |` block is a YAML literal scalar, so every line has to keep the
block's indentation. The python3 fallback I added used a heredoc whose
body sat at column 0, which terminates the scalar -- YAML then tried to
read `import zipfile, sys` as a mapping and gave up. Worse than a broken
check: a broken pipeline.

The fallback is gone rather than re-indented. unzip is present on this
runner, a second code path existed only to guard against a case that was
never observed, and its only contribution was an escaping hazard inside
YAML inside shell.

Verified before pushing this time, which is the actual lesson:
- build.yml now parses (yaml.safe_load), 6 + 15 steps;
- the guard's `run` script was extracted from the parsed YAML and executed
  verbatim against the real 45MB base.apk pulled off the device. It prints
  the drawable inventory, reports ic_stat_pluriwave OK and ic_auto_eq_on /
  ic_auto_eq_off missing, and exits 1 -- matching an independent zipfile
  inspection of the same file.

Two commits in a row shipped a CI change that had never been run. Both
were caught by the user rather than by me.
2026-08-07 12:35:02 +02:00
FreeTLab 107739caa3 merge: incorporate the CI version bump 2026-08-07 12:27:41 +02:00
FreeTLab b3bd71be84 merge: make the APK resource guard trustworthy 2026-08-07 12:27:40 +02:00
FreeTLab 0a47c327f1 fix(ci): stop the resource guard from lying when it cannot inspect the APK
The guard added in the previous commit reported all three drawables as
missing on its first run, including ic_stat_pluriwave -- which is
verifiably present: it was read out of the base.apk pulled off the device
byte by byte. The step also finished in 0s, so it never opened the file at
all. Either the APK is not at the assumed path on this runner or unzip is
unavailable, and the failing pipeline silently produced an empty listing
that every grep then "failed" against.

A guard that lies is worse than no guard: it sends you hunting ghosts,
which is exactly the failure mode this whole episode has been about.

It now verifies its own preconditions before judging anything:
- the APK must exist, and if it does not the step prints where the APKs
  actually are (find over build/app/outputs) instead of guessing;
- it needs unzip or python3, and says so plainly if neither is there;
- an empty listing is treated as "inspection unreliable", not as
  "everything is missing";
- it dumps the real res/drawable inventory before the verdict, so a
  future failure is readable without another round trip.

Matching is now exact (grep -qx) rather than substring.

The logic was run locally against the real 45MB base.apk taken off the
device: ic_stat_pluriwave OK, ic_auto_eq_on and ic_auto_eq_off missing --
which is precisely what an independent zipfile inspection of the same APK
reported yesterday. The check agrees with reality before shipping.
2026-08-07 12:27:40 +02:00
FreeTLab ec6ccb2db8 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 25s
Build & Deploy PluriWave / Build APK + AAB release (push) Failing after 2m0s
2026-08-07 11:38:27 +02:00
FreeTLab 8e00dc0c7c merge: resource guard in CI and group-aware station skipping 2026-08-07 11:38:27 +02:00
FreeTLab f01c0911f7 fix(auto): guard shipped resources, walk favourite groups when skipping
History review requested by the owner: when and why did the Android Auto
UI stop working.

ANSWER: 31 July, commit 2540556, "give the equalizer actions distinct,
state-aware icons".

  9eff760 (31-07)  androidIcon: 'drawable/ic_stat_pluriwave'  -> in the APK
  2540556 (31-07)  androidIcon: 'drawable/ic_auto_eq_on'      -> NEVER in it

That commit swapped a drawable that shipped for two that the stale CI
resource cache never included. From that moment getResourceId returned 0,
PlaybackStateCompat.CustomAction.Builder threw, and the throw aborted
AudioService.setState before the session was published -- so every Android
Auto symptom chased since is one line of that commit. The bitter part is
that 2540556 was itself a fix for a report about two identical icons.

Three changes.

1. CI guard. The build now unzips the release APK and fails if a drawable
resolved by NAME at runtime is missing. Resolution by name cannot fail at
compile time -- it fails in the car, silently, with id 0. This class of
bug shipped undetected for a week; it cannot ship again.

2. Skipping stations now walks the favourites GROUP first, as requested:
group -> all favourites -> my stations -> catalogue. Two deliberate
exclusions, both tested: `sinAsignarId` is the ABSENCE of a group, not a
group, so those walk all favourites; and a one-member group falls through
too, or both buttons would be dead ends. The group is read from the
FAVOURITE record, never from the playing station -- that one is rebuilt by
emisoraDesdeMediaItem, which carries no group id and would always report
"unfiled".

3. Diagnostics on the station skip. It was reported as doing nothing for
radio, and every early return in that method is silent: an empty list and
a single-entry list look identical from outside. The log now names which
one fired, so the next capture answers it instead of another hypothesis.

Tests: 1161 -> 1165.
2026-08-07 11:38:27 +02:00
FreeTLab 57f89c130f merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 26s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m39s
2026-08-07 00:24:18 +02:00
FreeTLab 02cfd48992 merge: clean the CI build so new Android resources ship 2026-08-07 00:24:18 +02:00
FreeTLab 72a291d0c0 fix(ci): clean before building so new Android resources reach the APK
The equalizer drawables were never in the shipped binary. Verified by
pulling base.apk off the device and reading it:

  res/drawable/ic_stat_pluriwave.xml   PRESENT  (added 02-07)
  res/drawable/ic_auto_eq_on.xml       ABSENT   (added 31-07, 2540556)
  res/drawable/ic_auto_eq_off.xml      ABSENT

Neither as a zip entry nor as a name in resources.arsc. Both files are in
git with content and on disk; the older sibling in the same folder is in
the APK. The difference is when they were added.

This runner is self-hosted and the workflow never cleaned, so build/
survives between runs and Gradle's incremental resource merge went stale:
resources present when the cache was built kept working, resources added
afterwards silently never made it in.

The cost was weeks of wrong diagnosis. getResourceId returned 0 for that
icon, PlaybackStateCompat.CustomAction.Builder throws on a 0 icon, and
that throw aborts AudioService.setState BEFORE mediaSession.setActive --
so Android Auto held a frozen, inactive session. On the device that
surfaced as a dead playback screen, a play button that never became
pause, PluriWave losing its pane to whichever app did have an active
session, and audio that played "as if it were not the app". One cause,
four symptoms.

Dart changes always shipped because Dart is recompiled every build, which
is exactly why this hid for so long: every fix appeared to land and
nothing behaved differently.

flutter clean costs build time. It buys the guarantee that what is in git
is what is in the binary, which this project just spent weeks not having.
2026-08-07 00:24:18 +02:00
FreeTLab 346cd2b6b9 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 26s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m37s
2026-08-07 00:18:49 +02:00
FreeTLab e337f6166c merge: reject a local track as a recording source 2026-08-07 00:18:48 +02:00
FreeTLab 54d87190fe fix(grabacion): reject a local track as a recording source
Reported, with this on screen:

  No se pudo iniciar la grabación: Invalid argument(s): Unsupported scheme
  'content' in URI content://com.android.externalstorage.documents/tree/
  primary%3AMusic/document/primary%3AMusic%2F...%2FNew Limit - Smile.mp3

The URI in that message is a local MP3, not a station.
PluriWaveAudioHandler._cambiarFuente sets `emisoraActual` for EVERY source
it plays, so a local track surfaces as an Emisora whose `url` is the SAF
content:// document URI it was opened from. EstadoGrabacion.iniciar only
checked for null, handed that straight to the recorder, and the HTTP
client failed with a message no user can act on.

"It used to work" is exactly right: before local music playback existed,
whatever was playing was always a real station, so the case could not
arise. The recorder never changed.

iniciar() now also requires a real network stream (esEmisoraGrabable) and
falls back to the existing "select a station first" message, which is the
correct guidance here -- recording a local file makes no sense anyway,
it is already on the device. No new l10n key, so no 13-locale churn for a
message that already says the right thing.

Tests: 1158 -> 1161.
2026-08-07 00:18:48 +02:00
FreeTLab e3638ea4a7 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 29s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m38s
2026-08-06 21:48:21 +02:00
FreeTLab a93e5b192b merge: make audio diagnostics visible in release builds 2026-08-06 21:48:20 +02:00
FreeTLab 1d5332453f fix(audio): make the audio diagnostics visible in release builds
Every diagnostic line in the audio path used `dart:developer`'s `log()`.
That function writes to the VM service, which a RELEASE build does not
have — so in the only build that ever runs in a car, all eleven of them
went nowhere. `debugPrint`/`print` do reach logcat in release; `log()`
does not.

That includes the two channels built specifically to end the guessing:
- `registrarErrorAudioService`, which subscribes to
  `AudioService.asyncError` so the plugin's swallowed platform exceptions
  stop vanishing (b0271fa). It moved them from a dropped PublishSubject
  to a dropped log call.
- `_trazarEstadoPublicado`, the published-state trace added in 7054a4c to
  settle why the car's play button never becomes pause.

So "no evidence" was never a quiet app. It was an app writing its
evidence somewhere release builds discard. Several rounds of hypotheses
were argued without data that the app was already producing.

All eleven now use `debugPrint` with a `[PluriWave][Tag]` prefix, so one
filter catches the audio path and the existing alarm lines together:

  adb logcat | grep PluriWave

No behaviour changes. Tests: 1158, unchanged.
2026-08-06 21:48:20 +02:00
FreeTLab 490ae29bd4 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 27s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m47s
2026-08-06 19:50:07 +02:00
FreeTLab 7d86ddfae9 merge: duck instead of pause, keep the service foreground, restore local music 2026-08-06 19:50:07 +02:00
FreeTLab 3398d02a43 fix(auto): keep the service alive through interruptions, restore local music
Four car reports, two root causes.

1. Local music vanished from the Android Auto menu. Self-inflicted, by
c1afe72 yesterday.

That commit moved registrarFuenteNavegacion above every await to keep a
headless engine from dying before it ran -- but left
registrarFuenteMusicaLocal below `await SharedPreferences.getInstance()`.
The root menu decides whether to offer "Música Local" with
`fuenteLocal != null && await fuenteLocal.hayCarpetaConfigurada()`, so
the car could now get a root response in the window between the two
registrations, find a null source, and be told there is no local music.
Android Auto caches the browse root, so it stayed missing for the whole
session. Before the reorder both registrations sat together after the
await and the window did not exist.

FuenteMusicaLocalAutoImpl never needed prefs to be CONSTRUCTED -- it
resolves them lazily per call, the same convention ServicioAlarmas uses
-- so it now registers beside the station source, above every await, and
the window is gone rather than narrowed.

2. PluriWave disappeared from the Auto pane mid-drive, the playback
screen sat frozen, and the equalizer was lost on every navigation
prompt. One cause for all three.

androidWillPauseWhenDucked: true made audio_session translate Android's
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK into a full PAUSE. In a car that
fires constantly: every navigation instruction, every speed-camera
warning, every voice assistant. And a pause publishes playing:false,
which AudioService.setState turns into exitPlayingState() and, with
androidStopForegroundOnPause: true, into stopForeground(...). The
plugin's own doc for that flag says what follows: "while in this lower
priority state, the operating system will also be able to kill your
service at any time to reclaim resources". A killed service is a media
session that vanishes from the car pane -- and another media app takes
the slot.

Now the app ducks instead of pausing, so playing stays true and session,
notification and pane all survive an interruption; and the service stays
foreground even on a real pause, so a genuine one is not a death
sentence either. androidNotificationOngoing goes to false because the
plugin asserts it implies stopForegroundOnPause, and nothing is lost: a
foreground service already forces the notification to be ongoing.

A real, non-duckable focus loss (a phone call) still pauses and still
auto-resumes -- asserted, so the duck change cannot silently turn a call
into a station playing over it.

3. Previous/next on the car playback screen, for stations too.

skipToPrevious/skipToNext are now advertised unconditionally, since
Android Auto only draws those buttons when the app declares support.
They are no longer inert without a local queue: they walk the narrowest
list the current station belongs to -- favourites, then my stations,
then the catalogue -- wrapping at both ends, because a button that goes
dead at the end of a list reads as broken on a screen with no visible
list position. Matching is by uuid so a refreshed snapshot still
resolves, and a station in no list leaves playback untouched.

The equalizer toggle still fits alongside them: prev/next take their two
reserved slots and the equalizer claims the remaining custom-action room
because construirControlesTransporte places it before MediaControl.stop.

The phone notification is deliberately untouched: `controls` still gates
skip on an active queue, so nativeActions and
androidCompactActionIndices are byte-identical. Only systemActions
changed, and only the car reads those.

Tests: 1146 -> 1158.
2026-08-06 19:49:57 +02:00
FreeTLab 1d8c9c57bc merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 29s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m31s
2026-08-06 17:18:54 +02:00
FreeTLab c1afe72aec fix(arranque): stop a headless engine from dying before runApp
Reported: with Android Auto connected, the car screen sometimes came up
completely BLACK, and opening the app on the phone then showed a
completely WHITE screen until the app was force-killed and reopened.
Never without Android Auto.

The user guessed portrait-only plus a landscape phone made the app "go a
bit crazy". Right file and right trigger, different mechanism -- a broken
layout renders overflow stripes or a red error box, never white. White
means nothing was ever built, so runApp had not run.

Verified in the plugin source: AudioServiceActivity.provideFlutterEngine
returns AudioServicePlugin.getFlutterEngine(context), which CREATES the
engine and executes the Dart entrypoint the first time it is asked. When
the car binds the MediaBrowserService before the app is opened, that
first ask is the service -- so main() runs HEADLESS, with no Activity.

SystemChrome.setPreferredOrientations travels the flutter/platform
channel, whose handler (PlatformPlugin) is installed by the Activity.
Headless there is nobody to answer it, so the call throws
MissingPluginException or never settles. It was the FIRST await in
main(), which made it fatal twice over: registrarFuenteNavegacion sits
below it and never ran, leaving getChildren with no source (black car
screen), and runApp was never reached. Opening the app then reused that
same cached, already-dead engine -- white screen. Only a force-kill,
which disposes the cached engine, recovered it. That is exactly the
workaround that was reported, and it is what makes the diagnosis fit
every detail rather than most of them.

Three changes, smallest first:
- The Android Auto browse registration moves above every await. It
  depends on nothing, and anything before it is a place to get stuck.
- The orientation call is no longer awaited. It is a display preference,
  never a prerequisite for runApp, and _OrientacionResponsiveApp already
  re-applies it in didChangeDependencies -- the only moment it can take
  effect anyway.
- aplicarPoliticaOrientacion swallows everything and logs, so the
  headless failure can never propagate again.

The policy itself is unchanged and now pure and tested
(orientacionesPara): phones portrait, >=600dp everything.

Tests: 1141 -> 1146.
2026-08-06 17:18:53 +02:00
FreeTLab 14985f6417 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 26s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m48s
2026-08-06 01:28:43 +02:00
FreeTLab a6cdf0e72c fix(auto): advertise the transport actions Android for Cars requires
Reported: on the Android Auto playback screen the play/pause button stays
on PLAY while audio is audibly playing, and "it used to work, in the
latest versions it doesn't".

Previous rounds looked for a regression in this repo's audio commits and
found none: every playbackState.add site publishes playing:true with a
ready processingState, and AudioService.getPlaybackState maps that to
STATE_PLAYING. That search was aimed at the wrong thing.

The Android for Cars guide ("Enable playback control") is explicit:
"Android Auto and AAOS display playback controls based on the actions
that are enabled in the PlaybackStateCompat object. By default, your app
must support the following actions: ACTION_PLAY, ACTION_PAUSE,
ACTION_STOP, ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH."

systemActions has carried only `seek` + `stop` since e9d1f67, the first
commit of the project -- git log -S confirms it was never once edited. So
the required actions have never been advertised, and no audio commit can
explain a change in behaviour. Android Auto ships as its own app and
updates itself, which is how a working screen breaks with a clean repo
history. That fits the report better than any commit here does.

The phone notification was never affected: it builds its play/pause
button from `controls`, not from these bits, which is exactly why the
symptom is car-only.

Skip actions stay conditional on an active queue on purpose -- the same
guide notes Auto reserves the prev/next slots for them and gives the
space to custom actions when the app does not support them, and that is
the space the equalizer toggle needs.

ACTION_PLAY_FROM_SEARCH is now implemented rather than merely claimed:
advertising it unimplemented would have the car's assistant accept "play
Radio X" and silently do nothing. emisoraParaBusqueda ranks exact name,
then prefix, then substring, then country, accent- and case-insensitive
because voice transcription rarely gets diacritics right; favourites are
searched first so they win a name tie, and a miss plays nothing rather
than something arbitrary.

Still a hypothesis for the play/pause symptom, not a confirmed fix -- it
is documentation-backed and cheap, but only a head unit can confirm it.

Tests: 1132 -> 1141.
2026-08-06 01:28:42 +02:00
FreeTLab 1e7c0daa90 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 30s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m30s
2026-08-05 23:07:20 +02:00
FreeTLab bca0a9bbb7 merge: dart-side snooze anchor guard, Auto progress bar, drop EQ folder 2026-08-05 23:07:19 +02:00
FreeTLab 80538900db fix(alarmas,auto): guard the last unguarded snooze path, surface car progress
Continuation of 7054a4c: the native anchor guard alone did not fix the
reported ~1444-minute snooze, because Dart runs AFTERWARDS on the
pre-notice path and had no guard at all.

1. Snooze from the pre-notice notification, root cause.

app.dart dispatches AFTER the receiver's postponeNext already ran and
after startActivity, and EstadoAlarmas.posponerProximaDesdePreaviso took
whatever occurrence it was handed on faith, then persisted and
rescheduled from it -- the last snooze path in the codebase with no
occurrence guard. The occurrence itself is not trustworthy either:
app.dart falls back to alarma.proximaEjecucion when the native event
carries none, and that field can already point at tomorrow.

_ocurrenciaSonando is generalized into _ocurrenciaValida with a caller-
supplied forward allowance and an externally-proposed occurrence that
still has to survive the same check. The pre-notice path gets a
ventanaPreaviso (30 min, matching AlarmScheduler.PRE_NOTICE_MILLIS) --
unlike the ringing-screen guard, this occurrence legitimately has not
happened yet, which is exactly why the existing helper could not just be
reused here.

Also heals state already poisoned by the missing guard: a snoozeHasta
parked past a 3-hour ceiling (posponerEjecucion clamps to 120 minutes,
so anything beyond that is corruption, not a long real snooze) is
dropped on recalculation. Without it, an alarm poisoned on a build
before this fix keeps reporting tomorrow after updating, and the user
reasonably concludes nothing changed.

2. Android Auto: no progress bar or time labels on a local track.

updatePosition was never set anywhere in the handler, so it sat at its
Duration.zero default while copyWith refreshed updateTime to now on every
push -- the car was told "position 0, as of right now" on every event, a
bar pinned at the start regardless of what was actually playing. Now set
from _player.position on both the player-state and buffered-position
listeners (the latter ticks ~2/s, which is what keeps the car's bar
smooth between player-state events). Also stream the MediaItem's
duration once the source reports it -- Auto draws no bar at all without
one, and radio streams correctly keep reporting none (live audio has no
length).

3. Android Auto: drop the Ecualizador browsable folder.

Owner decision after driving with it: a browsable six-preset list is
more interaction than a driver wants, and on/off from all three player
views (already fixed in 7054a4c to win the custom-action slot) is the
only equalizer control that belongs in the car. Preset selection stays on
the phone. This lands back on the redesign mockup's original rule ("sin
carpeta de ecualizador"), now for a road-tested reason. getChildren keeps
answering the folder's id transitionally, since a head unit can have the
old tree cached for a session or two.

The two "raiz always includes/ends with Ecualizador" tests are replaced,
not regressed -- same move the codebase already made once in the other
direction for the same folder.

Tests: 1127 -> 1132.
2026-08-05 23:07:10 +02:00
FreeTLab 2bafc7e5ac merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 26s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m44s
2026-08-05 10:17:50 +02:00
FreeTLab c4a39ee8ed merge: native snooze anchor guard and Android Auto EQ slot 2026-08-05 10:17:48 +02:00
FreeTLab 7054a4c871 fix(alarmas,auto): guard the native snooze anchor, surface the EQ in the car
Three reported issues, two fixed and one instrumented.

1. Posponer left the alarm snoozed ~1444 minutes (24h04m).

Traced end to end in Kotlin. onAlarmFired runs from the receiver BEFORE the
ringing notification exists, and persists snoozeOriginMillis = null plus a
triggerAtMillis already advanced to TOMORROW by computeNextTriggerMillis.
snooze() then anchored on `spec.snoozeOriginMillis ?: spec.triggerAtMillis`
and picked up tomorrow. The existing clamp could not catch it: it only
rescues anchors in the PAST, so an anchor +24h out sails through. The
countdown text is honest -- ceilMinutes(snoozeUntil - now) over Dart's own
template -- the corrupt value is snoozeUntil. With N=5 and a tap at T+1min
the arithmetic lands on 1444 exactly.

This is the defect a9da855 fixed on the Dart side with
EstadoAlarmas._ocurrenciaSonando, after 9c7cf4e had fixed only one of two
adjacent callers. The native lane never got that guard. Now it has
anchorOccurrenceMillis, with a per-surface forward allowance: none for
snooze() (the ringing notification closes an occurrence that has arrived)
and a full PRE_NOTICE_MILLIS for postponeNext() (the pre-notice
notification's occurrence legitimately has not).

No Kotlin test source set exists in this project, so CI cannot verify this
and no Dart test sees it (all use FakePuertoAlarmasAndroid). Verified by
reading; needs an on-device pass.

2. The equalizer toggle stayed invisible on the Android Auto playback
screen even on v1.2.14+136, which does contain it.

On Android 13+ createCustomAction (AudioService.java:466-469) turns
MediaControl.stop into a custom action too, so the car receives TWO in list
order and stop was first -- a head unit exposing one custom-action slot
shows that and buries the rest in an overflow menu. The equalizer now
precedes stop and wins the slot; it is the better occupant, since the car
has its own path to stop playback while the equalizer is reachable no other
way from that screen.

No platform detection needed, and the phone notification is untouched on
every API level: nativeActions comes out [prev?, play/pause, stop, next?]
below 13 and [prev?, play/pause, next?] on 13+, exactly as before. Both are
now asserted.

The list also moves to a public construirControlesTransporte. The guard
test used to re-declare its own copy of the shape, so it stayed green while
asserting a list lib/ no longer produced. It calls the real builder now.

3. Android Auto shows PLAY while a station is audibly playing: NOT fixed,
deliberately.

The car takes that icon from PlaybackStateCompat.getState()
(AudioService.java:601-611), not from controls -- so none of the recent
controls work can be the cause. All eight playbackState.add sites were
audited and none publishes playing:false while audio runs, which leaves no
traced input to fix. A proposed resync off bufferedPositionStream was
rejected: it can publish a spurious idle, which AudioService.java:565-567
turns into stop() and tears down the foreground service -- the exact
regression abc6b47 fixed, on the highest-frequency listener in the handler.

Added instead a change-gated trace of the state actually published, with
eqDisponible alongside it (that flag gates the equalizer action and is
otherwise unobservable). One car session with `adb logcat -s ServicioAudio`
settles both this and issue 2.

Tests: 1124 -> 1127.
2026-08-05 10:17:41 +02:00
FreeTLab 3a4dc3b4b1 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 25s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m37s
2026-08-03 22:04:45 +02:00
FreeTLab 04300592e0 fix(alarmas): heal alarms already poisoned by the old Detener anchor
The anchor fix stops NEW damage, but devices that ran the buggy build
still carry a future occurrence in ultimaEjecucionGestionada in
SharedPreferences. _esValida rejects any candidate matching it, so the
affected alarm would keep skipping that day with nothing in the UI to
explain it -- which reads as "still broken" rather than "fixed".

_recalcular now drops an ultimaEjecucionGestionada that is meaningfully
in the future. An occurrence cannot have been handled before it happens,
so such a value is corrupt by definition, and dropping it can only ever
restore a real future ring: the double-fire guard it also feeds needs a
PAST occurrence to do its job, and those are untouched.

Placed in the recalculation that every load and every mutation already
funnels through, so an affected alarm heals on the next app open with no
user action -- no delete-and-recreate.

Tests: 1122 -> 1124, including one proving a genuine past occurrence is
still preserved.
2026-08-03 22:04:37 +02:00
FreeTLab df9252a621 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 26s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m45s
2026-08-03 21:59:30 +02:00
FreeTLab ae3f96c0d1 merge: fix Detener consuming a future alarm occurrence 2026-08-03 21:59:30 +02:00
FreeTLab a9da855601 fix(alarmas): stop Detener from consuming an occurrence that never rang
Reported: an alarm set for Monday 16:20 never rang, and the "next alarm"
banner showed a different alarm (the next morning's) instead. No
vacation range involved, both alarms active.

finalizarEjecucion anchored the completed occurrence to proximaEjecucion
with no check that it was the one actually ringing. On the native-fire
path the fire-time sync advances proximaEjecucion to the NEXT occurrence
before the user can reach the ring screen, so tapping Detener recorded a
FUTURE occurrence in ultimaEjecucionGestionada.
ServicioProgramacionAlarmas._esValida then rejects that occurrence for
real: a Monday-only alarm stopped today simply never rings next Monday,
and every sibling outranks it in the banner because its own
proximaEjecucion is a week out.

Reproduced at its purest in the second test: with nothing ringing at
09:01 on Monday, Detener pushed a 16:20 alarm to the FOLLOWING Monday.

posponerAlarma already had exactly this guard -- 9c7cf4e, "anchor snooze
to the ringing occurrence, never a future one", written after the same
failure showed up as a snooze armed a day out. It was applied to the
snooze path and never to the stop path, which sat ten lines below it in
the same file with the identical hazard.

Both paths now share one _ocurrenciaSonando helper so they cannot drift
apart again, and the reason lives in its doc comment rather than in a
comment on one of the two callers.

Also drops snoozeHasta from the stop path's candidate chain: a pending
snooze target is in the future by definition, and snoozeOrigen already
covers a ring that follows a snooze.

Tests: 1120 -> 1122.
2026-08-03 21:59:19 +02:00
FreeTLab 34d04bbc1e merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 31s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m24s
2026-08-03 21:34:09 +02:00
FreeTLab e962b8ac58 merge: restore the Android Auto equalizer toggle and add user presets 2026-08-03 21:33:55 +02:00
FreeTLab f2f706b342 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.
2026-08-03 21:32:09 +02:00
FreeTLab d3999a20fb fix(audio): restore the media notification by keeping custom actions out of controls
Build & Deploy PluriWave / Análisis de código (push) Successful in 26s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m43s
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.
2026-08-01 20:33:51 +02:00
FreeTLab f2d7e98813 merge: incorporate the CI version bump 2026-08-01 20:33:51 +02:00
FreeTLab cacd3ece57 fix(audio): keep custom actions out of the media notification controls
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.
2026-08-01 20:33:34 +02:00
FreeTLab abc6b47ffb fix(audio): stop tearing down the media notification on every station change
Build & Deploy PluriWave / Análisis de código (push) Successful in 25s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m16s
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.
2026-08-01 19:24:48 +02:00
FreeTLab dca19cd1ab merge: incorporate the CI version bump 2026-08-01 19:20:24 +02:00
FreeTLab 1b126d5147 fix(audio): publish idle from stop() instead of trusting the player
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.
2026-08-01 19:19:30 +02:00
FreeTLab 6da3e69f7e fix(audio): stop emitting a transient idle during a source change
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.
2026-08-01 19:15:53 +02:00
FreeTLab b0271fa953 feat(audio): log AudioService.asyncError instead of swallowing it
`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.
2026-08-01 19:15:53 +02:00
FreeTLab 61c97858a1 merge: incorporate the CI version bump
Build & Deploy PluriWave / Análisis de código (push) Successful in 26s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m50s
2026-08-01 13:00:26 +02:00
FreeTLab 802b62f578 feat(tutorial): add 9-screen help/tutorial carousel
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.
2026-08-01 12:57:07 +02:00
FreeTLab b09d644a2c merge: incorporate main's safearea/auto-order/vacaciones fixes 2026-08-01 12:54:53 +02:00
FreeTLab 344af83e16 fix(ui,auto,alarmas): safe area, Android Auto order, vacation delete
Build & Deploy PluriWave / Análisis de código (push) Successful in 25s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m23s
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.
2026-08-01 12:52:15 +02:00
FreeTLab 257f1bbc68 merge: incorporate the CI version bump 2026-08-01 12:49:51 +02:00
FreeTLab f4f9e87970 docs(alarmas): fix helper name typo in vacation delete comment 2026-08-01 12:08:14 +02:00
FreeTLab 597701f497 fix(alarmas): add a delete action to the vacation range edit sheet
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.
2026-08-01 12:06:00 +02:00
FreeTLab 4d54908be6 fix(ui): add top-inset awareness to PluriRootHeader
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).
2026-08-01 12:00:48 +02:00
FreeTLab a949b4503d feat(tutorial): repoint Ajustes "Ayuda y tutorial" to the carousel
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.
2026-08-01 11:49:11 +02:00
FreeTLab e297413145 feat(tutorial): wire tutorial carousel into the first-launch flow
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.
2026-08-01 11:44:25 +02:00
FreeTLab 015a20a823 feat(tutorial): add 9-screen help/tutorial carousel
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.
2026-08-01 11:34:53 +02:00
FreeTLab cfd8bc9e6a fix(auto): preserve phone-chosen station order in Android Auto folders
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.
2026-08-01 11:26:24 +02:00
FreeTLab d945e1a313 fix(alarmas): stop scheduling failures from being silent
Build & Deploy PluriWave / Análisis de código (push) Successful in 28s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m24s
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.
2026-07-31 23:27:58 +02:00
FreeTLab c507218462 merge: incorporate the CI version bump 2026-07-31 23:27:57 +02:00
FreeTLab f2528c930b fix(alarmas): decode native failures with the real channel key names
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.
2026-07-31 23:27:45 +02:00
FreeTLab a8dca83cd9 feat(alarmas): surface the three native scheduling failures in Dart
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.
2026-07-31 23:24:01 +02:00
FreeTLab 7722f204ca feat(alarmas): verify native registration after a successful save
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.
2026-07-31 21:26:41 +02:00
FreeTLab c107c0e18a feat(alarmas): surface scheduling failures on the alarm card
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).
2026-07-31 21:19:57 +02:00
FreeTLab fd1b91fe9e fix(alarmas): wire scheduling failures into per-alarm exceptions
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.
2026-07-31 21:08:21 +02:00
FreeTLab 47d0b8a053 feat(alarmas): record and clear per-alarm scheduling failures
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.
2026-07-31 21:00:51 +02:00
FreeTLab 88bd251eba fix(alarmas): scope schedule-skip exceptions to skipNext only
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.
2026-07-31 20:59:02 +02:00
FreeTLab 3f80291e78 feat(auto): equalizer folder in the browse tree, one toggle on playback
Build & Deploy PluriWave / Análisis de código (push) Successful in 26s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m14s
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
2026-07-31 19:37:14 +02:00
FreeTLab 88818cf88c feat(alarmas): full Android reliability diagnostics with actionable fixes
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.
2026-07-31 19:30:36 +02:00
FreeTLab f19666508d fix(auto): remove the preset-cycling custom action, superseded by folder
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.
2026-07-31 19:15:31 +02:00
FreeTLab 8423ccdd0c 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.
2026-07-31 19:11:01 +02:00
FreeTLab 049ab78acb feat(alarmas): replace one-line reliability button with full diagnostics screen
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.
2026-07-31 19:10:52 +02:00
FreeTLab ef9705a30e feat(alarmas): add pure diagnostic mapping and autostart-guidance logic
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.
2026-07-31 18:32:42 +02:00
FreeTLab 25405564ee 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.
2026-07-31 18:11:57 +02:00
FreeTLab c8b2c4d2d6 feat(auto,eq,alarmas): address the second round of on-device feedback
Build & Deploy PluriWave / Análisis de código (push) Successful in 24s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m37s
- 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.
2026-07-31 01:12:13 +02:00
FreeTLab eba4eba397 merge: incorporate the CI version bump 2026-07-31 01:12:13 +02:00
FreeTLab 4168dc5019 fix(alarmas): show which days a weekday alarm actually fires on
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.
2026-07-31 01:05:46 +02:00