i'm trying to build a media app for android auto. I have tried to build the app according to official documentation but i've missed/misunderstood some stuffs, probably. I've created a "Playable" MediaItem and can show it with its metadata correctly. I also set the "PlaybackState" for my "MediaSession" but somehow, i can't stop/pause the media although i can play it.
Here is the UI before playing the media:
And here is the UI after started to playing the media:
I was expecting that there should be the "pause" button instead of "stop" when started to playing the media. Beside this expectation, the main problem is, this "stop" button not working. Nothing happens when i pressed it.
Here is the full code of my MusicService class:
// imports here
class MusicService : MediaBrowserServiceCompat() {
private lateinit var mediaSession: MediaSessionCompat
private lateinit var mediaController: MediaControllerCompat
private var playbackStateBuilder = PlaybackStateCompat.Builder()
override fun onCreate() {
super.onCreate()
mediaSession = MediaSessionCompat(baseContext, "MusicService").apply {
setCallback(MyMediaSessionCallback())
setPlaybackState(
playbackStateBuilder.apply {
setState(PlaybackStateCompat.STATE_NONE, 0L, 1f)
addActionsForPlayback(this)
// addCustomActionsForPlayback(this)
}.build()
)
// setFlags(
// MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
// MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
isActive = true
}
sessionToken = mediaSession.sessionToken
mediaController = MediaControllerCompat(baseContext, mediaSession.sessionToken).apply {
registerCallback(MyMediaControllerCallback())
}
}
override fun onGetRoot(
clientPackageName: String, clientUid: Int, rootHints: Bundle?
): BrowserRoot {
val extras = Bundle()
extras.putBoolean(
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true
)
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
)
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
)
return BrowserRoot("/", extras)
}
override fun onLoadChildren(
parentId: String, result: Result<MutableList<MediaItem>>
) {
val treeResult = BrowseTree(baseContext)[parentId]
if (treeResult.isNullOrEmpty()) {
result.detach()
} else {
result.sendResult(treeResult)
}
}
private fun addActionsForPlayback(playbackStateCompatBuilder: PlaybackStateCompat.Builder) {
playbackStateCompatBuilder.apply {
setActions(PlaybackStateCompat.ACTION_PLAY)
setActions(PlaybackStateCompat.ACTION_PAUSE)
// setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
setActions(PlaybackStateCompat.ACTION_STOP)
setActions(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)
setActions(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)
setActions(PlaybackStateCompat.ACTION_SEEK_TO)
}
}
private fun addCustomActionsForPlayback(playbackStateCompatBuilder: PlaybackStateCompat.Builder) {
// custom actions defined here
}
inner class MyMediaSessionCallback : MediaSessionCompat.Callback() {
private val musicSource = MusicSource(this@MusicService)
private var seekedPos: Long? = null
override fun onPrepare() {
println("Preparing")
super.onPrepare()
}
override fun onPrepareFromMediaId(mediaId: String?, extras: Bundle?) {
println("Preparing from media id")
super.onPrepareFromMediaId(mediaId, extras)
}
override fun onPlay() {
println("Playing")
mediaSession.setPlaybackState(
playbackStateBuilder.apply {
setState(PlaybackStateCompat.STATE_PLAYING, seekedPos ?: 0L, 1f)
setActions(PlaybackStateCompat.ACTION_PAUSE)
setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
setActions(PlaybackStateCompat.ACTION_STOP)
setActions(PlaybackStateCompat.ACTION_SEEK_TO)
setActions(PlaybackStateCompat.ACTION_PLAY)
}.build()
)
super.onPlay()
}
override fun onPause() {
println("Paused")
super.onPause()
}
override fun onStop() {
println("Stopped")
mediaSession.release()
super.onStop()
}
override fun onSeekTo(pos: Long) {
println("Seeked to: $pos")
seekedPos = pos
if (mediaController.playbackState.state == 3) {
mediaSession.setPlaybackState(
playbackStateBuilder.apply {
setState(PlaybackStateCompat.STATE_PLAYING, pos, 1f)
addActionsForPlayback(this)
}.build()
)
} else {
mediaSession.setPlaybackState(
playbackStateBuilder.apply {
setState(PlaybackStateCompat.STATE_PAUSED, pos, 1f)
addActionsForPlayback(this)
}.build()
)
}
super.onSeekTo(pos)
}
override fun onRewind() {
println("Rewind command")
super.onRewind()
}
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle) {
seekedPos = null
val defaultArt = BitmapFactory.decodeResource(
this@MusicService.resources, R.drawable.music_library_icon_2
)
val albumCover = musicSource.getAlbumCover(extras.getString("PATH", ""))
mediaSession.apply {
setPlaybackState(playbackStateBuilder.apply {
setState(PlaybackStateCompat.STATE_STOPPED, 0L, 1f)
setBufferedPosition(extras.getLong(MediaMetadataCompat.METADATA_KEY_DURATION))
setActions(PlaybackStateCompat.ACTION_PLAY)
setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE)
setActions(PlaybackStateCompat.ACTION_STOP)
setActions(PlaybackStateCompat.ACTION_SEEK_TO)
setActions(PlaybackStateCompat.ACTION_PAUSE)
val playbackStateExtras = Bundle().apply {
putString(
MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID,
extras.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)
)
}
setExtras(playbackStateExtras)
addActionsForPlayback(this)
}.build())
setMetadata(
MediaMetadataCompat.Builder().apply {
putString(
MediaMetadataCompat.METADATA_KEY_MEDIA_ID,
extras.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID)
)
putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE,
extras.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE)
)
putString(
MediaMetadataCompat.METADATA_KEY_ARTIST,
extras.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE)
)
putString(
MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST,
extras.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE)
)
putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
extras.getString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE)
)
putLong(
MediaMetadataCompat.METADATA_KEY_DURATION,
extras.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)
)
putString(
MediaMetadataCompat.METADATA_KEY_MEDIA_URI,
extras.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI)
)
if (albumCover != null) {
putBitmap(
MediaMetadataCompat.METADATA_KEY_ART, albumCover
)
} else {
putBitmap(
MediaMetadataCompat.METADATA_KEY_ART, defaultArt
)
}
putLong(
MediaConstants.METADATA_KEY_IS_EXPLICIT,
extras.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT)
)
}.build()
)
}
println("Playing from Media Id")
super.onPlayFromMediaId(mediaId, extras)
}
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
println("Playing from Search")
super.onPlayFromSearch(query, extras)
}
override fun onPlayFromUri(uri: Uri?, extras: Bundle?) {
println("Playing from Uri")
super.onPlayFromUri(uri, extras)
}
override fun onCommand(command: String?, extras: Bundle?, cb: ResultReceiver?) {
println("Command: $command")
super.onCommand(command, extras, cb)
}
override fun onCustomAction(action: String?, extras: Bundle?) {
println("Action: $action")
super.onCustomAction(action, extras)
}
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
println("Media button event")
return super.onMediaButtonEvent(mediaButtonEvent)
}
}
inner class MyMediaControllerCallback : MediaControllerCompat.Callback() {
override fun onMetadataChanged(metadata: MediaMetadataCompat?) {
println("Controller media id: ${metadata?.description?.mediaId}")
super.onMetadataChanged(metadata)
}
override fun onPlaybackStateChanged(state: PlaybackStateCompat?) {
println("Controller playback state : ${state?.state}")
super.onPlaybackStateChanged(state)
}
override fun onSessionReady() {
println("Controller session is ready")
super.onSessionReady()
}
}
}
I have tried to set the required actions for PlaybackState using the following codes:
setActions(PlaybackStateCompat.ACTION_PLAY)
setActions(PlaybackStateCompat.ACTION_PAUSE)
setActions(PlaybackStateCompat.ACTION_STOP)
setActions(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)
setActions(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)
setActions(PlaybackStateCompat.ACTION_SEEK_TO)
but these didn't show the "pause" button up or didn't make the "stop" button work. I can provide more code blocks that imported from other classes, if you need. Thanks in advance for your help.
It looks like the issue might be caused by the repeated calls to setActions
. Instead of calling it once per action, you should call it once with the bitmask of the supported actions.
setActions(ACTION_PLAY or ACTION_PAUSE or ACTION_STOP or ACTION_PLAY_FROM_MEDIA_ID or ACTION_PLAY_FROM_SEARCH or ACTION_SEEK_TO)
or
is the bitwise OR operator in Kotlin (similar to |
in other languages, including Java). It's a bit confusing, but in plain English, the above code translates to "This supports ACTION_PLAY and ACTION_PAUSE and ..."