merge: make audio diagnostics visible in release builds

This commit is contained in:
2026-08-06 21:48:20 +02:00
7 changed files with 69 additions and 80 deletions
+11 -10
View File
@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer' as developer;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -94,16 +93,18 @@ StreamSubscription<Object> observarErroresAudio(
); );
} }
/// Default [observarErroresAudio] logger: one `[PluriWave]`-prefixed /// Default [observarErroresAudio] logger: one line per swallowed plugin
/// `developer.log` line per swallowed plugin exception, at the same /// exception.
/// `level: 900` (SEVERE) that `servicio_audio.dart`'s existing error lines ///
/// use, so a single logcat/DevTools filter catches both. /// Uses [debugPrint], NOT `dart:developer`'s `log`. That distinction is the
/// whole reason this channel existed for weeks without ever producing a
/// single line of evidence: `log()` writes to the VM service, which a
/// RELEASE build does not have, so every exception this was built to catch
/// was still being thrown away — just one layer further down than before.
/// `debugPrint` reaches logcat in release, which is the only build that ever
/// runs in the car.
void registrarErrorAudioService(Object error) { void registrarErrorAudioService(Object error) {
developer.log( debugPrint('[PluriWave][ArranqueAudio] AudioService.asyncError: $error');
'[PluriWave] AudioService.asyncError: $error',
name: 'ArranqueAudio',
level: 900,
);
} }
/// Minimal branded bootstrap widget for the degraded path (Design "still /// Minimal branded bootstrap widget for the degraded path (Design "still
+2 -4
View File
@@ -97,7 +97,5 @@ DecisionAvanceCola decidirAvanceCola({
/// distinct `ColaLocal` during the async URI-resolve gap is correctly /// distinct `ColaLocal` during the async URI-resolve gap is correctly
/// detected as stale and aborts the advance, instead of silently racing an /// detected as stale and aborts the advance, instead of silently racing an
/// external play/stop. /// external play/stop.
bool avanceEsValido( bool avanceEsValido(ColaLocal? colaLocalActual, ColaLocal? siguienteEsperado) =>
ColaLocal? colaLocalActual, identical(colaLocalActual, siguienteEsperado);
ColaLocal? siguienteEsperado,
) => identical(colaLocalActual, siguienteEsperado);
+7 -7
View File
@@ -61,9 +61,10 @@ String nombreCarpetaDesdeUri(String treeUri, {required String nombreGenerico}) {
segmento = documentId.substring(ultimaBarra + 1); segmento = documentId.substring(ultimaBarra + 1);
} else { } else {
final ultimosDosPuntos = documentId.lastIndexOf(':'); final ultimosDosPuntos = documentId.lastIndexOf(':');
segmento = ultimosDosPuntos >= 0 segmento =
? documentId.substring(ultimosDosPuntos + 1) ultimosDosPuntos >= 0
: documentId; ? documentId.substring(ultimosDosPuntos + 1)
: documentId;
} }
final recortado = segmento.trim(); final recortado = segmento.trim();
@@ -200,10 +201,9 @@ class FuenteMusicaLocalAutoImpl implements FuenteMusicaLocalAuto {
try { try {
final uri = await _uriPersistida(); final uri = await _uriPersistida();
if (uri == null || uri.isEmpty) return false; if (uri == null || uri.isEmpty) return false;
final valido = await _canal.invokeMethod<bool>( final valido = await _canal.invokeMethod<bool>('hasPersistedPermission', {
'hasPersistedPermission', 'treeUri': uri,
{'treeUri': uri}, });
);
return valido ?? false; return valido ?? false;
} catch (_) { } catch (_) {
// Cold-start / revoked-permission safety (Spec "Permission revoked or // Cold-start / revoked-permission safety (Spec "Permission revoked or
+22 -36
View File
@@ -1,9 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer' as developer;
import 'dart:ui' show Locale; import 'dart:ui' show Locale;
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter/foundation.dart' show debugPrint, visibleForTesting;
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import '../l10n/display_names.dart'; import '../l10n/display_names.dart';
@@ -836,7 +835,14 @@ class PluriWaveAudioHandler extends BaseAudioHandler
/// `eqDisponible` rides along because the equalizer custom action is gated /// `eqDisponible` rides along because the equalizer custom action is gated
/// on it and the flag is otherwise unobservable — one car session answers /// on it and the flag is otherwise unobservable — one car session answers
/// both questions at once: /// both questions at once:
/// adb logcat -s ServicioAudio ///
/// adb logcat | grep PluriWave
///
/// It uses [debugPrint] and NOT `dart:developer`'s `log`, and that is not a
/// style choice. `log()` writes to the VM service, which a RELEASE build
/// does not have — so this trace, and every error line in this file, was
/// invisible in the only build that ever runs in a car. Weeks of "no
/// evidence" were this, not a quiet app. Do not convert these back.
void _trazarEstadoPublicado() { void _trazarEstadoPublicado() {
final s = playbackState.value; final s = playbackState.value;
final traza = final traza =
@@ -846,7 +852,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler
'controles=${s.controls.length}'; 'controles=${s.controls.length}';
if (traza == _ultimaTrazaEstado) return; if (traza == _ultimaTrazaEstado) return;
_ultimaTrazaEstado = traza; _ultimaTrazaEstado = traza;
developer.log(traza, name: 'ServicioAudio'); debugPrint('[PluriWave][ServicioAudio] estado $traza');
} }
/// Binds [construirControlesTransporte] — which holds the whole contract, /// Binds [construirControlesTransporte] — which holds the whole contract,
@@ -909,11 +915,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler
mensaje = _textos.audioErrorGeneric; mensaje = _textos.audioErrorGeneric;
} }
developer.log( debugPrint('[PluriWave][ServicioAudio] Error reproducción: $codigoLog');
'[PluriWave] Error reproducción: $codigoLog',
name: 'ServicioAudio',
level: 900,
);
_detenerReconexion(); _detenerReconexion();
playbackState.add( playbackState.add(
@@ -956,12 +958,10 @@ class PluriWaveAudioHandler extends BaseAudioHandler
} }
_reconectando = true; _reconectando = true;
developer.log( debugPrint(
'[PluriWave] Stall de red, reintento ${_reconexion.intentos}/' '[PluriWave][ServicioAudio] Stall de red, reintento ${_reconexion.intentos}/'
'${_reconexion.maxReintentos} en ' '${_reconexion.maxReintentos} en '
'${_reconexion.retrasoParaIntento(_reconexion.intentos).inSeconds}s', '${_reconexion.retrasoParaIntento(_reconexion.intentos).inSeconds}s',
name: 'ServicioAudio',
level: 800,
); );
playbackState.add( playbackState.add(
playbackState.value.copyWith( playbackState.value.copyWith(
@@ -1207,11 +1207,8 @@ class PluriWaveAudioHandler extends BaseAudioHandler
rethrow; rethrow;
} on Exception catch (e, stackTrace) { } on Exception catch (e, stackTrace) {
_cambiandoFuente = false; _cambiandoFuente = false;
developer.log( debugPrint(
'[PluriWave] Error inesperado en playMediaItem: $e', '[PluriWave][ServicioAudio] Error inesperado en playMediaItem: $e',
name: 'ServicioAudio',
level: 900,
stackTrace: stackTrace,
); );
if (revision == _revisionFuente) { if (revision == _revisionFuente) {
playbackState.add( playbackState.add(
@@ -1264,11 +1261,8 @@ class PluriWaveAudioHandler extends BaseAudioHandler
void _iniciarPlaySinBloquear(MediaItem mediaItem, int revision) { void _iniciarPlaySinBloquear(MediaItem mediaItem, int revision) {
unawaited( unawaited(
_player.play().catchError((Object error, StackTrace stackTrace) { _player.play().catchError((Object error, StackTrace stackTrace) {
developer.log( debugPrint(
'[PluriWave] Error al iniciar ${mediaItem.title}: $error', '[PluriWave][ServicioAudio] Error al iniciar ${mediaItem.title}: $error',
name: 'ServicioAudio',
level: 900,
stackTrace: stackTrace,
); );
if (revision == _revisionFuente) { if (revision == _revisionFuente) {
_gestionarErrorReproduccion(error); _gestionarErrorReproduccion(error);
@@ -1553,11 +1547,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler
if (destino == null) return; if (destino == null) return;
await playMediaItem(mediaItemParaEmisora(destino, l10n: _textos)); await playMediaItem(mediaItemParaEmisora(destino, l10n: _textos));
} catch (e) { } catch (e) {
developer.log( debugPrint('[PluriWave][ServicioAudio] Error saltando de emisora: $e');
'[PluriWave] Error saltando de emisora: $e',
name: 'ServicioAudio',
level: 900,
);
} }
} }
@@ -1720,10 +1710,8 @@ class PluriWaveAudioHandler extends BaseAudioHandler
if (emisora == null) return; if (emisora == null) return;
await playMediaItem(mediaItemParaEmisora(emisora, l10n: _textos)); await playMediaItem(mediaItemParaEmisora(emisora, l10n: _textos));
} catch (e) { } catch (e) {
developer.log( debugPrint(
'[PluriWave] Error en playFromSearch($query): $e', '[PluriWave][ServicioAudio] Error en playFromSearch($query): $e',
name: 'ServicioAudio',
level: 900,
); );
} }
} }
@@ -1793,10 +1781,8 @@ class PluriWaveAudioHandler extends BaseAudioHandler
); );
} catch (e) { } catch (e) {
// Spec "Unknown or stale media id": never propagate from the handler. // Spec "Unknown or stale media id": never propagate from the handler.
developer.log( debugPrint(
'[PluriWave] Error en playFromMediaId($mediaId): $e', '[PluriWave][ServicioAudio] Error en playFromMediaId($mediaId): $e',
name: 'ServicioAudio',
level: 900,
); );
} }
} }
+2 -5
View File
@@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer' as developer;
import 'package:audio_session/audio_session.dart'; import 'package:audio_session/audio_session.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@@ -89,10 +88,8 @@ class ServicioAudioSession {
(_) => unawaited(manejarDesconexionSalida()), (_) => unawaited(manejarDesconexionSalida()),
); );
} catch (e) { } catch (e) {
developer.log( debugPrint(
'[PluriWave] No se pudo configurar la sesion de audio: $e', '[PluriWave][ServicioAudioSession] No se pudo configurar la sesion de audio: $e',
name: 'ServicioAudioSession',
level: 900,
); );
} }
} }
@@ -157,7 +157,9 @@ class ServicioDispositivoAudioReal extends ServicioDispositivoAudio {
@override @override
Future<Map<String, String>> obtenerNombresEmparejados() async { Future<Map<String, String>> obtenerNombresEmparejados() async {
try { try {
final raw = await _methodChannel.invokeMethod<Map>('getBondedDeviceNames'); final raw = await _methodChannel.invokeMethod<Map>(
'getBondedDeviceNames',
);
if (raw == null) return const {}; if (raw == null) return const {};
return { return {
for (final entry in raw.entries) for (final entry in raw.entries)
+22 -17
View File
@@ -70,7 +70,10 @@ class ServicioEcualizador {
final porEmisora = _leerPresetsPorEmisora(prefs); final porEmisora = _leerPresetsPorEmisora(prefs);
final presetsDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo); final presetsDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo);
final presetsMatriz = _leerMapa(prefs, _keyPresetsMatriz); final presetsMatriz = _leerMapa(prefs, _keyPresetsMatriz);
final nombresDispositivos = _leerMapaStrings(prefs, _keyNombresDispositivos); final nombresDispositivos = _leerMapaStrings(
prefs,
_keyNombresDispositivos,
);
return ConfiguracionEcualizador( return ConfiguracionEcualizador(
principal: principal, principal: principal,
porEmisora: porEmisora, porEmisora: porEmisora,
@@ -124,21 +127,22 @@ class ServicioEcualizador {
String prefijo, String prefijo,
) async { ) async {
final presetsPorDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo); final presetsPorDispositivo = _leerMapa(prefs, _keyPresetsPorDispositivo);
final dispositivos = presetsPorDispositivo.keys final dispositivos =
.where((clave) => clave.startsWith(prefijo)) presetsPorDispositivo.keys
.toList(); .where((clave) => clave.startsWith(prefijo))
.toList();
final nombres = _leerMapaStrings(prefs, _keyNombresDispositivos); final nombres = _leerMapaStrings(prefs, _keyNombresDispositivos);
final nombresAPurgar = nombres.keys final nombresAPurgar =
.where((clave) => clave.startsWith(prefijo)) nombres.keys.where((clave) => clave.startsWith(prefijo)).toList();
.toList();
final matriz = _leerMapa(prefs, _keyPresetsMatriz); final matriz = _leerMapa(prefs, _keyPresetsMatriz);
final matrizAPurgar = matriz.keys.where((clave) { final matrizAPurgar =
final separador = clave.indexOf(':'); matriz.keys.where((clave) {
if (separador == -1) return false; final separador = clave.indexOf(':');
return clave.substring(separador + 1).startsWith(prefijo); if (separador == -1) return false;
}).toList(); return clave.substring(separador + 1).startsWith(prefijo);
}).toList();
for (final clave in dispositivos) { for (final clave in dispositivos) {
presetsPorDispositivo.remove(clave); presetsPorDispositivo.remove(clave);
@@ -183,11 +187,12 @@ class ServicioEcualizador {
// (station UUIDs are RFC4122 and contain no colons — multi-device-eq // (station UUIDs are RFC4122 and contain no colons — multi-device-eq
// ADR-3), since deviceId itself may contain colons (e.g. a MAC-based id). // ADR-3), since deviceId itself may contain colons (e.g. a MAC-based id).
final presetsMatriz = _leerMapa(prefs, _keyPresetsMatriz); final presetsMatriz = _leerMapa(prefs, _keyPresetsMatriz);
final clavesMatrizAPurgar = presetsMatriz.keys.where((clave) { final clavesMatrizAPurgar =
final separador = clave.indexOf(':'); presetsMatriz.keys.where((clave) {
if (separador == -1) return false; final separador = clave.indexOf(':');
return clave.substring(separador + 1) == deviceId; if (separador == -1) return false;
}).toList(); return clave.substring(separador + 1) == deviceId;
}).toList();
if (clavesMatrizAPurgar.isNotEmpty) { if (clavesMatrizAPurgar.isNotEmpty) {
for (final clave in clavesMatrizAPurgar) { for (final clave in clavesMatrizAPurgar) {
presetsMatriz.remove(clave); presetsMatriz.remove(clave);