First pass adding android auto and apple carplay support. Android auto tuned on emulator.
This commit is contained in:
@@ -9,6 +9,7 @@ dependencies {
|
||||
implementation("com.google.android.gms:play-services-cast-framework:22.1.0")
|
||||
implementation("androidx.mediarouter:mediarouter:1.7.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation("androidx.media:media:1.7.0")
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
@@ -42,12 +42,31 @@
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="com.example.kryz_go_flutter.CastOptionsProvider" />
|
||||
|
||||
<!-- Declares this app as an Android Auto media app. -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
|
||||
<service
|
||||
android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
tools:ignore="Instantiatable">
|
||||
</service>
|
||||
|
||||
<!--
|
||||
Dedicated MediaBrowserService for Android Auto. Android Auto connects here
|
||||
to browse content and send transport controls. Styled with KryzAutoTheme
|
||||
so Auto accents its media UI with the KRYZ brand orange (#E36A18).
|
||||
-->
|
||||
<service
|
||||
android:name="com.example.kryz_go_flutter.KryzAutoMediaBrowserService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:theme="@style/KryzAutoTheme"
|
||||
tools:ignore="Instantiatable">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||
</intent-filter>
|
||||
|
||||
+22
-33
@@ -3,7 +3,6 @@ package com.example.kryz_go_flutter
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioFocusRequest
|
||||
@@ -132,39 +131,29 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
|
||||
Log.d(TAG, "onCreate()")
|
||||
createNotificationChannel()
|
||||
|
||||
try {
|
||||
mediaSession = MediaSessionCompat(
|
||||
this,
|
||||
TAG,
|
||||
ComponentName(this, com.ryanheise.audioservice.MediaButtonReceiver::class.java),
|
||||
null,
|
||||
).apply {
|
||||
setCallback(mediaSessionCallback)
|
||||
@Suppress("DEPRECATION")
|
||||
setFlags(
|
||||
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
|
||||
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
|
||||
)
|
||||
setPlaybackState(
|
||||
PlaybackStateCompat.Builder()
|
||||
.setActions(
|
||||
PlaybackStateCompat.ACTION_PLAY or
|
||||
PlaybackStateCompat.ACTION_STOP or
|
||||
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
||||
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
|
||||
)
|
||||
.setState(PlaybackStateCompat.STATE_STOPPED, 0, 1f)
|
||||
.build()
|
||||
)
|
||||
setMetadata(buildLiveStreamMetadata())
|
||||
isActive = true
|
||||
}
|
||||
|
||||
sessionToken = mediaSession.sessionToken
|
||||
Log.d(TAG, "MediaSession created, sessionToken set")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to create MediaSession", e)
|
||||
mediaSession = MediaSessionCompat(this, TAG).apply {
|
||||
setCallback(mediaSessionCallback)
|
||||
@Suppress("DEPRECATION")
|
||||
setFlags(
|
||||
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
|
||||
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
|
||||
)
|
||||
setPlaybackState(
|
||||
PlaybackStateCompat.Builder()
|
||||
.setActions(
|
||||
PlaybackStateCompat.ACTION_PLAY or
|
||||
PlaybackStateCompat.ACTION_STOP or
|
||||
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
|
||||
PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH
|
||||
)
|
||||
.setState(PlaybackStateCompat.STATE_STOPPED, 0, 1f)
|
||||
.build()
|
||||
)
|
||||
setMetadata(buildLiveStreamMetadata())
|
||||
isActive = true
|
||||
}
|
||||
sessionToken = mediaSession.sessionToken
|
||||
Log.d(TAG, "MediaSession created, sessionToken set")
|
||||
}
|
||||
|
||||
override fun onGetRoot(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.kryz_go_flutter
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
@@ -26,6 +27,7 @@ class MainActivity : AudioServiceActivity(), MethodChannel.MethodCallHandler, Ev
|
||||
companion object {
|
||||
private const val METHOD_CHANNEL = "kryz_go_flutter/casting/methods"
|
||||
private const val EVENT_CHANNEL = "kryz_go_flutter/casting/events"
|
||||
private const val PLAYBACK_METHOD_CHANNEL = "kryz_go_flutter/playback/methods"
|
||||
}
|
||||
|
||||
private var castContext: CastContext? = null
|
||||
@@ -33,6 +35,9 @@ class MainActivity : AudioServiceActivity(), MethodChannel.MethodCallHandler, Ev
|
||||
private var chooserDialog: MediaRouteChooserDialog? = null
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var activeRemoteMediaClient: RemoteMediaClient? = null
|
||||
private var playbackMethodChannel: MethodChannel? = null
|
||||
private var flutterReady = false
|
||||
private var pendingAutoPlay = false
|
||||
|
||||
private var statusPayload: MutableMap<String, Any?> = mutableMapOf(
|
||||
"status" to "idle",
|
||||
@@ -101,9 +106,46 @@ class MainActivity : AudioServiceActivity(), MethodChannel.MethodCallHandler, Ev
|
||||
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
|
||||
.setStreamHandler(this)
|
||||
|
||||
playbackMethodChannel = MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
PLAYBACK_METHOD_CHANNEL,
|
||||
)
|
||||
|
||||
playbackMethodChannel?.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"flutterReady" -> {
|
||||
flutterReady = true
|
||||
// Stop the service's MediaPlayer and hand off to Flutter.
|
||||
val wasServicePlaying = KryzAutoMediaBrowserService.isServicePlaying()
|
||||
KryzAutoMediaBrowserService.stopServicePlayer()
|
||||
KryzAutoMediaBrowserService.registerDirectCallbacks(
|
||||
play = { mainHandler.post { invokeFlutterPlay() } },
|
||||
stop = { mainHandler.post { playbackMethodChannel?.invokeMethod("stop", null) } },
|
||||
)
|
||||
if (pendingAutoPlay || wasServicePlaying) {
|
||||
pendingAutoPlay = false
|
||||
invokeFlutterPlay()
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"updatePlaybackState" -> {
|
||||
val isPlaying = call.argument<Boolean>("isPlaying") ?: false
|
||||
val isBuffering = call.argument<Boolean>("isBuffering") ?: false
|
||||
KryzAutoMediaBrowserService.updatePlaybackState(isPlaying, isBuffering)
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
initializeCastContext()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"getStatus" -> result.success(statusPayload)
|
||||
@@ -139,6 +181,8 @@ class MainActivity : AudioServiceActivity(), MethodChannel.MethodCallHandler, Ev
|
||||
chooserDialog?.dismiss()
|
||||
chooserDialog = null
|
||||
|
||||
KryzAutoMediaBrowserService.unregisterDirectCallbacks()
|
||||
|
||||
castContext?.sessionManager?.removeSessionManagerListener(
|
||||
castSessionManagerListener,
|
||||
CastSession::class.java,
|
||||
@@ -148,6 +192,16 @@ class MainActivity : AudioServiceActivity(), MethodChannel.MethodCallHandler, Ev
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun invokeFlutterPlay() {
|
||||
if (!flutterReady) {
|
||||
pendingAutoPlay = true
|
||||
return
|
||||
}
|
||||
mainHandler.post {
|
||||
playbackMethodChannel?.invokeMethod("play", null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeCastContext() {
|
||||
try {
|
||||
castContext = CastContext.getSharedInstance(this)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Brand colors exposed to Android Auto. Android Auto's media UI reads colorPrimary
|
||||
from the app theme to accent its browsing and now-playing screens.
|
||||
#E36A18 matches AppThemeBusinessObject.lightColors.primarySeed (brand orange).
|
||||
-->
|
||||
<resources>
|
||||
<color name="kryz_primary">#E36A18</color>
|
||||
<color name="kryz_primary_dark">#B84E08</color>
|
||||
<color name="kryz_accent">#1663C7</color>
|
||||
</resources>
|
||||
@@ -16,4 +16,14 @@
|
||||
<item name="android:windowBackground">@android:color/white</item>
|
||||
<item name="android:windowIsTranslucent">false</item>
|
||||
</style>
|
||||
|
||||
<!--
|
||||
Theme used by KryzAutoMediaBrowserService. Android Auto reads colorPrimary
|
||||
from the service's theme to accent its media browsing UI with the KRYZ brand orange.
|
||||
-->
|
||||
<style name="KryzAutoTheme" parent="Theme.AppCompat">
|
||||
<item name="colorPrimary">@color/kryz_primary</item>
|
||||
<item name="colorPrimaryDark">@color/kryz_primary_dark</item>
|
||||
<item name="colorAccent">@color/kryz_accent</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<automotiveApp>
|
||||
<uses name="media"/>
|
||||
</automotiveApp>
|
||||
Reference in New Issue
Block a user