Add default implementation of Callback.onSubscribe

The library already maintains the subscribed controllers internally. This
change adds `MediaLibrarySession.getSubscribedControllers(mediaId)` to
access subscribed controllers for a given media ID.

To accept a subscription, `MediaLibraryService.Callback.onSubscribe` is
required to return `RESULT_SUCCESS`. So far, this isn't the case for the
default implementation of the library.

This change implements `Callback.onSubscribe` to conditionally
provide `RESULT_SUCCESS`. The default calls `Callback.onGetItem(mediaId)` to
assess the availability of the media item. If the app retruns `RESULT_SUCCESS`
with a browsable item, the subscription is accepted. If receiving a valid item
fails, the subscription is rejected.

Issue: androidx/media#561
PiperOrigin-RevId: 568925079
This commit is contained in:
bachinger 2023-09-27 12:17:20 -07:00 committed by Copybara-Service
parent 1df2210cf4
commit 99086b4007
12 changed files with 523 additions and 284 deletions

View File

@ -78,6 +78,13 @@
all supported API levels.
* Fix bug where `MediaController.getCurrentPosition()` is not advancing
when connected to a legacy `MediaSessionCompat`.
* Add `MediaLibrarySession.getSubscribedControllers(mediaId)` for
convenience.
* Override `MediaLibrarySession.Callback.onSubscribe()` to assert the
availability of the parent Id for which the controller subscribes. If
successful, the subscription is accepted and `notifyChildrenChanged()`
is called immediately to inform the browser
([#561](https://github.com/androidx/media/issues/561)).
* UI:
* Downloads:
* OkHttp Extension:

View File

@ -165,21 +165,6 @@ class PlaybackService : MediaLibraryService() {
return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null))
}
override fun onSubscribe(
session: MediaLibrarySession,
browser: ControllerInfo,
parentId: String,
params: LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
val children =
MediaItemTree.getChildren(parentId)
?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
)
session.notifyChildrenChanged(browser, parentId, children.size, params)
return Futures.immediateFuture(LibraryResult.ofVoid())
}
override fun onGetChildren(
session: MediaLibrarySession,
browser: ControllerInfo,

View File

@ -187,16 +187,19 @@ public final class MediaBrowser extends MediaController {
public interface Listener extends MediaController.Listener {
/**
* Called when there's change in the parent's children after you've subscribed to the parent
* Called when there's a change in the parent's children after you've subscribed to the parent
* with {@link #subscribe}.
*
* <p>This method is called when the library service called {@link
* MediaLibraryService.MediaLibrarySession#notifyChildrenChanged} for the parent.
* <p>This method is called when the app calls {@link
* MediaLibraryService.MediaLibrarySession#notifyChildrenChanged} for the parent, or it is
* called by the library immediately after calling {@link MediaBrowser#subscribe(String,
* LibraryParams)}.
*
* @param browser The browser for this event.
* @param parentId The non-empty parent id that you've specified with {@link #subscribe(String,
* LibraryParams)}.
* @param itemCount The number of children.
* @param itemCount The number of children, or {@link Integer#MAX_VALUE} if the number of items
* is unknown.
* @param params The optional parameters from the library service. Can be differ from the {@code
* params} that you've specified with {@link #subscribe(String, LibraryParams)}.
*/

View File

@ -19,6 +19,8 @@ import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotEmpty;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED;
import static androidx.media3.session.LibraryResult.RESULT_SUCCESS;
import static androidx.media3.session.LibraryResult.ofVoid;
import android.app.PendingIntent;
import android.content.Context;
@ -220,16 +222,28 @@ public abstract class MediaLibraryService extends MediaSessionService {
* Called when a {@link MediaBrowser} subscribes to the given parent id by {@link
* MediaBrowser#subscribe(String, LibraryParams)}.
*
* <p>See {@link #getSubscribedControllers(String)} also.
*
* <p>By default, the library calls {@link Callback#onGetItem(MediaLibrarySession,
* ControllerInfo, String)} for the {@code parentId} that the browser requests to subscribe
* to. If onGetItem returns {@link LibraryResult#RESULT_SUCCESS} with a {@linkplain
* MediaMetadata#isBrowsable browsable item}, the subscription is accepted and {@link
* MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, LibraryParams)} is
* immediately called with an itemCount of {@link Integer#MAX_VALUE}. In all other cases, the
* subscription is rejected and a result value different to {@link
* LibraryResult#RESULT_SUCCESS} is returned from this method.
*
* <p>To implement a different behavior, an app can safely override this method without
* calling super. Return a result code {@link LibraryResult#RESULT_SUCCESS} to accept the
* subscription, or return a result different to {@link LibraryResult#RESULT_SUCCESS} to
* prevent controllers from subscribing.
*
* <p>Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser
* asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}.
*
* <p>The {@link LibraryResult#params} should be the same as the given {@link LibraryParams
* params}.
*
* <p>It's your responsibility to keep subscriptions and call {@link
* MediaLibrarySession#notifyChildrenChanged(ControllerInfo, String, int, LibraryParams)} when
* the children of the parent are changed until it's {@link #onUnsubscribe unsubscribed}.
* <p>The {@link LibraryResult#params} returned to the caller should be the same as the {@link
* LibraryParams params} passed into this method.
*
* <p>Interoperability: This will be called by {@link
* android.support.v4.media.MediaBrowserCompat#subscribe}, but won't be called by {@link
@ -247,17 +261,44 @@ public abstract class MediaLibraryService extends MediaSessionService {
ControllerInfo browser,
String parentId,
@Nullable LibraryParams params) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
return Util.transformFutureAsync(
onGetItem(session, browser, parentId),
result -> {
if (result.resultCode != RESULT_SUCCESS
|| result.value == null
|| result.value.mediaMetadata.isBrowsable == null
|| !result.value.mediaMetadata.isBrowsable) {
// Reject subscription if no browsable item for the parent media ID is returned.
return Futures.immediateFuture(
LibraryResult.ofError(
result.resultCode != RESULT_SUCCESS
? result.resultCode
: LibraryResult.RESULT_ERROR_BAD_VALUE));
}
if (browser.getControllerVersion() != ControllerInfo.LEGACY_CONTROLLER_VERSION) {
// For legacy browsers, android.service.media.MediaBrowserService already calls
// MediaLibraryServiceLegacyStub.onLoadChildren() to send the current media items
// to the browser callback. So we call back Media3 browsers only.
session.notifyChildrenChanged(
browser, parentId, /* itemCount= */ Integer.MAX_VALUE, params);
}
return Futures.immediateFuture(ofVoid());
});
}
/**
* Called when a {@link MediaBrowser} unsubscribes from the given parent id by {@link
* Called when a {@link MediaBrowser} unsubscribes from the given parent ID by {@link
* MediaBrowser#unsubscribe(String)}.
*
* <p>Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser
* asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}.
*
* <p>Apps normally don't need to implement this method, because the library maintains the
* subscribed controllers internally and an app can use {@link
* #getSubscribedControllers(String)} to get subscribed controllers for which to call {@link
* #notifyChildrenChanged}.
*
* <p>Interoperability: This will be called by {@link
* android.support.v4.media.MediaBrowserCompat#unsubscribe}, but won't be called by {@link
* android.media.browse.MediaBrowser#unsubscribe}.
@ -270,7 +311,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
*/
default ListenableFuture<LibraryResult<Void>> onUnsubscribe(
MediaLibrarySession session, ControllerInfo browser, String parentId) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
return Futures.immediateFuture(LibraryResult.ofVoid());
}
/**
@ -554,16 +595,31 @@ public abstract class MediaLibraryService extends MediaSessionService {
}
/**
* Notifies a browser that is {@link Callback#onSubscribe subscribing} to the change to a
* parent's children. If the browser isn't subscribing to the parent, nothing will happen.
* Returns the controllers that are currently subscribed to the given {@code mediaId}.
*
* <p>This only tells the number of child {@link MediaItem media items}. {@link
* Callback#onGetChildren} will be called by the browser afterwards to get the list of {@link
* MediaItem media items}.
* <p>Use the returned {@linkplain ControllerInfo controller infos} to call {@link
* #notifyChildrenChanged} in case the children of the media item with the given media ID have
* changed and the connected controller should fetch them again.
*
* <p>Note that calling {@link #notifyChildrenChanged} for a controller that didn't subscribe to
* the media ID results in a no-op.
*
* @param mediaId The ID of the media item for which to get subscribed controllers.
* @return A list with the subscribed controllers, may be empty.
*/
@UnstableApi
public ImmutableList<ControllerInfo> getSubscribedControllers(String mediaId) {
return getImpl().getSubscribedControllers(mediaId);
}
/**
* Notifies a browser that is {@link Callback#onSubscribe subscribed} to a browsable media item
* that the children of the item have changed. This method is also called immediately after
* subscribing was successful.
*
* @param browser The browser to notify.
* @param parentId The non-empty id of the parent with changes to its children.
* @param itemCount The number of children.
* @param itemCount The number of children, or {@link Integer#MAX_VALUE} if unknown.
* @param params The parameters given by {@link Callback#onSubscribe}.
*/
public void notifyChildrenChanged(

View File

@ -17,8 +17,6 @@ package androidx.media3.session;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.postOrRun;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED;
import static androidx.media3.session.LibraryResult.RESULT_SUCCESS;
@ -32,25 +30,25 @@ import android.content.Context;
import android.os.Bundle;
import android.os.RemoteException;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.util.BitmapLoader;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaLibraryService.LibraryParams;
import androidx.media3.session.MediaLibraryService.MediaLibrarySession;
import androidx.media3.session.MediaSession.ControllerCb;
import androidx.media3.session.MediaSession.ControllerInfo;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@ -61,12 +59,10 @@ import java.util.concurrent.Future;
/* package */ class MediaLibrarySessionImpl extends MediaSessionImpl {
private static final String RECENT_LIBRARY_ROOT_MEDIA_ID = "androidx.media3.session.recent.root";
private final MediaLibrarySession instance;
private final MediaLibrarySession.Callback callback;
@GuardedBy("lock")
private final ArrayMap<ControllerCb, Set<String>> subscriptions;
private final HashMultimap<String, ControllerInfo> parentIdToSubscribedControllers;
private final HashMultimap<ControllerCb, String> controllerToSubscribedParentIds;
/** Creates an instance. */
public MediaLibrarySessionImpl(
@ -93,7 +89,8 @@ import java.util.concurrent.Future;
playIfSuppressed);
this.instance = instance;
this.callback = callback;
subscriptions = new ArrayMap<>();
parentIdToSubscribedControllers = HashMultimap.create();
controllerToSubscribedParentIds = HashMultimap.create();
}
@Override
@ -116,48 +113,6 @@ import java.util.concurrent.Future;
&& legacyStub.getConnectedControllersManager().isConnected(controller);
}
public void notifyChildrenChanged(
String parentId, int itemCount, @Nullable LibraryParams params) {
dispatchRemoteControllerTaskWithoutReturn(
(callback, seq) -> {
if (isSubscribed(callback, parentId)) {
callback.onChildrenChanged(seq, parentId, itemCount, params);
}
});
}
public void notifyChildrenChanged(
ControllerInfo browser, String parentId, int itemCount, @Nullable LibraryParams params) {
if (isMediaNotificationControllerConnected() && isMediaNotificationController(browser)) {
ControllerInfo systemUiBrowser = getSystemUiControllerInfo();
if (systemUiBrowser == null) {
return;
}
browser = systemUiBrowser;
}
dispatchRemoteControllerTaskWithoutReturn(
browser,
(callback, seq) -> {
if (!isSubscribed(callback, parentId)) {
return;
}
callback.onChildrenChanged(seq, parentId, itemCount, params);
});
}
public void notifySearchResultChanged(
ControllerInfo browser, String query, int itemCount, @Nullable LibraryParams params) {
if (isMediaNotificationControllerConnected() && isMediaNotificationController(browser)) {
ControllerInfo systemUiBrowser = getSystemUiControllerInfo();
if (systemUiBrowser == null) {
return;
}
browser = systemUiBrowser;
}
dispatchRemoteControllerTaskWithoutReturn(
browser, (callback, seq) -> callback.onSearchResultChanged(seq, query, itemCount, params));
}
public ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRootOnHandler(
ControllerInfo browser, @Nullable LibraryParams params) {
if (params != null && params.isRecent && isSystemUiController(browser)) {
@ -185,7 +140,7 @@ import java.util.concurrent.Future;
maybeUpdateLegacyErrorState(result);
}
},
(Runnable r) -> postOrRun(getApplicationHandler(), r));
this::postOrRunOnApplicationHandler);
return future;
}
@ -228,7 +183,7 @@ import java.util.concurrent.Future;
verifyResultItems(result, pageSize);
}
},
(Runnable r) -> postOrRun(getApplicationHandler(), r));
this::postOrRunOnApplicationHandler);
return future;
}
@ -243,21 +198,16 @@ import java.util.concurrent.Future;
maybeUpdateLegacyErrorState(result);
}
},
(Runnable r) -> postOrRun(getApplicationHandler(), r));
this::postOrRunOnApplicationHandler);
return future;
}
public ListenableFuture<LibraryResult<Void>> onSubscribeOnHandler(
ControllerInfo browser, String parentId, @Nullable LibraryParams params) {
ControllerCb controller = checkStateNotNull(browser.getControllerCb());
synchronized (lock) {
@Nullable Set<String> subscription = subscriptions.get(controller);
if (subscription == null) {
subscription = new HashSet<>();
subscriptions.put(controller, subscription);
}
subscription.add(parentId);
}
ControllerCb controllerCb = checkNotNull(browser.getControllerCb());
controllerToSubscribedParentIds.put(controllerCb, parentId);
parentIdToSubscribedControllers.put(parentId, browser);
// Call callbacks after adding it to the subscription list because library session may want
// to call notifyChildrenChanged() in the callback.
@ -270,31 +220,65 @@ import java.util.concurrent.Future;
instance, resolveControllerInfoForCallback(browser), parentId, params),
"onSubscribe must return non-null future");
// When error happens, remove from the subscription list.
future.addListener(
() -> {
@Nullable LibraryResult<Void> result = tryGetFutureResult(future);
if (result == null || result.resultCode != RESULT_SUCCESS) {
removeSubscription(controller, parentId);
// Remove subscription in case of an error.
removeSubscription(browser, parentId);
}
},
MoreExecutors.directExecutor());
this::postOrRunOnApplicationHandler);
return future;
}
public ImmutableList<ControllerInfo> getSubscribedControllers(String mediaId) {
return ImmutableList.copyOf(parentIdToSubscribedControllers.get(mediaId));
}
private boolean isSubscribed(ControllerCb controllerCb, String parentId) {
return controllerToSubscribedParentIds.containsEntry(controllerCb, parentId);
}
public ListenableFuture<LibraryResult<Void>> onUnsubscribeOnHandler(
ControllerInfo browser, String parentId) {
ListenableFuture<LibraryResult<Void>> future =
callback.onUnsubscribe(instance, resolveControllerInfoForCallback(browser), parentId);
future.addListener(
() -> removeSubscription(checkStateNotNull(browser.getControllerCb()), parentId),
MoreExecutors.directExecutor());
() -> removeSubscription(browser, parentId), this::postOrRunOnApplicationHandler);
return future;
}
public void notifyChildrenChanged(
String parentId, int itemCount, @Nullable LibraryParams params) {
dispatchRemoteControllerTaskWithoutReturn(
(callback, seq) -> {
if (isSubscribed(callback, parentId)) {
callback.onChildrenChanged(seq, parentId, itemCount, params);
}
});
}
public void notifyChildrenChanged(
ControllerInfo browser, String parentId, int itemCount, @Nullable LibraryParams params) {
if (isMediaNotificationControllerConnected() && isMediaNotificationController(browser)) {
ControllerInfo systemUiBrowser = getSystemUiControllerInfo();
if (systemUiBrowser == null) {
return;
}
browser = systemUiBrowser;
}
dispatchRemoteControllerTaskWithoutReturn(
browser,
(callback, seq) -> {
if (!isSubscribed(callback, parentId)) {
return;
}
callback.onChildrenChanged(seq, parentId, itemCount, params);
});
}
public ListenableFuture<LibraryResult<Void>> onSearchOnHandler(
ControllerInfo browser, String query, @Nullable LibraryParams params) {
ListenableFuture<LibraryResult<Void>> future =
@ -306,7 +290,7 @@ import java.util.concurrent.Future;
maybeUpdateLegacyErrorState(result);
}
},
(Runnable r) -> postOrRun(getApplicationHandler(), r));
this::postOrRunOnApplicationHandler);
return future;
}
@ -327,10 +311,33 @@ import java.util.concurrent.Future;
verifyResultItems(result, pageSize);
}
},
(Runnable r) -> postOrRun(getApplicationHandler(), r));
this::postOrRunOnApplicationHandler);
return future;
}
public void notifySearchResultChanged(
ControllerInfo browser, String query, int itemCount, @Nullable LibraryParams params) {
if (isMediaNotificationControllerConnected() && isMediaNotificationController(browser)) {
ControllerInfo systemUiBrowser = getSystemUiControllerInfo();
if (systemUiBrowser == null) {
return;
}
browser = systemUiBrowser;
}
dispatchRemoteControllerTaskWithoutReturn(
browser, (callback, seq) -> callback.onSearchResultChanged(seq, query, itemCount, params));
}
@Override
public void onDisconnectedOnHandler(ControllerInfo controller) {
ControllerCb controllerCb = checkNotNull(controller.getControllerCb());
Set<String> subscriptions = controllerToSubscribedParentIds.get(controllerCb);
for (String parentId : ImmutableSet.copyOf(subscriptions)) {
removeSubscription(controller, parentId);
}
super.onDisconnectedOnHandler(controller);
}
@Override
@Nullable
protected MediaLibraryServiceLegacyStub getLegacyBrowserService() {
@ -358,16 +365,6 @@ import java.util.concurrent.Future;
}
}
private boolean isSubscribed(ControllerCb callback, String parentId) {
synchronized (lock) {
@Nullable Set<String> subscriptions = this.subscriptions.get(callback);
if (subscriptions == null || !subscriptions.contains(parentId)) {
return false;
}
}
return true;
}
private void maybeUpdateLegacyErrorState(LibraryResult<?> result) {
PlayerWrapper playerWrapper = getPlayerWrapper();
if (result.resultCode == RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED
@ -410,16 +407,14 @@ import java.util.concurrent.Future;
}
}
private void removeSubscription(ControllerCb controllerCb, String parentId) {
synchronized (lock) {
@Nullable Set<String> subscription = subscriptions.get(controllerCb);
if (subscription != null) {
subscription.remove(parentId);
if (subscription.isEmpty()) {
subscriptions.remove(controllerCb);
}
}
}
private void removeSubscription(ControllerInfo controllerInfo, String parentId) {
ControllerCb controllerCb = checkNotNull(controllerInfo.getControllerCb());
parentIdToSubscribedControllers.remove(parentId, controllerInfo);
controllerToSubscribedParentIds.remove(controllerCb, parentId);
}
private void postOrRunOnApplicationHandler(Runnable runnable) {
Util.postOrRun(getApplicationHandler(), runnable);
}
private ListenableFuture<LibraryResult<ImmutableList<MediaItem>>>

View File

@ -97,7 +97,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private static final SessionResult RESULT_WHEN_CLOSED = new SessionResult(RESULT_INFO_SKIPPED);
protected final Object lock = new Object();
private final Object lock = new Object();
private final Uri sessionUri;
private final PlayerInfoChangedHandler onPlayerInfoChangedHandler;

View File

@ -56,16 +56,16 @@ public class MediaBrowserConstants {
public static final List<String> SEARCH_RESULT = new ArrayList<>();
public static final int SEARCH_RESULT_COUNT = 50;
public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL =
"subscribe_id_notify_children_changed_to_all";
public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE =
"subscribe_id_notify_children_changed_to_one";
public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID =
"subscribe_id_notify_children_changed_to_all_with_non_subscribed_id";
public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID =
"subscribe_id_notify_children_changed_to_one_with_non_subscribed_id";
public static final int NOTIFY_CHILDREN_CHANGED_ITEM_COUNT = 101;
public static final Bundle NOTIFY_CHILDREN_CHANGED_EXTRAS = TestUtils.createTestBundle();
public static final String SUBSCRIBE_PARENT_ID_1 = "subscribe_parent_id_1";
public static final String SUBSCRIBE_PARENT_ID_2 = "subscribe_parent_id_2";
public static final String EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_MEDIA_ID =
"notify_children_changed_media_id";
public static final String EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_ITEM_COUNT =
"notify_children_changed_item_count";
public static final String EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_DELAY_MS =
"notify_children_changed_delay";
public static final String EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_BROADCAST =
"notify_children_changed_broadcast";
public static final String CUSTOM_ACTION = "customAction";
public static final Bundle CUSTOM_ACTION_EXTRAS = new Bundle();

View File

@ -19,22 +19,18 @@ import static androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE;
import static androidx.media3.session.LibraryResult.RESULT_SUCCESS;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMPLETION_STATUS;
import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED;
import static androidx.media3.session.MockMediaLibraryService.createNotifyChildrenChangedBundle;
import static androidx.media3.test.session.common.CommonConstants.MOCK_MEDIA3_LIBRARY_SERVICE;
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_ASSERT_PARAMS;
import static androidx.media3.test.session.common.MediaBrowserConstants.LONG_LIST_COUNT;
import static androidx.media3.test.session.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_EXTRAS;
import static androidx.media3.test.session.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_ITEM_COUNT;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS_KEY;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS_VALUE;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_PARENT_ID_1;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_PARENT_ID_2;
import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS;
import static androidx.media3.test.session.common.TestUtils.NO_RESPONSE_TIMEOUT_MS;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@ -48,6 +44,8 @@ import androidx.media3.test.session.common.TestUtils;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@ -431,7 +429,7 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest {
@Test
public void onChildrenChanged_calledWhenSubscribed() throws Exception {
// This test uses MediaLibrarySession.notifyChildrenChanged().
String expectedParentId = SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL;
String expectedParentId = SUBSCRIBE_PARENT_ID_1;
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<String> parentIdRef = new AtomicReference<>();
@ -453,7 +451,7 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest {
LibraryResult<Void> result =
threadTestRule
.getHandler()
.postAndSync(() -> browser.subscribe(expectedParentId, null))
.postAndSync(() -> browser.subscribe(expectedParentId, /* params= */ null))
.get(TIMEOUT_MS, MILLISECONDS);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
@ -461,27 +459,23 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest {
// notifyChildrenChanged() in its onSubscribe() method.
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(parentIdRef.get()).isEqualTo(expectedParentId);
assertThat(itemCountRef.get()).isEqualTo(NOTIFY_CHILDREN_CHANGED_ITEM_COUNT);
MediaTestUtils.assertLibraryParamsEquals(paramsRef.get(), NOTIFY_CHILDREN_CHANGED_EXTRAS);
assertThat(itemCountRef.get()).isEqualTo(Integer.MAX_VALUE);
}
@Test
public void onChildrenChanged_calledWhenSubscribed2() throws Exception {
// This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo).
String expectedParentId = SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE;
public void onChildrenChanged_calledWhenSubscribedAndWithDelay() throws Exception {
String expectedParentId = SUBSCRIBE_PARENT_ID_2;
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<String> parentIdRef = new AtomicReference<>();
AtomicInteger itemCountRef = new AtomicInteger();
AtomicReference<LibraryParams> paramsRef = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(2);
List<String> parentIds = new ArrayList<>();
List<Integer> itemCounts = new ArrayList<>();
MediaBrowser.Listener browserListenerProxy =
new MediaBrowser.Listener() {
@Override
public void onChildrenChanged(
MediaBrowser browser, String parentId, int itemCount, LibraryParams params) {
parentIdRef.set(parentId);
itemCountRef.set(itemCount);
paramsRef.set(params);
parentIds.add(parentId);
itemCounts.add(itemCount);
latch.countDown();
}
};
@ -490,75 +484,108 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest {
LibraryResult<Void> result =
threadTestRule
.getHandler()
.postAndSync(() -> browser.subscribe(expectedParentId, null))
.postAndSync(
() -> {
// Bundle to request to call onChildrenChanged() after a given delay.
Bundle requestNotifyChildren =
createNotifyChildrenChangedBundle(
expectedParentId,
/* itemCount= */ 12,
/* delayMs= */ 250L,
/* broadcast= */ false);
return browser.subscribe(
expectedParentId,
new LibraryParams.Builder().setExtras(requestNotifyChildren).build());
})
.get(TIMEOUT_MS, MILLISECONDS);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
// The MediaLibrarySession in MockMediaLibraryService is supposed to call
// notifyChildrenChanged(ControllerInfo) in its onSubscribe() method.
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(parentIdRef.get()).isEqualTo(expectedParentId);
assertThat(itemCountRef.get()).isEqualTo(NOTIFY_CHILDREN_CHANGED_ITEM_COUNT);
MediaTestUtils.assertLibraryParamsEquals(paramsRef.get(), NOTIFY_CHILDREN_CHANGED_EXTRAS);
assertThat(parentIds).containsExactly(expectedParentId, expectedParentId);
assertThat(itemCounts).containsExactly(Integer.MAX_VALUE, 12);
}
@Test
public void onChildrenChanged_notCalledWhenNotSubscribed() throws Exception {
// This test uses MediaLibrarySession.notifyChildrenChanged().
String subscribedMediaId = SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID;
CountDownLatch latch = new CountDownLatch(1);
MediaBrowser.Listener browserListenerProxy =
String mediaId1 = SUBSCRIBE_PARENT_ID_1;
String mediaId2 = SUBSCRIBE_PARENT_ID_2;
List<String> notifiedParentIds = new ArrayList<>();
List<Integer> notifiedItemCounts = new ArrayList<>();
CountDownLatch childrenChangedLatch = new CountDownLatch(4);
CountDownLatch disconnectLatch = new CountDownLatch(2);
MediaBrowser.Listener browserListener =
new MediaBrowser.Listener() {
@Override
public void onChildrenChanged(
MediaBrowser browser, String parentId, int itemCount, LibraryParams params) {
latch.countDown();
notifiedParentIds.add(parentId);
notifiedItemCounts.add(itemCount);
childrenChangedLatch.countDown();
}
};
MediaBrowser browser = createBrowser(null, browserListenerProxy);
LibraryResult<Void> result =
threadTestRule
.getHandler()
.postAndSync(() -> browser.subscribe(subscribedMediaId, null))
.get(TIMEOUT_MS, MILLISECONDS);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
// The MediaLibrarySession in MockMediaLibraryService is supposed to call
// notifyChildrenChanged() in its onSubscribe() method, but with a different media ID.
// Therefore, onChildrenChanged() should not be called.
assertThat(latch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse();
}
@Test
public void onChildrenChanged_notCalledWhenNotSubscribed2() throws Exception {
// This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo).
String subscribedMediaId = SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID;
CountDownLatch latch = new CountDownLatch(1);
MediaBrowser.Listener browserListenerProxy =
new MediaBrowser.Listener() {
@Override
public void onChildrenChanged(
MediaBrowser browser, String parentId, int itemCount, LibraryParams params) {
latch.countDown();
public void onDisconnected(MediaController controller) {
disconnectLatch.countDown();
}
};
MediaBrowser browser = createBrowser(null, browserListenerProxy);
LibraryResult<Void> result =
MediaBrowser browser1 = createBrowser(/* connectionHints= */ null, browserListener);
MediaBrowser browser2 = createBrowser(/* connectionHints= */ null, browserListener);
// Subscribe both browsers each to a different media IDs and request a second update after a
// delay.
LibraryResult<Void> subscriptionResult1 =
threadTestRule
.getHandler()
.postAndSync(() -> browser.subscribe(subscribedMediaId, null))
.postAndSync(
() -> {
Bundle requestNotifyChildren =
createNotifyChildrenChangedBundle(
mediaId1,
/* itemCount= */ 123,
/* delayMs= */ 200L,
/* broadcast= */ true);
return browser1.subscribe(
mediaId1,
new LibraryParams.Builder().setExtras(requestNotifyChildren).build());
})
.get(TIMEOUT_MS, MILLISECONDS);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
assertThat(subscriptionResult1.resultCode).isEqualTo(RESULT_SUCCESS);
LibraryResult<Void> result2 =
threadTestRule
.getHandler()
.postAndSync(
() -> {
Bundle requestNotifyChildren =
createNotifyChildrenChangedBundle(
mediaId2,
/* itemCount= */ 567,
/* delayMs= */ 200L,
/* broadcast= */ true);
return browser2.subscribe(
mediaId2,
new LibraryParams.Builder().setExtras(requestNotifyChildren).build());
})
.get(TIMEOUT_MS, MILLISECONDS);
assertThat(result2.resultCode).isEqualTo(RESULT_SUCCESS);
// The MediaLibrarySession in MockMediaLibraryService is supposed to call
// notifyChildrenChanged(ControllerInfo) in its onSubscribe() method, but
// with a different media ID.
// Therefore, onChildrenChanged() should not be called.
assertThat(latch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse();
assertThat(childrenChangedLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(notifiedParentIds)
.containsExactly(
mediaId1, // callback when subscribing browser1
mediaId2, // callback when subscribing browser2
mediaId1, // callback on first delayed notification
mediaId2) // callback on second delayed notification
.inOrder();
assertThat(notifiedItemCounts)
.containsExactly(Integer.MAX_VALUE, Integer.MAX_VALUE, 123, 567)
.inOrder();
threadTestRule
.getHandler()
.postAndSync(
() -> {
browser1.release();
browser2.release();
});
assertThat(disconnectLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
}
private void setExpectedLibraryParam(MediaBrowser browser, LibraryParams params)

View File

@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
@ -100,7 +101,8 @@ public class MediaLibraryServiceTest {
return session.get();
});
// Create the remote browser to start the service.
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(token);
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(token, /* connectionHints= */ Bundle.EMPTY);
// Get the started service instance after creation.
MockMediaLibraryService service =
(MockMediaLibraryService) testServiceRegistry.getServiceInstance();

View File

@ -15,11 +15,15 @@
*/
package androidx.media3.session;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_SETUP_REQUIRED;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_PARENT_ID_1;
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
@ -37,7 +41,9 @@ import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
@ -77,11 +83,13 @@ public class MediaLibrarySessionCallbackTest {
}
@Test
public void onSubscribe() throws Exception {
String testParentId = "testSubscribeId";
public void onSubscribeUnsubscribe() throws Exception {
String testParentId = "testSubscribeUnsubscribeId";
LibraryParams testParams = MediaTestUtils.createLibraryParams();
CountDownLatch latch = new CountDownLatch(1);
CountDownLatch latch = new CountDownLatch(2);
AtomicReference<LibraryParams> libraryParamsRef = new AtomicReference<>();
List<ControllerInfo> subscribedControllers = new ArrayList<>();
List<String> parentIds = new ArrayList<>();
MediaLibrarySession.Callback sessionCallback =
new MediaLibrarySession.Callback() {
@Override
@ -90,24 +98,154 @@ public class MediaLibrarySessionCallbackTest {
ControllerInfo browser,
String parentId,
@Nullable LibraryParams params) {
assertThat(parentId).isEqualTo(testParentId);
MediaTestUtils.assertLibraryParamsEquals(testParams, params);
parentIds.add(parentId);
libraryParamsRef.set(params);
subscribedControllers.addAll(session.getSubscribedControllers(parentId));
latch.countDown();
return Futures.immediateFuture(LibraryResult.ofVoid(params));
}
};
@Override
public ListenableFuture<LibraryResult<Void>> onUnsubscribe(
MediaLibrarySession session, ControllerInfo browser, String parentId) {
parentIds.add(parentId);
subscribedControllers.addAll(session.getSubscribedControllers(parentId));
latch.countDown();
return Futures.immediateFuture(LibraryResult.ofVoid());
}
};
MockMediaLibraryService service = new MockMediaLibraryService();
service.attachBaseContext(context);
MediaLibrarySession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaLibrarySession.Builder(service, player, sessionCallback)
.setId("testOnSubscribe")
.build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken());
Bundle connectionHints = new Bundle();
connectionHints.putBoolean("onSubscribeTestBrowser", true);
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), connectionHints);
browser.subscribe(testParentId, testParams);
browser.unsubscribe(testParentId);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(parentIds).containsExactly(testParentId, testParentId);
MediaTestUtils.assertLibraryParamsEquals(testParams, libraryParamsRef.get());
assertThat(subscribedControllers).hasSize(2);
assertThat(
subscribedControllers
.get(0)
.getConnectionHints()
.getBoolean("onSubscribeTestBrowser", /* defaultValue= */ false))
.isTrue();
assertThat(
subscribedControllers
.get(1)
.getConnectionHints()
.getBoolean("onSubscribeTestBrowser", /* defaultValue= */ false))
.isTrue();
// After unsubscribing the list of subscribed controllers is empty.
assertThat(session.getSubscribedControllers(testParentId)).isEmpty();
}
@Test
public void onSubscribe_returnsNonSuccessResult_subscribedControllerNotRegistered()
throws Exception {
String testParentId = "onSubscribe_returnsNoSuccessResult_subscribedControllerNotRegistered";
LibraryParams testParams = MediaTestUtils.createLibraryParams();
CountDownLatch latch = new CountDownLatch(1);
List<ControllerInfo> subscribedControllers = new ArrayList<>();
MediaLibrarySession.Callback sessionCallback =
new MediaLibrarySession.Callback() {
@Override
public ListenableFuture<LibraryResult<Void>> onSubscribe(
MediaLibrarySession session,
ControllerInfo browser,
String parentId,
@Nullable LibraryParams params) {
latch.countDown();
subscribedControllers.addAll(session.getSubscribedControllers(parentId));
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED));
}
};
MockMediaLibraryService service = new MockMediaLibraryService();
service.attachBaseContext(context);
MediaLibrarySession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaLibrarySession.Builder(service, player, sessionCallback)
.setId("testOnSubscribe")
.build());
Bundle connectionHints = new Bundle();
connectionHints.putBoolean("onSubscribeTestBrowser", true);
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), connectionHints);
browser.subscribe(testParentId, testParams);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
// Inside the callback the subscribed controller is available even when not returning
// `RESULT_SUCCESS`. It will be removed after the result has been received.
assertThat(subscribedControllers).hasSize(1);
assertThat(
subscribedControllers
.get(0)
.getConnectionHints()
.getBoolean("onSubscribeTestBrowser", /* defaultValue= */ false))
.isTrue();
// After subscribing the list of subscribed controllers is empty, because the callback returns a
// result different to `RESULT_SUCCESS`.
assertThat(session.getSubscribedControllers(testParentId)).isEmpty();
}
@Test
public void onSubscribe_onGetItemNotImplemented_errorNotSupported() throws Exception {
String testParentId = SUBSCRIBE_PARENT_ID_1;
LibraryParams testParams = MediaTestUtils.createLibraryParams();
MockMediaLibraryService service = new MockMediaLibraryService();
service.attachBaseContext(context);
MediaLibrarySession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaLibrarySession.Builder(service, player, new MediaLibrarySession.Callback() {})
.setId("testOnSubscribe")
.build());
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
int resultCode = browser.subscribe(testParentId, testParams).resultCode;
assertThat(session.getSubscribedControllers(testParentId)).isEmpty();
assertThat(resultCode).isEqualTo(RESULT_ERROR_NOT_SUPPORTED);
assertThat(session.getSubscribedControllers(testParentId)).isEmpty();
}
@Test
public void onSubscribe_onGetItemNotSucceeded_correctErrorCodeReported() throws Exception {
LibraryParams testParams = MediaTestUtils.createLibraryParams();
MockMediaLibraryService service = new MockMediaLibraryService();
service.attachBaseContext(context);
MediaLibrarySession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaLibrarySession.Builder(
service,
player,
new MediaLibrarySession.Callback() {
@Override
public ListenableFuture<LibraryResult<MediaItem>> onGetItem(
MediaLibrarySession session, ControllerInfo browser, String mediaId) {
return Futures.immediateFuture(
LibraryResult.ofError(RESULT_ERROR_SESSION_SETUP_REQUIRED));
}
})
.setId("testOnSubscribe")
.build());
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
int resultCode = browser.subscribe(SUBSCRIBE_PARENT_ID_1, testParams).resultCode;
assertThat(resultCode).isEqualTo(RESULT_ERROR_SESSION_SETUP_REQUIRED);
assertThat(session.getSubscribedControllers(SUBSCRIBE_PARENT_ID_1)).isEmpty();
}
@Test
@ -115,12 +253,15 @@ public class MediaLibrarySessionCallbackTest {
String testParentId = "testUnsubscribeId";
CountDownLatch latch = new CountDownLatch(1);
List<ControllerInfo> subscribedControllers = new ArrayList<>();
List<String> parentIds = new ArrayList<>();
MediaLibrarySession.Callback sessionCallback =
new MediaLibrarySession.Callback() {
@Override
public ListenableFuture<LibraryResult<Void>> onUnsubscribe(
MediaLibrarySession session, ControllerInfo browser, String parentId) {
assertThat(parentId).isEqualTo(testParentId);
parentIds.add(parentId);
subscribedControllers.addAll(session.getSubscribedControllers(parentId));
latch.countDown();
return Futures.immediateFuture(LibraryResult.ofVoid());
}
@ -134,9 +275,14 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, sessionCallback)
.setId("testOnUnsubscribe")
.build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken());
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(
session.getToken(), /* connectionHints= */ Bundle.EMPTY);
browser.unsubscribe(testParentId);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(parentIds).containsExactly(testParentId);
// The browser wasn't subscribed.
assertThat(subscribedControllers).isEmpty();
}
@Test
@ -167,7 +313,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_callForRecentRootNonSystemUiPackageName_notIntercepted")
.build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken());
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<MediaItem> libraryRoot =
browser.getLibraryRoot(new LibraryParams.Builder().setRecent(true).build());
@ -212,7 +359,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems")
.build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken());
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<ImmutableList<MediaItem>> recentItem =
browser.getChildren(
@ -254,7 +402,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems")
.build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken());
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<ImmutableList<MediaItem>> recentItem =
browser.getChildren(
@ -291,7 +440,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems")
.build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken());
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<ImmutableList<MediaItem>> recentItem =
browser.getChildren(
@ -329,7 +479,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems")
.build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken());
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<ImmutableList<MediaItem>> recentItem =
browser.getChildren(
@ -372,7 +523,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems")
.build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken());
RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<ImmutableList<MediaItem>> recentItem =
browser.getChildren(

View File

@ -93,10 +93,10 @@ public final class RemoteControllerTestRule extends ExternalResource {
* Creates {@link RemoteMediaBrowser} from {@link SessionToken} with default options waiting for
* connection.
*/
public RemoteMediaBrowser createRemoteBrowser(SessionToken token) throws RemoteException {
public RemoteMediaBrowser createRemoteBrowser(SessionToken token, Bundle connectionHints)
throws RemoteException {
RemoteMediaBrowser browser =
new RemoteMediaBrowser(
context, token, /* waitForConnection= */ true, /* connectionHints= */ null);
new RemoteMediaBrowser(context, token, /* waitForConnection= */ true, connectionHints);
controllers.add(browser);
return browser;
}

View File

@ -16,28 +16,31 @@
package androidx.media3.session;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_BAD_VALUE;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMPLETION_STATUS;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT;
import static androidx.media3.session.MediaConstants.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED;
import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY;
import static androidx.media3.session.MediaTestUtils.assertLibraryParamsEquals;
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION;
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_ASSERT_PARAMS;
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_EXTRAS;
import static androidx.media3.test.session.common.MediaBrowserConstants.EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_BROADCAST;
import static androidx.media3.test.session.common.MediaBrowserConstants.EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_DELAY_MS;
import static androidx.media3.test.session.common.MediaBrowserConstants.EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_ITEM_COUNT;
import static androidx.media3.test.session.common.MediaBrowserConstants.EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_MEDIA_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.GET_CHILDREN_RESULT;
import static androidx.media3.test.session.common.MediaBrowserConstants.LONG_LIST_COUNT;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_BROWSABLE_ITEM;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_METADATA;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM;
import static androidx.media3.test.session.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_EXTRAS;
import static androidx.media3.test.session.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_ITEM_COUNT;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_NO_CHILDREN;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY;
@ -48,10 +51,8 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.SEARCH_Q
import static androidx.media3.test.session.common.MediaBrowserConstants.SEARCH_RESULT;
import static androidx.media3.test.session.common.MediaBrowserConstants.SEARCH_RESULT_COUNT;
import static androidx.media3.test.session.common.MediaBrowserConstants.SEARCH_TIME_IN_MS;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_PARENT_ID_1;
import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_PARENT_ID_2;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.app.PendingIntent;
@ -70,6 +71,7 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.test.session.common.CommonConstants;
import androidx.media3.test.session.common.TestHandler;
import androidx.media3.test.session.common.TestUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures;
@ -77,6 +79,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
@ -108,8 +111,6 @@ public class MockMediaLibraryService extends MediaLibraryService {
.build();
public static final LibraryParams ROOT_PARAMS =
new LibraryParams.Builder().setExtras(ROOT_EXTRAS).build();
private static final LibraryParams NOTIFY_CHILDREN_CHANGED_PARAMS =
new LibraryParams.Builder().setExtras(NOTIFY_CHILDREN_CHANGED_EXTRAS).build();
private static final String TAG = "MockMediaLibrarySvc2";
@ -121,11 +122,11 @@ public class MockMediaLibraryService extends MediaLibraryService {
private static LibraryParams expectedParams;
@Nullable private static byte[] testArtworkData;
private final AtomicInteger boundControllerCount;
private final ConditionVariable allControllersUnbound;
@Nullable MediaLibrarySession session;
@Nullable TestHandler handler;
@Nullable HandlerThread handlerThread;
public MockMediaLibraryService() {
@ -159,6 +160,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
super.onCreate();
handlerThread = new HandlerThread(TAG);
handlerThread.start();
handler = new TestHandler(handlerThread.getLooper());
}
@Override
@ -231,6 +233,16 @@ public class MockMediaLibraryService extends MediaLibraryService {
}
}
public static Bundle createNotifyChildrenChangedBundle(
String mediaId, int itemCount, long delayMs, boolean broadcast) {
Bundle bundle = new Bundle();
bundle.putString(EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_MEDIA_ID, mediaId);
bundle.putInt(EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_ITEM_COUNT, itemCount);
bundle.putLong(EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_DELAY_MS, delayMs);
bundle.putBoolean(EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_BROADCAST, broadcast);
return bundle;
}
private class TestLibrarySessionCallback implements MediaLibrarySession.Callback {
@Override
@ -293,8 +305,13 @@ public class MockMediaLibraryService extends MediaLibraryService {
@Override
public ListenableFuture<LibraryResult<MediaItem>> onGetItem(
MediaLibrarySession session, ControllerInfo browser, String mediaId) {
if (mediaId.startsWith(SUBSCRIBE_PARENT_ID_1)) {
return Futures.immediateFuture(
LibraryResult.ofItem(createBrowsableMediaItem(mediaId), /* params= */ null));
}
switch (mediaId) {
case MEDIA_ID_GET_BROWSABLE_ITEM:
case SUBSCRIBE_PARENT_ID_2:
return Futures.immediateFuture(
LibraryResult.ofItem(createBrowsableMediaItem(mediaId), /* params= */ null));
case MEDIA_ID_GET_PLAYABLE_ITEM:
@ -306,7 +323,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
LibraryResult.ofItem(createMediaItemWithMetadata(mediaId), /* params= */ null));
default: // fall out
}
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE));
}
@Override
@ -318,19 +335,21 @@ public class MockMediaLibraryService extends MediaLibraryService {
int pageSize,
@Nullable LibraryParams params) {
assertLibraryParams(params);
if (PARENT_ID.equals(parentId)) {
if (Objects.equals(parentId, PARENT_ID_NO_CHILDREN)) {
return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), params));
} else if (Objects.equals(parentId, PARENT_ID)) {
return Futures.immediateFuture(
LibraryResult.ofItemList(
getPaginatedResult(GET_CHILDREN_RESULT, page, pageSize), params));
} else if (PARENT_ID_LONG_LIST.equals(parentId)) {
} else if (Objects.equals(parentId, PARENT_ID_LONG_LIST)) {
List<MediaItem> list = new ArrayList<>(LONG_LIST_COUNT);
for (int i = 0; i < LONG_LIST_COUNT; i++) {
list.add(createPlayableMediaItem(TestUtils.getMediaIdInFakeTimeline(i)));
}
return Futures.immediateFuture(LibraryResult.ofItemList(list, params));
} else if (PARENT_ID_ERROR.equals(parentId)) {
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
} else if (PARENT_ID_AUTH_EXPIRED_ERROR.equals(parentId)) {
} else if (Objects.equals(parentId, PARENT_ID_ERROR)) {
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE));
} else if (Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR)) {
Bundle bundle = new Bundle();
Intent signInIntent = new Intent("action");
int flags = Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0;
@ -346,8 +365,37 @@ public class MockMediaLibraryService extends MediaLibraryService {
LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED,
new LibraryParams.Builder().setExtras(bundle).build()));
}
// Includes the case of PARENT_ID_NO_CHILDREN.
return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), params));
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE, params));
}
@Override
public ListenableFuture<LibraryResult<Void>> onSubscribe(
MediaLibrarySession session,
ControllerInfo browser,
String parentId,
@Nullable LibraryParams params) {
if (params != null) {
String mediaId = params.extras.getString(EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_MEDIA_ID, null);
long delayMs = params.extras.getLong(EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_DELAY_MS, 0L);
if (mediaId != null && delayMs > 0) {
int itemCount =
params.extras.getInt(
EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_ITEM_COUNT, Integer.MAX_VALUE);
boolean broadcast =
params.extras.getBoolean(EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_BROADCAST, false);
// Post a delayed update as requested.
handler.postDelayed(
() -> {
if (broadcast) {
session.notifyChildrenChanged(mediaId, itemCount, params);
} else {
session.notifyChildrenChanged(browser, mediaId, itemCount, params);
}
},
delayMs);
}
}
return MediaLibrarySession.Callback.super.onSubscribe(session, browser, parentId, params);
}
@Override
@ -406,46 +454,10 @@ public class MockMediaLibraryService extends MediaLibraryService {
return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), params));
} else {
// SEARCH_QUERY_ERROR will be handled here.
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE));
}
}
@Override
public ListenableFuture<LibraryResult<Void>> onSubscribe(
MediaLibrarySession session,
ControllerInfo browser,
String parentId,
LibraryParams params) {
assertLibraryParams(params);
String unsubscribedId = "unsubscribedId";
switch (parentId) {
case SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL:
MockMediaLibraryService.this.session.notifyChildrenChanged(
parentId, NOTIFY_CHILDREN_CHANGED_ITEM_COUNT, NOTIFY_CHILDREN_CHANGED_PARAMS);
return Futures.immediateFuture(LibraryResult.ofVoid(params));
case SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE:
MockMediaLibraryService.this.session.notifyChildrenChanged(
browser,
parentId,
NOTIFY_CHILDREN_CHANGED_ITEM_COUNT,
NOTIFY_CHILDREN_CHANGED_PARAMS);
return Futures.immediateFuture(LibraryResult.ofVoid(params));
case SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID:
MockMediaLibraryService.this.session.notifyChildrenChanged(
unsubscribedId, NOTIFY_CHILDREN_CHANGED_ITEM_COUNT, NOTIFY_CHILDREN_CHANGED_PARAMS);
return Futures.immediateFuture(LibraryResult.ofVoid(params));
case SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID:
MockMediaLibraryService.this.session.notifyChildrenChanged(
browser,
unsubscribedId,
NOTIFY_CHILDREN_CHANGED_ITEM_COUNT,
NOTIFY_CHILDREN_CHANGED_PARAMS);
return Futures.immediateFuture(LibraryResult.ofVoid(params));
default: // fall out
}
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
@Override
public ListenableFuture<SessionResult> onCustomCommand(
MediaSession session,
@ -471,7 +483,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
private void assertLibraryParams(@Nullable LibraryParams params) {
synchronized (MockMediaLibraryService.class) {
if (assertLibraryParams) {
assertLibraryParamsEquals(expectedParams, params);
MediaTestUtils.assertLibraryParamsEquals(expectedParams, params);
}
}
}