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.
This commit is contained in:
+51
-18
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'app.dart';
|
||||
import 'servicios/arranque_audio.dart';
|
||||
import 'servicios/musica_local_auto.dart';
|
||||
import 'servicios/navegacion_auto.dart';
|
||||
import 'servicios/servicio_audio.dart';
|
||||
@@ -37,16 +38,13 @@ Future<void> main() async {
|
||||
// injected into every state/service below.
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final handler = await AudioService.init(
|
||||
builder: () => PluriWaveAudioHandler(),
|
||||
config: configuracionAudioService,
|
||||
);
|
||||
registrarHandler(handler);
|
||||
|
||||
// Android Auto browse source (Design "getChildren data source, cold-start
|
||||
// safe") — registered before EstadoRadio builds so a headless Auto bind
|
||||
// (main() runs but the lazily-created Provider tree may never build) can
|
||||
// still serve favourites/custom stations from local reads.
|
||||
// safe") — registered BEFORE the AudioService.init await below (Design
|
||||
// "Reorder handler-independent startup work before the init await"):
|
||||
// neither this nor the local-music registration depends on the
|
||||
// AudioHandler, so browse sources exist for the car even while the
|
||||
// MediaBrowser handshake (no native timeout, see arranque_audio.dart) is
|
||||
// still pending.
|
||||
final fuenteAuto = FuenteEmisorasAutoLocal();
|
||||
registrarFuenteNavegacion(fuenteAuto);
|
||||
|
||||
@@ -57,16 +55,51 @@ Future<void> main() async {
|
||||
// permanently hidden (`hayCarpetaConfigurada()` has no fuente to ask).
|
||||
registrarFuenteMusicaLocal(FuenteMusicaLocalAutoImpl(prefs: prefs));
|
||||
|
||||
// S3-R1: audio focus — phone calls / transient losses pause or duck the
|
||||
// radio; headphones unplugged pauses it.
|
||||
final sesionAudio = ServicioAudioSession(objetivo: handler);
|
||||
unawaited(sesionAudio.configurar());
|
||||
|
||||
runApp(
|
||||
_OrientacionResponsiveApp(
|
||||
child: PluriWaveApp(prefs: prefs, fuenteAuto: fuenteAuto),
|
||||
),
|
||||
// Design "Timeout without re-init": AudioService.init is started exactly
|
||||
// ONCE here and `handlerFuturo` is the only future ever awaited for it —
|
||||
// the plugin caches state internally, so a double-configure call is
|
||||
// unsafe and must never happen, even on the degraded/timeout path below.
|
||||
final handlerFuturo = AudioService.init(
|
||||
builder: () => PluriWaveAudioHandler(),
|
||||
config: configuracionAudioService,
|
||||
);
|
||||
|
||||
// S3-R1: audio focus — phone calls / transient losses pause or duck the
|
||||
// radio; headphones unplugged pauses it. Shared by both the on-time and
|
||||
// degraded/late-completion paths below.
|
||||
void conectarHandler(PluriWaveAudioHandler handler) {
|
||||
registrarHandler(handler);
|
||||
final sesionAudio = ServicioAudioSession(objetivo: handler);
|
||||
unawaited(sesionAudio.configurar());
|
||||
}
|
||||
|
||||
Widget construirApp() => _OrientacionResponsiveApp(
|
||||
child: PluriWaveApp(prefs: prefs, fuenteAuto: fuenteAuto),
|
||||
);
|
||||
|
||||
final resultado = await esperarArranqueAudio(handlerFuturo);
|
||||
switch (resultado) {
|
||||
case ArranqueAudioListo<PluriWaveAudioHandler>(:final handler):
|
||||
// Handshake finished in time — exactly today's startup path.
|
||||
conectarHandler(handler);
|
||||
runApp(construirApp());
|
||||
case ArranqueAudioPendiente<PluriWaveAudioHandler>(
|
||||
handlerFuturo: final futuroPendiente,
|
||||
):
|
||||
// Handshake still hung after the timeout: run the app anyway with a
|
||||
// minimal loading bootstrap that keeps awaiting the SAME
|
||||
// `futuroPendiente` — identical to the outer `handlerFuturo`, per
|
||||
// esperarArranqueAudio's contract (never a second AudioService.init
|
||||
// call) — and wires the handler + swaps to the real app once it
|
||||
// eventually resolves.
|
||||
runApp(
|
||||
ArranqueAudioApp<PluriWaveAudioHandler>(
|
||||
handlerFuturo: futuroPendiente,
|
||||
alListo: conectarHandler,
|
||||
construirApp: (_) => construirApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _aplicarPoliticaOrientacion([ui.Display? display]) async {
|
||||
|
||||
Reference in New Issue
Block a user