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. all supported API levels.
* Fix bug where `MediaController.getCurrentPosition()` is not advancing * Fix bug where `MediaController.getCurrentPosition()` is not advancing
when connected to a legacy `MediaSessionCompat`. 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: * UI:
* Downloads: * Downloads:
* OkHttp Extension: * OkHttp Extension:

View File

@ -165,21 +165,6 @@ class PlaybackService : MediaLibraryService() {
return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null)) 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( override fun onGetChildren(
session: MediaLibrarySession, session: MediaLibrarySession,
browser: ControllerInfo, browser: ControllerInfo,

View File

@ -187,16 +187,19 @@ public final class MediaBrowser extends MediaController {
public interface Listener extends MediaController.Listener { 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}. * with {@link #subscribe}.
* *
* <p>This method is called when the library service called {@link * <p>This method is called when the app calls {@link
* MediaLibraryService.MediaLibrarySession#notifyChildrenChanged} for the parent. * 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 browser The browser for this event.
* @param parentId The non-empty parent id that you've specified with {@link #subscribe(String, * @param parentId The non-empty parent id that you've specified with {@link #subscribe(String,
* LibraryParams)}. * 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 * @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)}. * 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.checkNotEmpty;
import static androidx.media3.common.util.Assertions.checkNotNull; 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_ERROR_NOT_SUPPORTED;
import static androidx.media3.session.LibraryResult.RESULT_SUCCESS;
import static androidx.media3.session.LibraryResult.ofVoid;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; 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 * Called when a {@link MediaBrowser} subscribes to the given parent id by {@link
* MediaBrowser#subscribe(String, LibraryParams)}. * 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 * <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 * asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}. * {@link Futures#immediateFuture(Object)}.
* *
* <p>The {@link LibraryResult#params} should be the same as the given {@link LibraryParams * <p>The {@link LibraryResult#params} returned to the caller should be the same as the {@link
* params}. * LibraryParams params} passed into this method.
*
* <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>Interoperability: This will be called by {@link * <p>Interoperability: This will be called by {@link
* android.support.v4.media.MediaBrowserCompat#subscribe}, but won't 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, ControllerInfo browser,
String parentId, String parentId,
@Nullable LibraryParams params) { @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)}. * MediaBrowser#unsubscribe(String)}.
* *
* <p>Return a {@link ListenableFuture} to send a {@link LibraryResult} back to the browser * <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 * asynchronously. You can also return a {@link LibraryResult} directly by using Guava's
* {@link Futures#immediateFuture(Object)}. * {@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 * <p>Interoperability: This will be called by {@link
* android.support.v4.media.MediaBrowserCompat#unsubscribe}, but won't be called by {@link * android.support.v4.media.MediaBrowserCompat#unsubscribe}, but won't be called by {@link
* android.media.browse.MediaBrowser#unsubscribe}. * android.media.browse.MediaBrowser#unsubscribe}.
@ -270,7 +311,7 @@ public abstract class MediaLibraryService extends MediaSessionService {
*/ */
default ListenableFuture<LibraryResult<Void>> onUnsubscribe( default ListenableFuture<LibraryResult<Void>> onUnsubscribe(
MediaLibrarySession session, ControllerInfo browser, String parentId) { 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 * Returns the controllers that are currently subscribed to the given {@code mediaId}.
* parent's children. If the browser isn't subscribing to the parent, nothing will happen.
* *
* <p>This only tells the number of child {@link MediaItem media items}. {@link * <p>Use the returned {@linkplain ControllerInfo controller infos} to call {@link
* Callback#onGetChildren} will be called by the browser afterwards to get the list of {@link * #notifyChildrenChanged} in case the children of the media item with the given media ID have
* MediaItem media items}. * 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 browser The browser to notify.
* @param parentId The non-empty id of the parent with changes to its children. * @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}. * @param params The parameters given by {@link Callback#onSubscribe}.
*/ */
public void notifyChildrenChanged( 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.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; 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_NOT_SUPPORTED;
import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED; import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED;
import static androidx.media3.session.LibraryResult.RESULT_SUCCESS; import static androidx.media3.session.LibraryResult.RESULT_SUCCESS;
@ -32,25 +30,25 @@ import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.os.RemoteException; import android.os.RemoteException;
import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.BitmapLoader;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.MediaLibraryService.LibraryParams;
import androidx.media3.session.MediaLibraryService.MediaLibrarySession; import androidx.media3.session.MediaLibraryService.MediaLibrarySession;
import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerCb;
import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.ControllerInfo;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList; 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.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@ -61,12 +59,10 @@ import java.util.concurrent.Future;
/* package */ class MediaLibrarySessionImpl extends MediaSessionImpl { /* package */ class MediaLibrarySessionImpl extends MediaSessionImpl {
private static final String RECENT_LIBRARY_ROOT_MEDIA_ID = "androidx.media3.session.recent.root"; private static final String RECENT_LIBRARY_ROOT_MEDIA_ID = "androidx.media3.session.recent.root";
private final MediaLibrarySession instance; private final MediaLibrarySession instance;
private final MediaLibrarySession.Callback callback; private final MediaLibrarySession.Callback callback;
private final HashMultimap<String, ControllerInfo> parentIdToSubscribedControllers;
@GuardedBy("lock") private final HashMultimap<ControllerCb, String> controllerToSubscribedParentIds;
private final ArrayMap<ControllerCb, Set<String>> subscriptions;
/** Creates an instance. */ /** Creates an instance. */
public MediaLibrarySessionImpl( public MediaLibrarySessionImpl(
@ -93,7 +89,8 @@ import java.util.concurrent.Future;
playIfSuppressed); playIfSuppressed);
this.instance = instance; this.instance = instance;
this.callback = callback; this.callback = callback;
subscriptions = new ArrayMap<>(); parentIdToSubscribedControllers = HashMultimap.create();
controllerToSubscribedParentIds = HashMultimap.create();
} }
@Override @Override
@ -116,48 +113,6 @@ import java.util.concurrent.Future;
&& legacyStub.getConnectedControllersManager().isConnected(controller); && 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( public ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRootOnHandler(
ControllerInfo browser, @Nullable LibraryParams params) { ControllerInfo browser, @Nullable LibraryParams params) {
if (params != null && params.isRecent && isSystemUiController(browser)) { if (params != null && params.isRecent && isSystemUiController(browser)) {
@ -185,7 +140,7 @@ import java.util.concurrent.Future;
maybeUpdateLegacyErrorState(result); maybeUpdateLegacyErrorState(result);
} }
}, },
(Runnable r) -> postOrRun(getApplicationHandler(), r)); this::postOrRunOnApplicationHandler);
return future; return future;
} }
@ -228,7 +183,7 @@ import java.util.concurrent.Future;
verifyResultItems(result, pageSize); verifyResultItems(result, pageSize);
} }
}, },
(Runnable r) -> postOrRun(getApplicationHandler(), r)); this::postOrRunOnApplicationHandler);
return future; return future;
} }
@ -243,21 +198,16 @@ import java.util.concurrent.Future;
maybeUpdateLegacyErrorState(result); maybeUpdateLegacyErrorState(result);
} }
}, },
(Runnable r) -> postOrRun(getApplicationHandler(), r)); this::postOrRunOnApplicationHandler);
return future; return future;
} }
public ListenableFuture<LibraryResult<Void>> onSubscribeOnHandler( public ListenableFuture<LibraryResult<Void>> onSubscribeOnHandler(
ControllerInfo browser, String parentId, @Nullable LibraryParams params) { ControllerInfo browser, String parentId, @Nullable LibraryParams params) {
ControllerCb controller = checkStateNotNull(browser.getControllerCb());
synchronized (lock) { ControllerCb controllerCb = checkNotNull(browser.getControllerCb());
@Nullable Set<String> subscription = subscriptions.get(controller); controllerToSubscribedParentIds.put(controllerCb, parentId);
if (subscription == null) { parentIdToSubscribedControllers.put(parentId, browser);
subscription = new HashSet<>();
subscriptions.put(controller, subscription);
}
subscription.add(parentId);
}
// Call callbacks after adding it to the subscription list because library session may want // Call callbacks after adding it to the subscription list because library session may want
// to call notifyChildrenChanged() in the callback. // to call notifyChildrenChanged() in the callback.
@ -270,31 +220,65 @@ import java.util.concurrent.Future;
instance, resolveControllerInfoForCallback(browser), parentId, params), instance, resolveControllerInfoForCallback(browser), parentId, params),
"onSubscribe must return non-null future"); "onSubscribe must return non-null future");
// When error happens, remove from the subscription list.
future.addListener( future.addListener(
() -> { () -> {
@Nullable LibraryResult<Void> result = tryGetFutureResult(future); @Nullable LibraryResult<Void> result = tryGetFutureResult(future);
if (result == null || result.resultCode != RESULT_SUCCESS) { 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; 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( public ListenableFuture<LibraryResult<Void>> onUnsubscribeOnHandler(
ControllerInfo browser, String parentId) { ControllerInfo browser, String parentId) {
ListenableFuture<LibraryResult<Void>> future = ListenableFuture<LibraryResult<Void>> future =
callback.onUnsubscribe(instance, resolveControllerInfoForCallback(browser), parentId); callback.onUnsubscribe(instance, resolveControllerInfoForCallback(browser), parentId);
future.addListener( future.addListener(
() -> removeSubscription(checkStateNotNull(browser.getControllerCb()), parentId), () -> removeSubscription(browser, parentId), this::postOrRunOnApplicationHandler);
MoreExecutors.directExecutor());
return future; 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( public ListenableFuture<LibraryResult<Void>> onSearchOnHandler(
ControllerInfo browser, String query, @Nullable LibraryParams params) { ControllerInfo browser, String query, @Nullable LibraryParams params) {
ListenableFuture<LibraryResult<Void>> future = ListenableFuture<LibraryResult<Void>> future =
@ -306,7 +290,7 @@ import java.util.concurrent.Future;
maybeUpdateLegacyErrorState(result); maybeUpdateLegacyErrorState(result);
} }
}, },
(Runnable r) -> postOrRun(getApplicationHandler(), r)); this::postOrRunOnApplicationHandler);
return future; return future;
} }
@ -327,10 +311,33 @@ import java.util.concurrent.Future;
verifyResultItems(result, pageSize); verifyResultItems(result, pageSize);
} }
}, },
(Runnable r) -> postOrRun(getApplicationHandler(), r)); this::postOrRunOnApplicationHandler);
return future; 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 @Override
@Nullable @Nullable
protected MediaLibraryServiceLegacyStub getLegacyBrowserService() { 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) { private void maybeUpdateLegacyErrorState(LibraryResult<?> result) {
PlayerWrapper playerWrapper = getPlayerWrapper(); PlayerWrapper playerWrapper = getPlayerWrapper();
if (result.resultCode == RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED if (result.resultCode == RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED
@ -410,16 +407,14 @@ import java.util.concurrent.Future;
} }
} }
private void removeSubscription(ControllerCb controllerCb, String parentId) { private void removeSubscription(ControllerInfo controllerInfo, String parentId) {
synchronized (lock) { ControllerCb controllerCb = checkNotNull(controllerInfo.getControllerCb());
@Nullable Set<String> subscription = subscriptions.get(controllerCb); parentIdToSubscribedControllers.remove(parentId, controllerInfo);
if (subscription != null) { controllerToSubscribedParentIds.remove(controllerCb, parentId);
subscription.remove(parentId); }
if (subscription.isEmpty()) {
subscriptions.remove(controllerCb); private void postOrRunOnApplicationHandler(Runnable runnable) {
} Util.postOrRun(getApplicationHandler(), runnable);
}
}
} }
private ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> 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); 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 Uri sessionUri;
private final PlayerInfoChangedHandler onPlayerInfoChangedHandler; 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 List<String> SEARCH_RESULT = new ArrayList<>();
public static final int SEARCH_RESULT_COUNT = 50; public static final int SEARCH_RESULT_COUNT = 50;
public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL = public static final String SUBSCRIBE_PARENT_ID_1 = "subscribe_parent_id_1";
"subscribe_id_notify_children_changed_to_all"; public static final String SUBSCRIBE_PARENT_ID_2 = "subscribe_parent_id_2";
public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE = public static final String EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_MEDIA_ID =
"subscribe_id_notify_children_changed_to_one"; "notify_children_changed_media_id";
public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID = public static final String EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_ITEM_COUNT =
"subscribe_id_notify_children_changed_to_all_with_non_subscribed_id"; "notify_children_changed_item_count";
public static final String SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID = public static final String EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_DELAY_MS =
"subscribe_id_notify_children_changed_to_one_with_non_subscribed_id"; "notify_children_changed_delay";
public static final int NOTIFY_CHILDREN_CHANGED_ITEM_COUNT = 101; public static final String EXTRAS_KEY_NOTIFY_CHILDREN_CHANGED_BROADCAST =
public static final Bundle NOTIFY_CHILDREN_CHANGED_EXTRAS = TestUtils.createTestBundle(); "notify_children_changed_broadcast";
public static final String CUSTOM_ACTION = "customAction"; public static final String CUSTOM_ACTION = "customAction";
public static final Bundle CUSTOM_ACTION_EXTRAS = new Bundle(); 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.LibraryResult.RESULT_SUCCESS;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMPLETION_STATUS; 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.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.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.CUSTOM_ACTION_ASSERT_PARAMS;
import static androidx.media3.test.session.common.MediaBrowserConstants.LONG_LIST_COUNT; 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.PARENT_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS; 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_KEY;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS_VALUE; 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.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_PARENT_ID_1;
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_PARENT_ID_2;
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.TestUtils.LONG_TIMEOUT_MS; 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 androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS; 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.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest; import androidx.test.filters.LargeTest;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@ -431,7 +429,7 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest {
@Test @Test
public void onChildrenChanged_calledWhenSubscribed() throws Exception { public void onChildrenChanged_calledWhenSubscribed() throws Exception {
// This test uses MediaLibrarySession.notifyChildrenChanged(). // 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); CountDownLatch latch = new CountDownLatch(1);
AtomicReference<String> parentIdRef = new AtomicReference<>(); AtomicReference<String> parentIdRef = new AtomicReference<>();
@ -453,7 +451,7 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest {
LibraryResult<Void> result = LibraryResult<Void> result =
threadTestRule threadTestRule
.getHandler() .getHandler()
.postAndSync(() -> browser.subscribe(expectedParentId, null)) .postAndSync(() -> browser.subscribe(expectedParentId, /* params= */ null))
.get(TIMEOUT_MS, MILLISECONDS); .get(TIMEOUT_MS, MILLISECONDS);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
@ -461,27 +459,23 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest {
// notifyChildrenChanged() in its onSubscribe() method. // notifyChildrenChanged() in its onSubscribe() method.
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(parentIdRef.get()).isEqualTo(expectedParentId); assertThat(parentIdRef.get()).isEqualTo(expectedParentId);
assertThat(itemCountRef.get()).isEqualTo(NOTIFY_CHILDREN_CHANGED_ITEM_COUNT); assertThat(itemCountRef.get()).isEqualTo(Integer.MAX_VALUE);
MediaTestUtils.assertLibraryParamsEquals(paramsRef.get(), NOTIFY_CHILDREN_CHANGED_EXTRAS);
} }
@Test @Test
public void onChildrenChanged_calledWhenSubscribed2() throws Exception { public void onChildrenChanged_calledWhenSubscribedAndWithDelay() throws Exception {
// This test uses MediaLibrarySession.notifyChildrenChanged(ControllerInfo). String expectedParentId = SUBSCRIBE_PARENT_ID_2;
String expectedParentId = SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE;
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(2);
AtomicReference<String> parentIdRef = new AtomicReference<>(); List<String> parentIds = new ArrayList<>();
AtomicInteger itemCountRef = new AtomicInteger(); List<Integer> itemCounts = new ArrayList<>();
AtomicReference<LibraryParams> paramsRef = new AtomicReference<>();
MediaBrowser.Listener browserListenerProxy = MediaBrowser.Listener browserListenerProxy =
new MediaBrowser.Listener() { new MediaBrowser.Listener() {
@Override @Override
public void onChildrenChanged( public void onChildrenChanged(
MediaBrowser browser, String parentId, int itemCount, LibraryParams params) { MediaBrowser browser, String parentId, int itemCount, LibraryParams params) {
parentIdRef.set(parentId); parentIds.add(parentId);
itemCountRef.set(itemCount); itemCounts.add(itemCount);
paramsRef.set(params);
latch.countDown(); latch.countDown();
} }
}; };
@ -490,75 +484,108 @@ public class MediaBrowserListenerTest extends MediaControllerListenerTest {
LibraryResult<Void> result = LibraryResult<Void> result =
threadTestRule threadTestRule
.getHandler() .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); .get(TIMEOUT_MS, MILLISECONDS);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS); 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(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(parentIdRef.get()).isEqualTo(expectedParentId); assertThat(parentIds).containsExactly(expectedParentId, expectedParentId);
assertThat(itemCountRef.get()).isEqualTo(NOTIFY_CHILDREN_CHANGED_ITEM_COUNT); assertThat(itemCounts).containsExactly(Integer.MAX_VALUE, 12);
MediaTestUtils.assertLibraryParamsEquals(paramsRef.get(), NOTIFY_CHILDREN_CHANGED_EXTRAS);
} }
@Test @Test
public void onChildrenChanged_notCalledWhenNotSubscribed() throws Exception { public void onChildrenChanged_notCalledWhenNotSubscribed() throws Exception {
// This test uses MediaLibrarySession.notifyChildrenChanged(). String mediaId1 = SUBSCRIBE_PARENT_ID_1;
String subscribedMediaId = SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ALL_WITH_NON_SUBSCRIBED_ID; String mediaId2 = SUBSCRIBE_PARENT_ID_2;
CountDownLatch latch = new CountDownLatch(1); List<String> notifiedParentIds = new ArrayList<>();
List<Integer> notifiedItemCounts = new ArrayList<>();
MediaBrowser.Listener browserListenerProxy = CountDownLatch childrenChangedLatch = new CountDownLatch(4);
CountDownLatch disconnectLatch = new CountDownLatch(2);
MediaBrowser.Listener browserListener =
new MediaBrowser.Listener() { new MediaBrowser.Listener() {
@Override @Override
public void onChildrenChanged( public void onChildrenChanged(
MediaBrowser browser, String parentId, int itemCount, LibraryParams params) { 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 @Override
public void onChildrenChanged( public void onDisconnected(MediaController controller) {
MediaBrowser browser, String parentId, int itemCount, LibraryParams params) { disconnectLatch.countDown();
latch.countDown();
} }
}; };
MediaBrowser browser1 = createBrowser(/* connectionHints= */ null, browserListener);
MediaBrowser browser = createBrowser(null, browserListenerProxy); MediaBrowser browser2 = createBrowser(/* connectionHints= */ null, browserListener);
LibraryResult<Void> result = // Subscribe both browsers each to a different media IDs and request a second update after a
// delay.
LibraryResult<Void> subscriptionResult1 =
threadTestRule threadTestRule
.getHandler() .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); .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 assertThat(childrenChangedLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
// notifyChildrenChanged(ControllerInfo) in its onSubscribe() method, but assertThat(notifiedParentIds)
// with a different media ID. .containsExactly(
// Therefore, onChildrenChanged() should not be called. mediaId1, // callback when subscribing browser1
assertThat(latch.await(NO_RESPONSE_TIMEOUT_MS, MILLISECONDS)).isFalse(); 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) 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.ComponentName;
import android.content.Context; import android.content.Context;
import android.os.Bundle;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player; import androidx.media3.common.Player;
@ -100,7 +101,8 @@ public class MediaLibraryServiceTest {
return session.get(); return session.get();
}); });
// Create the remote browser to start the service. // 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. // Get the started service instance after creation.
MockMediaLibraryService service = MockMediaLibraryService service =
(MockMediaLibraryService) testServiceRegistry.getServiceInstance(); (MockMediaLibraryService) testServiceRegistry.getServiceInstance();

View File

@ -15,11 +15,15 @@
*/ */
package androidx.media3.session; 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 androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context; import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata; 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.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule; import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
@ -77,11 +83,13 @@ public class MediaLibrarySessionCallbackTest {
} }
@Test @Test
public void onSubscribe() throws Exception { public void onSubscribeUnsubscribe() throws Exception {
String testParentId = "testSubscribeId"; String testParentId = "testSubscribeUnsubscribeId";
LibraryParams testParams = MediaTestUtils.createLibraryParams(); LibraryParams testParams = MediaTestUtils.createLibraryParams();
CountDownLatch latch = new CountDownLatch(2);
CountDownLatch latch = new CountDownLatch(1); AtomicReference<LibraryParams> libraryParamsRef = new AtomicReference<>();
List<ControllerInfo> subscribedControllers = new ArrayList<>();
List<String> parentIds = new ArrayList<>();
MediaLibrarySession.Callback sessionCallback = MediaLibrarySession.Callback sessionCallback =
new MediaLibrarySession.Callback() { new MediaLibrarySession.Callback() {
@Override @Override
@ -90,24 +98,154 @@ public class MediaLibrarySessionCallbackTest {
ControllerInfo browser, ControllerInfo browser,
String parentId, String parentId,
@Nullable LibraryParams params) { @Nullable LibraryParams params) {
assertThat(parentId).isEqualTo(testParentId); parentIds.add(parentId);
MediaTestUtils.assertLibraryParamsEquals(testParams, params); libraryParamsRef.set(params);
subscribedControllers.addAll(session.getSubscribedControllers(parentId));
latch.countDown(); latch.countDown();
return Futures.immediateFuture(LibraryResult.ofVoid(params)); 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(); MockMediaLibraryService service = new MockMediaLibraryService();
service.attachBaseContext(context); service.attachBaseContext(context);
MediaLibrarySession session = MediaLibrarySession session =
sessionTestRule.ensureReleaseAfterTest( sessionTestRule.ensureReleaseAfterTest(
new MediaLibrarySession.Builder(service, player, sessionCallback) new MediaLibrarySession.Builder(service, player, sessionCallback)
.setId("testOnSubscribe") .setId("testOnSubscribe")
.build()); .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.subscribe(testParentId, testParams);
browser.unsubscribe(testParentId);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); 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 @Test
@ -115,12 +253,15 @@ public class MediaLibrarySessionCallbackTest {
String testParentId = "testUnsubscribeId"; String testParentId = "testUnsubscribeId";
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
List<ControllerInfo> subscribedControllers = new ArrayList<>();
List<String> parentIds = new ArrayList<>();
MediaLibrarySession.Callback sessionCallback = MediaLibrarySession.Callback sessionCallback =
new MediaLibrarySession.Callback() { new MediaLibrarySession.Callback() {
@Override @Override
public ListenableFuture<LibraryResult<Void>> onUnsubscribe( public ListenableFuture<LibraryResult<Void>> onUnsubscribe(
MediaLibrarySession session, ControllerInfo browser, String parentId) { MediaLibrarySession session, ControllerInfo browser, String parentId) {
assertThat(parentId).isEqualTo(testParentId); parentIds.add(parentId);
subscribedControllers.addAll(session.getSubscribedControllers(parentId));
latch.countDown(); latch.countDown();
return Futures.immediateFuture(LibraryResult.ofVoid()); return Futures.immediateFuture(LibraryResult.ofVoid());
} }
@ -134,9 +275,14 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, sessionCallback) new MediaLibrarySession.Builder(service, player, sessionCallback)
.setId("testOnUnsubscribe") .setId("testOnUnsubscribe")
.build()); .build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken()); RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(
session.getToken(), /* connectionHints= */ Bundle.EMPTY);
browser.unsubscribe(testParentId); browser.unsubscribe(testParentId);
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(parentIds).containsExactly(testParentId);
// The browser wasn't subscribed.
assertThat(subscribedControllers).isEmpty();
} }
@Test @Test
@ -167,7 +313,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback) new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_callForRecentRootNonSystemUiPackageName_notIntercepted") .setId("onGetChildren_callForRecentRootNonSystemUiPackageName_notIntercepted")
.build()); .build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken()); RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<MediaItem> libraryRoot = LibraryResult<MediaItem> libraryRoot =
browser.getLibraryRoot(new LibraryParams.Builder().setRecent(true).build()); browser.getLibraryRoot(new LibraryParams.Builder().setRecent(true).build());
@ -212,7 +359,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback) new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems") .setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems")
.build()); .build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken()); RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<ImmutableList<MediaItem>> recentItem = LibraryResult<ImmutableList<MediaItem>> recentItem =
browser.getChildren( browser.getChildren(
@ -254,7 +402,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback) new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems") .setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems")
.build()); .build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken()); RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<ImmutableList<MediaItem>> recentItem = LibraryResult<ImmutableList<MediaItem>> recentItem =
browser.getChildren( browser.getChildren(
@ -291,7 +440,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback) new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems") .setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems")
.build()); .build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken()); RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<ImmutableList<MediaItem>> recentItem = LibraryResult<ImmutableList<MediaItem>> recentItem =
browser.getChildren( browser.getChildren(
@ -329,7 +479,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback) new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems") .setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems")
.build()); .build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken()); RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<ImmutableList<MediaItem>> recentItem = LibraryResult<ImmutableList<MediaItem>> recentItem =
browser.getChildren( browser.getChildren(
@ -372,7 +523,8 @@ public class MediaLibrarySessionCallbackTest {
new MediaLibrarySession.Builder(service, player, callback) new MediaLibrarySession.Builder(service, player, callback)
.setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems") .setId("onGetChildren_systemUiCallForRecentItems_returnsRecentItems")
.build()); .build());
RemoteMediaBrowser browser = controllerTestRule.createRemoteBrowser(session.getToken()); RemoteMediaBrowser browser =
controllerTestRule.createRemoteBrowser(session.getToken(), Bundle.EMPTY);
LibraryResult<ImmutableList<MediaItem>> recentItem = LibraryResult<ImmutableList<MediaItem>> recentItem =
browser.getChildren( 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 * Creates {@link RemoteMediaBrowser} from {@link SessionToken} with default options waiting for
* connection. * connection.
*/ */
public RemoteMediaBrowser createRemoteBrowser(SessionToken token) throws RemoteException { public RemoteMediaBrowser createRemoteBrowser(SessionToken token, Bundle connectionHints)
throws RemoteException {
RemoteMediaBrowser browser = RemoteMediaBrowser browser =
new RemoteMediaBrowser( new RemoteMediaBrowser(context, token, /* waitForConnection= */ true, connectionHints);
context, token, /* waitForConnection= */ true, /* connectionHints= */ null);
controllers.add(browser); controllers.add(browser);
return browser; return browser;
} }

View File

@ -16,28 +16,31 @@
package androidx.media3.session; package androidx.media3.session;
import static androidx.media3.common.util.Assertions.checkNotNull; 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_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_INTENT_COMPAT;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_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.EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED;
import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; 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.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;
import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION_ASSERT_PARAMS; 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.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.GET_CHILDREN_RESULT;
import static androidx.media3.test.session.common.MediaBrowserConstants.LONG_LIST_COUNT; 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_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_ITEM_WITH_METADATA;
import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM; 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;
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;
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_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_ERROR;
import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST; 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_EXTRAS;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID;
import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_ID_SUPPORTS_BROWSABLE_CHILDREN_ONLY; 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;
import static androidx.media3.test.session.common.MediaBrowserConstants.SEARCH_RESULT_COUNT; 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.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_PARENT_ID_1;
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_PARENT_ID_2;
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 java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.app.PendingIntent; import android.app.PendingIntent;
@ -70,6 +71,7 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.test.session.common.CommonConstants; import androidx.media3.test.session.common.CommonConstants;
import androidx.media3.test.session.common.TestHandler;
import androidx.media3.test.session.common.TestUtils; import androidx.media3.test.session.common.TestUtils;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
@ -77,6 +79,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -108,8 +111,6 @@ public class MockMediaLibraryService extends MediaLibraryService {
.build(); .build();
public static final LibraryParams ROOT_PARAMS = public static final LibraryParams ROOT_PARAMS =
new LibraryParams.Builder().setExtras(ROOT_EXTRAS).build(); 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"; private static final String TAG = "MockMediaLibrarySvc2";
@ -121,11 +122,11 @@ public class MockMediaLibraryService extends MediaLibraryService {
private static LibraryParams expectedParams; private static LibraryParams expectedParams;
@Nullable private static byte[] testArtworkData; @Nullable private static byte[] testArtworkData;
private final AtomicInteger boundControllerCount; private final AtomicInteger boundControllerCount;
private final ConditionVariable allControllersUnbound; private final ConditionVariable allControllersUnbound;
@Nullable MediaLibrarySession session; @Nullable MediaLibrarySession session;
@Nullable TestHandler handler;
@Nullable HandlerThread handlerThread; @Nullable HandlerThread handlerThread;
public MockMediaLibraryService() { public MockMediaLibraryService() {
@ -159,6 +160,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
super.onCreate(); super.onCreate();
handlerThread = new HandlerThread(TAG); handlerThread = new HandlerThread(TAG);
handlerThread.start(); handlerThread.start();
handler = new TestHandler(handlerThread.getLooper());
} }
@Override @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 { private class TestLibrarySessionCallback implements MediaLibrarySession.Callback {
@Override @Override
@ -293,8 +305,13 @@ public class MockMediaLibraryService extends MediaLibraryService {
@Override @Override
public ListenableFuture<LibraryResult<MediaItem>> onGetItem( public ListenableFuture<LibraryResult<MediaItem>> onGetItem(
MediaLibrarySession session, ControllerInfo browser, String mediaId) { MediaLibrarySession session, ControllerInfo browser, String mediaId) {
if (mediaId.startsWith(SUBSCRIBE_PARENT_ID_1)) {
return Futures.immediateFuture(
LibraryResult.ofItem(createBrowsableMediaItem(mediaId), /* params= */ null));
}
switch (mediaId) { switch (mediaId) {
case MEDIA_ID_GET_BROWSABLE_ITEM: case MEDIA_ID_GET_BROWSABLE_ITEM:
case SUBSCRIBE_PARENT_ID_2:
return Futures.immediateFuture( return Futures.immediateFuture(
LibraryResult.ofItem(createBrowsableMediaItem(mediaId), /* params= */ null)); LibraryResult.ofItem(createBrowsableMediaItem(mediaId), /* params= */ null));
case MEDIA_ID_GET_PLAYABLE_ITEM: case MEDIA_ID_GET_PLAYABLE_ITEM:
@ -306,7 +323,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
LibraryResult.ofItem(createMediaItemWithMetadata(mediaId), /* params= */ null)); LibraryResult.ofItem(createMediaItemWithMetadata(mediaId), /* params= */ null));
default: // fall out default: // fall out
} }
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE));
} }
@Override @Override
@ -318,19 +335,21 @@ public class MockMediaLibraryService extends MediaLibraryService {
int pageSize, int pageSize,
@Nullable LibraryParams params) { @Nullable LibraryParams params) {
assertLibraryParams(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( return Futures.immediateFuture(
LibraryResult.ofItemList( LibraryResult.ofItemList(
getPaginatedResult(GET_CHILDREN_RESULT, page, pageSize), params)); 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); List<MediaItem> list = new ArrayList<>(LONG_LIST_COUNT);
for (int i = 0; i < LONG_LIST_COUNT; i++) { for (int i = 0; i < LONG_LIST_COUNT; i++) {
list.add(createPlayableMediaItem(TestUtils.getMediaIdInFakeTimeline(i))); list.add(createPlayableMediaItem(TestUtils.getMediaIdInFakeTimeline(i)));
} }
return Futures.immediateFuture(LibraryResult.ofItemList(list, params)); return Futures.immediateFuture(LibraryResult.ofItemList(list, params));
} else if (PARENT_ID_ERROR.equals(parentId)) { } else if (Objects.equals(parentId, PARENT_ID_ERROR)) {
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE));
} else if (PARENT_ID_AUTH_EXPIRED_ERROR.equals(parentId)) { } else if (Objects.equals(parentId, PARENT_ID_AUTH_EXPIRED_ERROR)) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
Intent signInIntent = new Intent("action"); Intent signInIntent = new Intent("action");
int flags = Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0; 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, LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED,
new LibraryParams.Builder().setExtras(bundle).build())); new LibraryParams.Builder().setExtras(bundle).build()));
} }
// Includes the case of PARENT_ID_NO_CHILDREN. return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_BAD_VALUE, params));
return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), 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 @Override
@ -406,46 +454,10 @@ public class MockMediaLibraryService extends MediaLibraryService {
return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), params)); return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), params));
} else { } else {
// SEARCH_QUERY_ERROR will be handled here. // 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 @Override
public ListenableFuture<SessionResult> onCustomCommand( public ListenableFuture<SessionResult> onCustomCommand(
MediaSession session, MediaSession session,
@ -471,7 +483,7 @@ public class MockMediaLibraryService extends MediaLibraryService {
private void assertLibraryParams(@Nullable LibraryParams params) { private void assertLibraryParams(@Nullable LibraryParams params) {
synchronized (MockMediaLibraryService.class) { synchronized (MockMediaLibraryService.class) {
if (assertLibraryParams) { if (assertLibraryParams) {
assertLibraryParamsEquals(expectedParams, params); MediaTestUtils.assertLibraryParamsEquals(expectedParams, params);
} }
} }
} }