Commit Graph
12 Commits
Author SHA1 Message Date
FreeTLab a0fae57219 fix: el ecualizador del coche aplica el preset real y en el orden correcto
Reportado desde el uso real: desde el movil el ecualizador va bien, pero el
boton de Android Auto a veces no hace nada y a veces suena como si se aplicara
una doble ecualizacion.

El preset del handler nunca se sembraba desde disco

`_presetActual` arrancaba en `flat` a fuego. registrarHandler sembraba el flag
de encendido pero no el preset, asi que en un motor donde la interfaz del
telefono nunca corrio -- el que arranca Android Auto -- el toggle del coche
aplicaba `flat`, o lo que hubiera quedado, en vez del preset del usuario. Es la
misma clase de fallo que ya se corrigio para el flag: aquel recibio un puerto
headless y el preset se quedo fuera. Ahora tiene el suyo, con la misma forma:
opcional, el fallo se traza y cae al valor por defecto, nunca propaga.

La siembra respeta un preset ya elegido por EstadoEcualizador, que es mas rico
que la clave principal, para que la lectura de disco en vuelo no lo pise.

El efecto se habilitaba antes de escribir las ganancias

La ruta era setEnabled -> setEnabled -> ganancias: `aplicarPreset` volvia a
llamar a setEnabled por su cuenta. Entre la habilitacion y la escritura sonaban
las ganancias anteriores, y ese hueco es lo que se percibia como doble
ecualizacion. Ahora una funcion pura devuelve los pasos en orden y ambas rutas
la recorren: ganancias primero, habilitacion despues.

Las ganancias NO se resetean al apagar, y es deliberado: setEnabled(false)
puentea el efecto sin liberarlo ni limpiar sus niveles, y la ruta de encendido
los reescribe enteros antes de habilitar, asi que no queda ninguna ventana de
ganancia rancia que un reset pudiera cerrar.

El boton desaparecia en cada cambio de emisora

`_recrearPlayer` bajaba `_eqDisponible` sin republicar controles, asi que cada
cambio de emisora emitia al menos un estado sin la accion de EQ. Peor: las
llamadas nativas estan detras de ese flag, de modo que un toggle en esa ventana
cambiaba el icono sin tocar el audio. Ahora el unico que lo escribe es
`_activarEcualizador`.

Mantenerlo optimista exigia quitar de la ruta del toggle el `await
_eq.parameters`, que es un Completer que solo se completa cuando el reproductor
se engancha: esperarlo dejaba el boton pendiente durante toda la carga, y para
siempre si la carga fallaba. Se cachean los parametros al activarse.

Los fallos nativos dejan de ser mudos

El `catch (_) {}` ocultaba que la llamada nativa habia fallado y dejaba el icono
afirmando un estado que el audio no tenia. Ahora se traza, y un fallo al
habilitar revierte el flag, republica los controles y no persiste.

EstadoEcualizador adopta lo que el motor acepto en vez de asumir que su peticion
prospero: sin eso, el telefono escribia en disco un valor que el handler acababa
de rechazar, reabriendo la divergencia que el dueño unico habia cerrado.

Suite completa: 1515 pasan, 2 omitidos. flutter analyze mantiene los 5 avisos
preexistentes.
2026-09-06 00:08:09 +02:00
FreeTLab 3449e2cb79 fix: corregir ecualizador desincronizado, musica local en Android Auto y bloqueo del paywall
Build & Deploy PluriWave / Análisis de código (push) Successful in 27s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 3m23s
Tres fallos reportados en uso real, con sus causas raiz verificadas en codigo.

1. Ecualizador: el estado no tenia dueño unico

El handler arrancaba con `_ecualizadorActivo = true` a fuego. El valor
persistido solo llegaba por EstadoEcualizador.cargarPersistido(), alcanzable
unicamente desde el arbol de widgets, que un arranque headless de Android Auto
nunca construye. Resultado: el coche reproducia con el EQ forzado a ON mientras
disco e interfaz decian OFF.

Ahora registrarHandler siembra el flag desde disco en todos los motores y
setEcualizadorActivo persiste por su cuenta, asi que un toggle desde el coche o
la notificacion sobrevive sin EstadoEcualizador. _resincronizarConHandler pasa
a ser adopcion pura de interfaz.

Ademas mapearGananciaNativa enviaba 0 dB al punto MEDIO del rango nativo. Con
un getBandLevelRange() asimetrico, un preset plano metia varios dB de boost
real: la causa del "suena muy alto con el boton apagado". Reescrito para
escalar cada lado contra su propio extremo, de modo que 0 dB es siempre 0.

El dispatch de customAction no tenia ningun test. Se extrae decidirToggleEq y
se cubre contra el handler real. El efecto nativo se re-asierta al reactivarse
el reproductor, porque AudioEffect.setEnabled de just_audio es un no-op
mientras la plataforma esta desacoplada.

2. Musica Local no aparecia en el arbol de Android Auto

hayCarpetaConfigurada() consultaba MethodChannel('pluriwave/file_actions'),
registrado solo en MainActivity.configureFlutterEngine. Sin Activity no hay
handler, invokeMethod lanza MissingPluginException y el catch la confundia con
"permiso revocado", omitiendo el nodo. No dependia del entitlement.

La logica SAF sale a packages/pluriwave_file_actions, un paquete plugin local.
El motor headless que crea audio_service ejecuta GeneratedPluginRegistrant en
su constructor, asi que el canal queda registrado en ambos motores. Repuntar el
manifest a una subclase de AudioService no era viable: AudioServicePlugin
enlaza por ComponentName explicito y la app perderia el audio.

EstadoCarpetaLocal de tres valores separa "sin carpeta" de "canal no
disponible"; la raiz decide por la URI persistida y el subarbol muestra un item
explicativo en vez de una carpeta vacia. La invalidacion del arbol cacheado se
dispara al reanudar con el coche ya suscrito; el guardia anterior miraba
View.maybeOf, que bajo runApp siempre existe, por lo que se gastaba en el
arranque headless y no volvia a dispararse.

3. El paywall bloqueaba las compras

restorePurchases() de in_app_purchase_android emite siempre, y con lista vacia
si no hay nada que restaurar. El `if (compras.isEmpty) return;` se la tragaba,
noEncontrada nunca se emitia y la rama que limpia _compraEnCurso estaba muerta
en produccion. Como comprar y restaurar comparten ese flag, un usuario sin
compras que pulsaba restaurar se quedaba sin poder comprar.

Suite completa: 1366 pasan, 2 omitidos. 17 tests nuevos, todos nacidos rojos y
verificados por mutacion. flutter analyze mantiene los 5 avisos preexistentes.
2026-08-31 14:34:49 +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 4042cf5ffd fix(eq): stop the phone's FM sink from posing as the active output
Build & Deploy PluriWave / Análisis de código (push) Successful in 24s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 2m23s
Regression from the previous commit. Ranking every AudioDeviceInfo type this
build does not name individually ABOVE the built-in speaker was meant to let
a car stereo on LE Audio or an automotive bus win. It also promoted the
internal sinks a phone exposes permanently: on the Xiaomi test device
AudioManager reports TYPE_FM (14) as an output, so getActiveAudioDevice
picked it over the real speaker with nothing connected at all. Confirmed on
device:

  audio_devices.onListen -> {id=other:14:4, type=14, name=2412DPC0AG}

It then reached Dart under an `other:14:4` id whose type is neither the base
speaker nor a known one, slipped past the collision guard and had a preset
row persisted for it -- reinstating the exact symptom this series set out to
kill: a permanent green active-output dot on a device that was not connected.

Replace the deny-by-omission ranking with an explicit allow list of outputs a
user actually connects. The built-in speaker sits below all of them and above
everything else, so any sink that physically exists but is never where media
plays (TYPE_FM, TYPE_BUILTIN_SPEAKER_SAFE, telephony, remote submix) can no
longer be selected. A one-time purge clears the `other:` rows the bad build
persisted; genuine ones re-register on their next connection.

Fix the USB type constant while here: TYPE_USB_HEADSET is 22, not 14, and 14
is TYPE_FM. The Kotlin USB branch hardcoded 14 and the Dart type table
mirrored the same mistake, so the two cancelled out for real USB headsets
while making a phone's own FM sink decode as USB audio. Both now use 22.

Verified with javap against android.jar (android-36) rather than trusting the
comment that introduced the error.
2026-07-25 20:43:47 +02:00
FreeTLab 39ead7bea4 fix(eq): stop the phone speaker from impersonating a Bluetooth device
Build & Deploy PluriWave / Análisis de código (push) Successful in 27s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m39s
deviceToMap handed the builtin_speaker id to EVERY output type its `when`
did not name. A car stereo on LE Audio (TYPE_BLE_HEADSET) or an automotive
bus (TYPE_BUS) therefore arrived in Dart under the phone speaker's own id,
carrying a type that maps to `desconocido` -- which slipped past the
type-only esBase guard and persisted a device entry keyed builtin_speaker.
From that moment on, every playback through the phone's own speaker matched
that entry, so the green active-output dot stayed pinned to whatever the user
had renamed it to (a car, in the reported case) whether or not anything was
connected. The dot was never wrong; the row was poisoned.

Give unnamed output types their own `other:<type>:<address>` id namespace,
and match esBase by id as well as by type so no future native regression can
re-create the collision. A guarded one-time migration purges what the
collision already persisted from all three device-keyed maps.

Fix the ranking too: builtin_speaker sat inside the priority list as a peer,
so any type absent from that list sorted BELOW the always-present speaker
and could never win. The speaker is now the explicit last resort, externally
connected outputs outrank it, and virtual or call-only sinks (earpiece,
telephony, remote submix, SCO) are ranked below it so they can never be
reported as where music is playing.

Route every AudioDeviceInfo.getAddress read through a version-guarded
helper. It is API 28 with minSdk 24, and two pre-existing unguarded calls in
this same method were latent NoSuchMethodError crashes on Android 7-8.1.
Android lint for :app goes from 8 errors to 6.

Also lets the user manage the list, which is how they recover from a bad
entry without waiting for a release: a remove action clears a device's
preset, name and matrix entries, unnamed rows show their transport and
address tail instead of a raw bt_a2dp:AA:BB:... id, and the green dot
finally carries a tooltip and a semantics label saying what it means.

Device QA pending for wired and USB outputs: no jack or adapter available to
exercise those paths. Their detection is unchanged by this commit.
2026-07-25 16:10:36 +02:00
FreeTLab 45b7fc8741 fix(eq): keep valid presets when stored maps are partially corrupt
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m39s
Build & Deploy PluriWave / Análisis de código (push) Successful in 36s
Convert the 4 EQ persistence readers (device presets, matrix presets,
device names, per-station presets) to per-entry tolerant parsing via
the shared persistencia_tolerante helper, so one corrupt entry no
longer discards every sibling preset. The principal-preset reader
gains diagnostic logging on its existing fallback path. No degraded
flag or quarantine here (unlike alarms/stations) since EQ writes are
explicit-only and presets are trivially re-creatable.
2026-07-11 13:00:15 +02:00
FreeTLab b17c582572 fix(devices): cache platform device names, dedupe placeholder ids, purge collided EQ entries
Build & Deploy PluriWave / Análisis de código (push) Successful in 36s
Build & Deploy PluriWave / Build APK + AAB release (push) Successful in 1m47s
Dart half of bt-device-identity. EstadoEcualizador now caches each
device's platform-reported name in memory so the settings screen
shows the device's own Bluetooth name instead of its raw id when no
custom rename exists, and skips auto-creating preset entries for the
composite-placeholder sentinel. Enabling multi-device EQ triggers the
Bluetooth permission request through the new channel contract. A
flag-guarded one-time migration purges only entries keyed by the
exact literal placeholder id from the three per-device preference
maps, since those collided entries cannot be attributed to a device.

Work unit 2/2 of bt-device-identity (Dart state + migration).
2026-07-11 00:25:00 +02:00
FreeTLab 58922de6fc fix(eq): seed device ID at startup and add device management UI
Fix multi-device EQ auto-switching by calling obtenerDispositivoActual()
during cargarPersistido() to seed the initial device ID. Add device
management modal with rename support, EQ preset editing, and connection
status indicator. Translate device UI keys to all 13 locales.
2026-06-28 11:55:15 +02:00
FreeTLab 4632d53eb8 feat(eq): add per-device equalizer with 4-level preset resolution
Introduce multi-device EQ support allowing each audio output device
(built-in speaker, wired, USB, individual Bluetooth by MAC) to have
its own equalizer preset, combined with existing per-station presets
for a full station×device matrix.

- Add DispositivoAudio model and ServicioDispositivoAudio interface
- Add Android platform channel (AudioDeviceCallback) for device detection
- Add iOS AudioDevicesPlugin (AVAudioSession route tracking)
- Extend ServicioEcualizador with device and matrix persistence keys
- Implement 4-level resolution: matrix > station > device > global
- Add advanced EQ settings section with feature toggle (off by default)
- Extend export/import to v3 with backward compatibility
- 184 tests passing, zero analyzer issues
2026-06-27 11:33:53 +02:00
FreeTLab 079e19f0ee feat(audio): audio session integration and runtime robustness
- Integrate audio_session (new servicio_audio_session.dart): incoming calls pause the radio and resume on end, headphone unplug pauses without auto-resume, permanent focus loss never auto-resumes, duck lowers volume
- Add play-intent flag to ServicioAudio so interruption handling and future reconnect logic can distinguish user pause from system-driven stops
- Eliminate read-modify-write race in ServicioAlarmas with an in-memory cache and single-writer queue across all mutations; recalcularTodas persists only when state actually changed
- Convert ServicioAlarmasAndroid static StreamController/handler to injectable instance fields, restoring test isolation
- Inject a single cached SharedPreferences from main.dart across services and state (removes 23 inline getInstance() calls)
- Move configurarLocalizaciones out of MiniReproductor.build() (was running on every rebuild during playback)
- Bound the alarm fire-dedup set (cap 200 entries, 24h pruning)
- 12 new tests (89 total green), flutter analyze clean
2026-06-11 16:25:09 +02:00
FreeTLab 921e972183 fix(player): stabilize equalizer and visualizer
Build & Deploy Pluriwave / Análisis de código (push) Successful in 12s
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 1m50s
2026-05-21 21:59:59 +02:00
Javier Bautista Fernández d579a0e107 feat: Implement startup retry mechanism for custom stations and equalizer persistence
- Added state management for startup retry and custom station handling in `EstadoRadio`.
- Created tasks for implementing strict TDD with RED tests for HTTP failure retries and EQ persistence.
- Developed verification report to ensure compliance with TDD practices.
- Introduced fake services for testing, including `FakeServicioAudio`, `FakeServicioFavoritos`, and `FakeServicioRadio`.
- Implemented widget tests for `PantallaInicio` and `PantallaFavoritos` to validate UI behavior with custom stations.
- Enhanced `ServicioRadio` to support host rotation and retry logic for API calls.
- Established a new configuration file to enforce project constraints and testing rules.
2026-04-27 17:34:04 +02:00