05f70af7f1bd7aec9a4e6344536c18cabead5fa4
19
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8a71bc237f |
feat: restaurar los grupos de favoritos al importar y recordar la lista del coche
Dos fallos reportados desde el uso real en el coche. Los grupos de favoritos no volvian al importar La exportacion nunca estuvo rota: los grupos viajaban desde siempre y la asignacion de cada emisora va dentro de cada favorito como grupo_id. El fallo estaba solo al importar. La restauracion reutilizaba ServicioFavoritos.agregar, que fuerza "sin asignar" a proposito, porque una emisora recien marcada como favorita no tiene grupo. Correcto para esa ruta, destructivo como primitiva de restauracion: los grupos volvian vacios y todo aterrizaba en Sin asignar. Se separa la ruta de restaurar, que respeta el grupo_id del backup y su orden. Ningun test lo detectaba porque el doble de pruebas era infiel: el fake conservaba el grupo que la implementacion real destruia, y no tenia restaurarGrupo, asi que esa ruta estaba sin cubrir. Los tests existentes lo esquivaban pasando siempre una lista de grupos vacia. Se corrige el doble. No hace falta subir la version del formato: el envoltorio ya llevaba todo. El coche perdia la lista al reconectar El contexto de reproduccion vivia solo en memoria, y el motor que arranca Android Auto es un proceso nuevo sin interfaz ni EstadoRadio, asi que al reconectar se reproducia la ultima emisora sin saber a que lista pertenecia y siguiente/anterior no hacian nada hasta entrar a favoritos a mano. Ahora se persiste el TIPO de contexto y, cuando aplica, el id del grupo, y se resuelve contra las listas vivas en cada salto: si el grupo cambia de contenido entre sesiones, el coche ve lo actual y no una foto vieja. Cadena de repliegue, decidida por el propietario: grupo vivo con la emisora dentro -> se recorre el grupo grupo vivo sin la emisora -> se permanece en el grupo, primera grupo vivo pero vacio -> se ensancha a favoritos grupo borrado -> favoritos sin favoritos -> comportamiento actual Honrar un grupo de una sola emisora exigia levantar dos barreras, no una: el resolutor y el propio _saltarEmisora, que se negaba a nombrar un grupo con menos de dos miembros. emisoraVecina queda intacta: su contrato de no saltar a ciegas es deliberado y se usa desde mas sitios, asi que el caso de "la emisora se salio del grupo" se trata en el flujo del salto. La puerta de entitlement no cambia: un conductor sin premium sigue recorriendo solo el conjunto gratuito, y un contexto congelado mientras pagaba se descarta en vez de recorrerse. Suite completa: 1501 pasan, 2 omitidos. flutter analyze mantiene los 5 avisos preexistentes. |
||
|
|
575ba793ae |
fix: corregir ecualizador desincronizado, musica local en Android Auto y bloqueo del paywall
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.
|
||
|
|
9cfa5ac17d |
fix(iap): address code review defects in freemium/IAP change
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] |
||
|
|
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. |
||
|
|
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
|
||
|
|
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. |
||
|
|
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 |
||
|
|
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.
|
||
|
|
d0abe32eef |
fix(audio): survive audio_service init hang on Android Auto cold start
AudioService.init has no internal timeout and an unhandled onConnectionSuspended case in its MediaBrowser self-bind; under bind contention with the car's connection it can hang forever, so runApp never ran (black car screen, white phone UI until process kill). Race init against an 8s timeout without ever re-calling it: on timeout run a bootstrap app that waits on the same future, wires the handler exactly once when it resolves, reports errors via FlutterError, and swaps to the real app. Auto browse sources now register before the init await since they take no handler dependency. |
||
|
|
6ae7e378c4 |
feat(auto): browse and play local music folders in Android Auto [size:exception]
Phase 1: pick a device folder via SAF (persisted grant, no new permission), browse its nested subfolders/tracks as a 5th Android Auto root folder (hidden until configured), and play tracks through the existing pipeline (EQ, art rotation, cold-start-safe source). No metadata/sort/filter/shuffle yet -- filename is the title, generic rotating art is the placeholder; deferred to a follow-up phase. Adds a new pluriwave/file_actions native method (listAudioChildren) and an onActivityResult override in MainActivity for the SAF folder picker -- both static-review-only, no Android build available here. |
||
|
|
35bb180612 |
feat(auto): browsable Android Auto media tree with play-by-id [size:exception]
Expose PluriWave to Android Auto (projected) as a media app: - Declare car media support (automotive_app_desc.xml + manifest meta-data) so Android Auto discovers the existing MediaBrowserService. - New navegacion_auto.dart: ConstructorArbolAuto builds the browse tree (Favoritos / Todas las emisoras / Mis emisoras, 50-item cap, stable emisora:<id> media ids), reproducirPorMediaId routes a car tap to the existing playMediaItem pipeline, FuenteEmisorasAutoLocal serves the tree cold-start-safe (local favorites/custom stations before Flutter UI runs). - PluriWaveAudioHandler overrides getChildren/getMediaItem/playFromMediaId as thin delegations; playback pipeline untouched. - EstadoRadio pushes live station snapshots to the browse source and reconciles the selected station when playback starts from the car. - Every playable item ships title + artUri; stations without logo fall back to a bundled default art (android.resource://). Tests: 52/52 green (10 new navegacion_auto, 2 new estado_radio, plus audio safety-net suites). Handler overrides and native XML are static-review-only (no Android build env). Size exception approved for a single reviewable commit. |
||
|
|
8f2bf2bdd6 |
feat(notifications): add branded monochrome icon and color to all notifications
Replace generic system icons (info bubble, stock alarm clock) with a custom equalizer-bars vector drawable across all 4 notification builders: pre-notice, snooze countdown, ringing alarm, and the audio player. Apply the app's cyan brand color to the 3 alarm notifications that previously had none. Audio notification now explicitly declares its icon instead of falling back to the full-color launcher icon, which Android was auto-silhouetting into an illegible status-bar blob. |
||
|
|
202bef3539 |
feat(ui): design token discipline, accessibility and i18n pass
- Replace all hardcoded Color literals outside lib/tema with theme tokens (new static brand palette in PluriWaveTokens); media notification uses the brand color instead of the Material default purple - Favorite button on station cards grows to a 48dp target and becomes an independent semantics node for screen readers (Semantics container fix) - All flutter_animate call sites route through the PluriAnimate reduced-motion gate (zero direct .animate() left) - Locale-aware short dates via intl DateFormat (new lib/l10n/formato_fechas.dart) replacing the hardcoded DD/MM/YYYY; proper plural messages for the favorites counter; example stream URL as a localized key - all 13 locales - Rounded shimmer placeholders matching card radii; shimmer loading state in search instead of a bare spinner; rounded icon variants unified in settings; bottom-sheet conventions on the custom station form - Fix latent debug crash: vacation editor read AppLocalizations in initState - 11 new tests (121 total green), flutter analyze clean |
||
|
|
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 |
||
|
|
ffe1c41458 | eliminados los snooze | ||
|
|
5fd3d6deb9 |
feat(v0.3.0): ecualizador + favoritos en tarjeta + emisoras custom + export/import + fix MainActivity
Flutter CI/CD — PluriWave / Test + Build (push) Has been cancelled
- MainActivity: extiende AudioServiceActivity (fix pantalla en blanco) - ServicioAudio: AndroidEqualizer en AudioPipeline, aplicarPreset(), setBanda() - PresetEcualizador: modelo independiente (Flat/Rock/Pop/BassBoost/Jazz/Voz) - EcualizadorWidget: 5 sliders verticales + PresetsEcualizadorWidget - TarjetaEmisora: botón favorito visible en grid y lista (toggle con SnackBar) - EstadoRadio: emisoras custom (CRUD), export/import JSON v1, presets por emisora - PantallaAjustes: ecualizador interactivo, form añadir emisora, backup export/import - pubspec: +file_picker ^8.1.7, +uuid ^4.5.1 |
||
|
|
81db383a47 |
fix(v0.3.0): audio background + emisoras rotas + errores toast + icono
- ServicioAudio: delega a PluriWaveAudioHandler (audio_service) para mantener audio vivo en background. AudioService.init() en main.dart. onTaskRemoved() libera player. mediaItem con nombre/artista/artwork. - ServicioRadio: lastcheckok=1 en todas las peticiones — solo emisoras verificadas como funcionales por Radio Browser API. - EstadoRadio: errorStream (broadcast) para errores de reproducción y búsqueda. App.dart suscribe y muestra SnackBar flotante 3s. Los errores de carga de lista siguen como banner inline. - Icono: generado con SDXL (morado, ondas radio blancas, Material You). 5 densidades Android (48-192px), ic_launcher_round añadido. |
||
|
|
e9d1f67aa4 |
feat(mvp): PluriWave Fase 1 — estructura completa de la app
Flutter CI/CD — PluriWave / Test + Build (pull_request) Has been cancelled
- Modelo Emisora: campos completos Radio Browser API (fromApi + fromMap) - ServicioRadio: cliente Radio Browser API (populares, tendencias, buscar por nombre/país/idioma/tag) - ServicioAudio: just_audio + audio_service wrapper (play/pause/stop/toggle, fade, background handler) - ServicioTimer: countdown con fade out gradual (15/30/60/90 min) - ServicioFavoritos: actualizado a v2 con campos codec/bitrate/votes/clickcount - EstadoRadio: ChangeNotifier global con Provider - PantallaInicio: grid emisoras populares, chips género, shimmer loading, pull-to-refresh - PantallaBuscar: SearchBar + filtros país/idioma, lista resultados - PantallaFavoritos: ReorderableListView + swipe-to-delete (Dismissible) - TarjetaEmisora: card + modo compacto ListTile, cached_network_image, shimmer fallback - MiniReproductor: barra inferior persistente con stream de estado - app.dart: MaterialApp + Provider + NavigationBar + timer dialog - main.dart: punto de entrada limpio - AndroidManifest.xml: permisos INTERNET + FOREGROUND_SERVICE + audio_service receivers |
||
|
|
00b11f88df |
init: proyecto Flutter PluriWave — radio mundial con ecualizador
- Flutter create con org es.freetimelab - pubspec.yaml con dependencias core (just_audio, audio_service, provider, sqflite) - README con features y stack - Assets preparados |