media: MediaLibraryService.MediaLibrarySession::notifyChildrenChanged doesn't update Android Auto Browse

Version

Media3 1.1.0

More version details

I have an issue when a MediaItem is added locally on the device, I call MediaLibraryService.MediaLibrarySession::notifyChildrenChanged with the updated library, however the browser in Android Auto isn’t updated (the Queue is however updated).

Wondering if there are issues in this area, or if this is not the correct API to call?

Devices that reproduce the issue

Pixel 7 with Sony Android Auto head unit Emulated Pixel 6 and Android Auto head unit emulator I believe all other devices.

Devices that do not reproduce the issue

None

Reproducible in the demo app?

Not tested

Reproduction steps

Connect phone to Android Auto head unit Add an item to the phone’s media database Call the MediaLibraryService.MediaLibrarySession::notifyChildrenChanged API open the Browser on Android Auto Notice new stream is not displayed.

Expected result

New stream should be displayed.

Actual result

New stream is not displayed.

Media

N/A

Bug Report

  • You will email the zip file produced by adb bugreport to dev.exoplayer@gmail.com after filing this issue.

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 1
  • Comments: 28 (8 by maintainers)

Commits related to this issue

Most upvoted comments

The commit above provides a default implementation of Callback.onSubscribe(parentId). It calls Callback.onGetItem(parentId) and expects the app to return a browsable item with RESULT_SUCCESS. If this is the case the browser is automatically subscribed and notifyChildrenChanged(browser, ...) is called once immediately.

An app can later call MediaLibrarySession.getSubscribedControllers(parentId) to get all controllers subscribed to the parentId and call notifyChildrenChanged(controller, parentId, ...) to inform about further changes.

This behavior can be changed by overriding MediaLibrarySession.Callback.onSubscribe().

This will be included in 1.2.0 for which we will ship the next alpha soon. I’m closing this issue. Please open a new issue if required.

I now grasp the reasoning behind the decision to not have onSubscribe automatically return RESULT_SUCCESS. Lemme offer my perspective on this matter, based on my observations of media apps (note that this is subjective) :

I believe that most media apps don’t require verification of callers. This is primarily because media content, like music, is generally not sensitive (with an emphasis on “most” here). Additionally, it’s rare, if not non-existent, to encounter malicious apps that would attempt to exploit a media app’s browser service solely to access its data. Many music apps either play local files or enable remote music playback, such as the likes of Spotify.

In my experience, there’s often no need to validate the identity of callers. The need for caller identity verification is quite niche and, therefore, negligible. I don’t see any compelling reasons, other than those you’ve mentioned, for onSubscribe not to return RESULT_SUCCESS by default. Implementing a default limit of 20 subscribers seems practical, and in reality, this limit would likely never be reached. To mitigate any potential confusion, it would be beneficial to include this information in the Javadocs; this way any confusion can be addressed, and users will have clear guidance on the limit.

Let alone the fact that this may potentially fix the (probably) broken Android Auto support, where Android Auto is not refreshing the media library (#644) since it’s requiring all browsers to explicitly subscribe in order to receive callbacks. This however will require some way to add the android auto browser passively to the subscriber list.

The only thing that comes to mind is whether your service overrides subscribe/unsubscribe and returns a result with RESULT_SUCCESS? Because if not, then the subscription is not registered.

I had a problem where notifyChildrenChanged did nothing even after subscription, but @marcbaechinger’s comment pointed me in the right direction. I should have overridden onSubscribe and made it return Futures.immediateFuture(LibraryResult.ofVoid()). This way, the subscription seemingly got registered as it should be and everything worked great. Thank you so much for your assistance.

By the way, I think the Javadocs should put a lot more emphasis on this, because it’s pretty easy to miss, or maybe make the onSubscribe return a RESULT_SUCCESS by default ?

@marcbaechinger Assuming this is a common requirement for Android Auto implementations, would it make sense to maintain this Map<String, List<ControllerInfo>> (keyed by parentId) inside the library instead? And then add a convenience getter along the lines of getSubscribersForParentId(String)?

@ChernyshovYuriy

Hey Yuriy! Nice to meet you again. It’s been a while! 😃

mSession.notifyChildrenChanged

That’s the correct API to use.

I tested your case with the session demo app and this seems to work I’m afraid. So not sure how helpful the following is for you:

I changed PlaybackService.onGetChildren of the session demo app as follows:

var categoryChanged = false

override fun onGetChildren(
  session: MediaLibrarySession,
  browser: ControllerInfo,
  parentId: String,
  page: Int,
  pageSize: Int,
  params: LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
  Log.d("subscribe", "onGetChildren called for $parentId")
  val children =
    MediaItemTree.getChildren(parentId)
      ?: return Futures.immediateFuture(
        LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
      )

  if (!categoryChanged && parentId == "[artist]The Kyoto Connection") {
    categoryChanged = true
    Log.d("subscribe", "post delayed")
    Handler(Looper.myLooper()!!).postDelayed({
      Log.d("subscribe", "trigger notifyChildrenChanged for $parentId")
      session.notifyChildrenChanged(browser, parentId, children.size, params)
    }, 10_000)
  }

  return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
}

Unfortunately, your sample app doesnt have automotive variant to test on.

This worked for me by just deploying the session demo app from Android Studio to the Automotive emulator. Then select another app (like News), and then select the demo app again to make it load the library root.

This change adds an if statement that is executed once when the children of the parentId with value [artist]The Kyoto Connection are requested. I then post a delayed Runnable that after 10 seconds calls session.notifyChildrenChanged(browser, parentId, children.size, params).

I added a bit more logs in other classes, but essentially I see at the end that 10 seconds after the category is requested (19:31:40.609), the call to notifyChildrenChanged makes Automotive request the category again:

// starting the session demo app in Automtoive emulator
19:31:15.516 16112-16112 subscribe           D  onGetChildren called for [rootID]
19:31:15.534 16112-16112 subscribe           D  MediaLibrarySessionImpl.notifyChildrenChanged: [rootID] is subscribed. Calling onChildrenChanged
19:31:15.534 16112-16112 subscribe           D  MediaLibraryServiceLegacyStub.onChildrenChanged: calling MediaBrowserServiceCompat.notifyChildrenChanged for [rootID]
19:31:15.537 16112-16112 subscribe           D  onGetChildren called for [artistID]
19:31:15.555 16112-16112 subscribe           D  MediaLibrarySessionImpl.notifyChildrenChanged: [artistID] is subscribed. Calling onChildrenChanged
19:31:15.555 16112-16112 subscribe           D  MediaLibraryServiceLegacyStub.onChildrenChanged: calling MediaBrowserServiceCompat.notifyChildrenChanged for [artistID]
19:31:15.556 16112-16112 subscribe           D  onGetChildren called for [rootID]
19:31:15.566 16112-16112 subscribe           D  onGetChildren called for [artistID]

// manually select the category `[artist]The Kyoto Connection` that then post the `Runnable`
19:31:30.599 16112-16112 subscribe           D  onGetChildren called for [artist]The Kyoto Connection
19:31:30.599 16112-16112 subscribe           D  post delayed
19:31:30.624 16112-16112 subscribe           D  MediaLibrarySessionImpl.notifyChildrenChanged: [artist]The Kyoto Connection is subscribed. Calling onChildrenChanged
19:31:30.624 16112-16112 subscribe           D  MediaLibraryServiceLegacyStub.onChildrenChanged: calling MediaBrowserServiceCompat.notifyChildrenChanged for [artist]The Kyoto Connection
19:31:30.627 16112-16112 subscribe           D  onGetChildren called for [artist]The Kyoto Connection

// ten seconds later `notifyChildrenChanged` is called and Automotive comes again to get the
// children of the category again
19:31:40.609 16112-16112 subscribe           D  trigger notifyChildrenChanged for [artist]The Kyoto Connection
19:31:40.610 16112-16112 subscribe           D  MediaLibrarySessionImpl.notifyChildrenChanged: [artist]The Kyoto Connection is subscribed. Calling onChildrenChanged
19:31:40.610 16112-16112 subscribe           D  MediaLibraryServiceLegacyStub.onChildrenChanged: calling MediaBrowserServiceCompat.notifyChildrenChanged for [artist]The Kyoto Connection
19:31:40.610 16112-16112 subscribe           D  onGetChildren called for [artist]The Kyoto Connection

I’m testing with Automotive emulator API 29 that I randomly created with the device manager.

isSubscribed returns false, so execution ending there

I noticed that Automotive is only subscribed to a single parentId at the time, which is the categroy that is currently displayed. When the user navigates away from that parentId, the subscribtion is removed. What I’m trying to say is that the expectation has to be that Automotive is only subscribed to the categroy that is currently duisplayed by the UI. I’m not sure from your report whether this is the case when you test, so wanted to clarify.

When testing with https://github.com/googlesamples/android-media-controller I see similar behaviour.

Specifically, with both, Automotive and the controller test app I see that MediaLirbarySessionImpl.subscriptions has the subscription for the given parentId stored as expected.

So this looks fine to me from the library side. The only thing that comes to mind is whether your service overrides subscribe/unsubscribe and returns a result with RESULT_SUCCESS? Because if not, then the subscription is not registered.