刷新 MediaBrowserService 订阅内容

Refreshing MediaBrowserService subcription content

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.

我想实现以下功能:

  1. 启动 MediaBrowserServiceCompat 服务。
  2. 从 activity,连接并订阅媒体浏览器服务。
  3. 允许服务继续运行并在应用程序关闭时播放音乐。
  4. 稍后或在 SwipeRefreshLayout 事件中,重新连接并订阅该服务以获取新内容。

我收到的问题是,在 MediaBrowserService 中(创建订阅后)您只能从 onLoadChildren() 方法调用 sendResult() 一次,因此下次您尝试订阅媒体浏览器时使用相同根的服务,第二次调用 sendResult() 时会出现以下异常:

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$onSyncFinished$playerEventListener.onPlayerStateChanged(MediaService.kt:376)
                                                    at com.google.android.exoplayer2.ExoPlayerImpl.handleEvent(ExoPlayerImpl.java:422)
                                                    at com.google.android.exoplayer2.ExoPlayerImpl.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)

我调用以下方法连接到媒体浏览器和断开连接(同样,第一次连接时一切运行顺利,但在第二次连接时我不确定如何通过订阅刷新内容):

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()
        }
}

我在 onPause() 而不是 onDestroy() 中取消订阅并断开连接,这样即使 activity 保留在后台堆栈中,也会重新创建订阅。

滑动刷新的实际使用方法,分别在activity和服务中:

Activity

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

服务

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)
            }
        }
    }}

其他研究:

我参考了 Github 上的 Google 示例代码,以及...

在创建媒体浏览器服务并且 activity 至少订阅了一次后,上述存储库似乎都无法处理 刷新 内容的问题 - 我希望避免重新启动服务,以便音乐可以在后台继续播放。

可能的相关问题:

我的问题与 MediaBrowserServiceCompat class 无关。出现这个问题是因为我正在调用 result.detach() 以实现一些异步数据获取,而我使用的侦听器同时具有传入的 onLoadChildren 方法的 parentIdresult 变量并分配最终 val 而不是 var.

我仍然不完全理解为什么会发生这种情况,是否是在另一个异步网络调用侦听器中使用 Player.EventListener 的潜在结果,但解决方案是创建并分配一个变量(也许有人否则可以解释这种现象):

// 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
                            }
                        }
                        ...
                     }
                }
       }

    }

调用您的音乐服务实现 notifyChildrenChanged(String parentId) 将触发 onLoadChildren 并且在其中,您可以使用 result.sendResult() 发送不同的结果。

我所做的是在我的音乐服务中添加了一个 BroadcastReceiver,并在其中调用了 notifyChildrenChanged(String parentId)。在我的 Activity 里面,我在更改音乐列表时发送了一个广播。

可选(不推荐)快速修复

音乐服务 ->

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)
     }
}

可选(推荐)

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 )
}

音乐服务 ->

 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()
            }
        }
    }
}