androidkotlinandroid-mediaplayerandroid-music-playerandroid-auto

PlaybackState "ACTION_STOP" not working in Android Auto Media App


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: App UI before playing the media

And here is the UI after started to playing the media: App UI after 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.


Solution

  • 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 ..."