Refreshing MediaBrowserService subcription content

4k Views Asked by At

TL;DR: I have successfully created and coupled (via a subscription) an activity to a media browser service. This media browser service can continue running and play music in the background. I'd like to be able to refresh the content at some stage, either when the app comes to the foreground again or during a SwipeRefreshLayout event.

I have the following functionality I'd like to implement:

  1. Start a MediaBrowserServiceCompat service.
  2. From an activity, connect to and subscribe to the media browser service.
  3. Allow the service to continue running and playing music while the app is closed.
  4. At a later stage, or on a SwipeRefreshLayout event, reconnect and subscribe to the service to get fresh content.

The issue I am receiving is that within a MediaBrowserService (after a subscription has been created) you can only call sendResult() once from the onLoadChildren() method, so the next time you try to subscribe to the media browser service using the same root, you get the following exception when sendResult() is called for the second time:

E/UncaughtException: java.lang.IllegalStateException: sendResult() called when either sendResult() or sendError() had already been called for: MEDIA_ID_ROOT
                                                    at android.support.v4.media.MediaBrowserServiceCompat$Result.sendResult(MediaBrowserServiceCompat.java:602)
                                                    at com.roostermornings.android.service.MediaService.loadChildrenImpl(MediaService.kt:422)
                                                    at com.roostermornings.android.service.MediaService.access$loadChildrenImpl(MediaService.kt:50)
                                                    at com.roostermornings.android.service.MediaService$onLoadChildren$1$onSyncFinished$playerEventListener$1.onPlayerStateChanged(MediaService.kt:376)
                                                    at com.google.android.exoplayer2.ExoPlayerImpl.handleEvent(ExoPlayerImpl.java:422)
                                                    at com.google.android.exoplayer2.ExoPlayerImpl$1.handleMessage(ExoPlayerImpl.java:103)
                                                    at android.os.Handler.dispatchMessage(Handler.java:102)
                                                    at android.os.Looper.loop(Looper.java:150)
                                                    at android.app.ActivityThread.main(ActivityThread.java:5665)
                                                    at java.lang.reflect.Method.invoke(Native Method)
                                                    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:822)
                                                    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:712)

I call the following methods to connect to and disconnect from the media browser (again, everything runs smoothly on first connection, but on the second connection I'm not sure how to refresh the content via a subscription):

override fun onStart() {
        super.onStart()

        mMediaBrowser = MediaBrowserCompat(this, ComponentName(this, MediaService::class.java), connectionCallback, null)

        if (!mMediaBrowser.isConnected)
            mMediaBrowser.connect()
}

override fun onPause() {
        super.onPause()

        //Unsubscribe and unregister MediaControllerCompat callbacks
        MediaControllerCompat.getMediaController(this@DiscoverFragmentActivity)?.unregisterCallback(mediaControllerCallback)
        if (mMediaBrowser.isConnected) {
            mMediaBrowser.unsubscribe(mMediaBrowser.root, subscriptionCallback)
            mMediaBrowser.disconnect()
        }
}

I unsubscribe and disconnect in onPause() instead of onDestroy() so that the subscription is recreated even if the activity is kept on the back-stack.

Actual method used for swipe refresh, in activity and service respectively:

Activity

if (mMediaBrowser.isConnected)
        mMediaController?.sendCommand(MediaService.Companion.CustomCommand.REFRESH.toString(), null, null)

Service

inner class MediaPlaybackPreparer : MediaSessionConnector.PlaybackPreparer {

    ...

    override fun onCommand(command: String?, extras: Bundle?, cb: ResultReceiver?) {
        when(command) {
            // Refresh media browser content and send result to subscribers
            CustomCommand.REFRESH.toString() -> {
                notifyChildrenChanged(MEDIA_ID_ROOT)
            }
        }
    }}

Other research:

I have referred to the Google Samples code on Github, as well as...

Neither of the above repos seem to handle the issue of refreshing content after the media browser service has been created and the activity has subscribed at least once - I'd like to avoid restarting the service so that the music can continue playing in the background.

Possible related issues:

3

There are 3 best solutions below

1
On BEST ANSWER

My issue was unrelated to the MediaBrowserServiceCompat class. The issue was coming about because I was calling result.detach() in order to implement some asynchronous data fetching, and the listener I was using had both the parentId and result variables from the onLoadChildren method passed in and assigned final val rather than var.

I still don't fully understand why this occurs, whether it's an underlying result of using a Player.EventListener within another asynchronous network call listener, but the solution was to create and assign a variable (and perhaps someone else can explain this phenomenon):

// Create variable
var currentResult: Result<List<MediaBrowserCompat.MediaItem>>? = null

override fun onLoadChildren(parentId: String, result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>) {
    // Use result.detach to allow calling result.sendResult from another thread
    result.detach()
    // Assign returned result to temporary variable
    currentResult = result
    currentParentId = parentId

    // Create listener for network call
    ChannelManager.onFlagChannelManagerDataListener = object : ChannelManager.Companion.OnFlagChannelManagerDataListener {
       override fun onSyncFinished() {
            // Create a listener to determine when player is prepared
            val playerEventListener = object : Player.EventListener {

                override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
                     when(playbackState) {
                        Player.STATE_READY -> {
                            if(mPlayerPreparing) {
                                // Prepare content to send to subscribed content
                                loadChildrenImpl(currentParentId, currentResult as MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>)
                                mPlayerPreparing = false
                            }
                        }
                        ...
                     }
                }
       }

    }
0
On

Optional (not Recommended) Quick fix

MusicService ->

companion object {
    var musicServiceInstance:MusicService?=null
}

override fun onCreate() {
    super.onCreate()
    musicServiceInstance=this
}

//api call
fun fetchSongs(params:Int){
    serviceScope.launch {
        firebaseMusicSource.fetchMediaData(params)

        //Edit Data or Change Data
         notifyChildrenChanged(MEDIA_ROOT_ID)
    }
}

ViewModel ->

fun fetchSongs(){
    MusicService.musicServiceInstance?.let{
      it.fetchSongs(params)
     }
}

Optional (Recommended)

MusicPlaybackPreparer

class MusicPlaybackPreparer (
private val firebaseMusicSource: FirebaseMusicSource,
private val serviceScope: CoroutineScope,
private val exoPlayer: SimpleExoPlayer,
private val playerPrepared: (MediaMetadataCompat?) -> Unit

) : MediaSessionConnector.PlaybackPreparer {

override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?
): Boolean {
    when(command){
         //edit data or fetch more data from api
        "Add Songs"->{
            serviceScope.launch {
                firebaseMusicSource.fetchMediaData()
            }
         }
       
    }
    return false
}


override fun getSupportedPrepareActions(): Long {
    return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
            PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
}

override fun onPrepare(playWhenReady: Boolean) = Unit

override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
    firebaseMusicSource.whenReady {
        val itemToPlay = firebaseMusicSource.songs.find { mediaId == it.description.mediaId }
        playerPrepared(itemToPlay)
    }
}

override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) = Unit

override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) = Unit

}

MusicServiceConnection

fun sendCommand(command: String, parameters: Bundle?) =
    sendCommand(command, parameters) { _, _ -> }

private fun sendCommand(
    command: String,
    parameters: Bundle?,
    resultCallback: ((Int, Bundle?) -> Unit)
) = if (mediaBrowser.isConnected) {
    mediaController.sendCommand(command, parameters, object : ResultReceiver(Handler()) {
        override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
            resultCallback(resultCode, resultData)
        }
    })
    true
} else {
    false
}

ViewModel

 fun fetchSongs(){
    val args = Bundle()
    args.putInt("nRecNo", 2)
    musicServiceConnection.sendCommand("Add Songs", args )
}

MusicService ->

 override fun onLoadChildren(
    parentId: String,
    result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
    when(parentId) {
        MEDIA_ROOT_ID -> {
            val resultsSent = firebaseMusicSource.whenReady { isInitialized ->
                if(isInitialized) {
                    try {
                        result.sendResult(firebaseMusicSource.asMediaItems())
                        if(!isPlayerInitialized && firebaseMusicSource.songs.isNotEmpty()) {
                            preparePlayer(firebaseMusicSource.songs, firebaseMusicSource.songs[0], true)
                            isPlayerInitialized = true
                        }
                    }
                   catch (exception: Exception){
                       // not recommend to notify here , instead notify when you 
                       // change existing list in MusicPlaybackPreparer onCommand()
                       notifyChildrenChanged(MEDIA_ROOT_ID)
                   }
                } else {
                    result.sendResult(null)
                }
            }
            if(!resultsSent) {
                result.detach()
            }
        }
    }
}
0
On

Calling you music service implementations notifyChildrenChanged(String parentId) will trigger the onLoadChildren and inside there, you can send a different result with result.sendResult().

What I did was that I added a BroadcastReceiver to my music service and inside it, I just called the notifyChildrenChanged(String parentId). And inside my Activity, I sent a broadcast when I changed the music list.