diff --git a/android/app/src/main/kotlin/com/example/kryz_go_flutter/KryzAutoMediaBrowserService.kt b/android/app/src/main/kotlin/com/example/kryz_go_flutter/KryzAutoMediaBrowserService.kt index a21c92d..97f434b 100644 --- a/android/app/src/main/kotlin/com/example/kryz_go_flutter/KryzAutoMediaBrowserService.kt +++ b/android/app/src/main/kotlin/com/example/kryz_go_flutter/KryzAutoMediaBrowserService.kt @@ -5,6 +5,8 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.content.ComponentName import android.content.Context +import android.os.Handler +import android.os.Looper import android.support.v4.media.MediaBrowserCompat import android.net.Uri import android.os.Build @@ -43,8 +45,10 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() { private lateinit var mediaSession: MediaSessionCompat private var audioServiceBrowser: MediaBrowserCompat? = null private var audioServiceController: MediaControllerCompat? = null + private val mainHandler = Handler(Looper.getMainLooper()) private var pendingPlayRequest: Boolean = false private var pendingStopRequest: Boolean = false + private var reconnectScheduled: Boolean = false private val mediaControllerCallback = object : MediaControllerCompat.Callback() { override fun onPlaybackStateChanged(state: PlaybackStateCompat?) { @@ -75,12 +79,14 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() { override fun onConnectionSuspended() { Log.w(TAG, "audio_service browser connection suspended") - disconnectFromFlutterAudioService() + handleControllerDisconnected() + scheduleBrowserReconnect() } override fun onConnectionFailed() { Log.e(TAG, "audio_service browser connection failed") - disconnectFromFlutterAudioService() + handleControllerDisconnected() + scheduleBrowserReconnect() } } @@ -331,11 +337,24 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() { } private fun connectToFlutterAudioService() { + if (audioServiceController != null) { + return + } + val existing = audioServiceBrowser if (existing?.isConnected == true) { return } + if (existing != null) { + try { + existing.connect() + } catch (error: Exception) { + Log.w(TAG, "Failed reconnect attempt: ${error.message}") + } + return + } + val browser = MediaBrowserCompat( this, ComponentName(this, AudioService::class.java), @@ -348,8 +367,10 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() { } private fun disconnectFromFlutterAudioService() { - audioServiceController?.unregisterCallback(mediaControllerCallback) - audioServiceController = null + reconnectScheduled = false + mainHandler.removeCallbacksAndMessages(null) + + handleControllerDisconnected() audioServiceBrowser?.let { browser -> if (browser.isConnected) { @@ -359,6 +380,26 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() { audioServiceBrowser = null } + private fun handleControllerDisconnected() { + audioServiceController?.unregisterCallback(mediaControllerCallback) + audioServiceController = null + } + + private fun scheduleBrowserReconnect() { + if (reconnectScheduled) { + return + } + + reconnectScheduled = true + mainHandler.postDelayed( + { + reconnectScheduled = false + connectToFlutterAudioService() + }, + 1200, + ) + } + private fun buildLiveStreamMetadata(): MediaMetadataCompat { val iconUri = Uri.parse("android.resource://${packageName}/mipmap/ic_launcher") return MediaMetadataCompat.Builder() diff --git a/lib/main.dart b/lib/main.dart index 69e66e6..ad20574 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,13 +13,16 @@ import 'streaming_audio_business_object.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); tz.initializeTimeZones(); - unawaited(StreamingAudioBusinessObject.initializeBackgroundAudio()); - await SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - DeviceOrientation.portraitDown, - ]); - runApp(const MyApp()); + + // Keep startup non-blocking so UI can render even if background media + // services are initializing after an Android Auto cold start. + unawaited( + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]), + ); } class MyApp extends StatefulWidget { @@ -36,6 +39,7 @@ class _MyAppState extends State { @override void initState() { super.initState(); + unawaited(_audio.ensureInitialized()); _casting.initialize(); } diff --git a/lib/streaming_audio_business_object.dart b/lib/streaming_audio_business_object.dart index ab3f1f7..d364047 100644 --- a/lib/streaming_audio_business_object.dart +++ b/lib/streaming_audio_business_object.dart @@ -10,10 +10,6 @@ enum StreamingPlaybackStatus { stopped, buffering, playing } class StreamingAudioBusinessObject extends ChangeNotifier { StreamingAudioBusinessObject() { - if (_audioHandler == null && _audioHandlerInitFuture == null) { - initializeBackgroundAudio(); - } - final initFuture = _audioHandlerInitFuture; if (initFuture != null) { initFuture.then((handler) { @@ -74,6 +70,10 @@ class StreamingAudioBusinessObject extends ChangeNotifier { String get currentTitle => _currentTitle; String get currentSubtitle => _currentSubtitle; + Future ensureInitialized() async { + await initializeBackgroundAudio(); + } + StreamingPlaybackStatus _mapPlaybackStateToStatus(PlaybackState state) { if (state.processingState == AudioProcessingState.completed) { return StreamingPlaybackStatus.stopped;