Support setMediaItem(s) in MediaControllerImplLegacy

These calls were not implemented so far as they require a mix of
initial prepareFrom/playFrom calls and addQueueItem. We can also
support clients without queue handling to set single MediaItems.

To make the calls consistent and predictable in the session,
we need to ensure that none of the play/pause/addQueueItem/
removeQueueItem/prepare/playFromXYZ/prepareFromXYZ are called
before the controller is prepared and has media.

#minor-release

PiperOrigin-RevId: 455110246
(cherry picked from commit b475f1f2daba8e0ed2497cbf17f4b834e58c59a4)
This commit is contained in:
tonihei 2022-06-15 13:01:33 +00:00 committed by Marc Baechinger
parent 080b1862c2
commit 6ed3e40681
8 changed files with 252 additions and 557 deletions

View File

@ -179,6 +179,8 @@
of requests.
* Forward legacy `MediaController` calls to play media to
`MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`.
* Support `setMediaItems(s)` methods when `MediaController` connects to a
legacy media session.
* Data sources:
* Rename `DummyDataSource` to `PlaceholderDataSource`.
* Workaround OkHttp interrupt handling.

View File

@ -81,13 +81,14 @@ import org.checkerframework.checker.initialization.qual.Initialized;
* <li><a href="#ControllerLifeCycle">Controller Lifecycle</a>
* <li><a href="#ThreadingModel">Threading Model</a>
* <li><a href="#PackageVisibilityFilter">Package Visibility Filter</a>
* <li><a href="#BackwardCompatibility">Backward Compatibility with legacy media sessions</a>
* </ol>
*
* <h2 id="ControllerLifeCycle">Controller Lifecycle</h2>
*
* <p>When a controller is created with the {@link SessionToken} for a {@link MediaSession} (i.e.
* session token type is {@link SessionToken#TYPE_SESSION}), the controller will connect to the
* specific session.
* specific session.F
*
* <p>When a controller is created with the {@link SessionToken} for a {@link MediaSessionService}
* (i.e. session token type is {@link SessionToken#TYPE_SESSION_SERVICE} or {@link
@ -127,6 +128,34 @@ import org.checkerframework.checker.initialization.qual.Initialized;
* <!-- Or, as a package name -->
* <package android:name="package_name_of_the_other_app" />
* }</pre>
*
* <h2 id="BackwardCompatibility">Backward Compatibility with legacy media sessions</h2>
*
* <p>In addition to {@link MediaSession}, the controller also supports connecting to a legacy media
* session - {@linkplain android.media.session.MediaSession framework session} and {@linkplain
* MediaSessionCompat AndroidX session compat}.
*
* <p>To request legacy sessions to play media, use one of the {@link #setMediaItem} methods and set
* either {@link MediaItem#mediaId}, {@link MediaItem.RequestMetadata#mediaUri} or {@link
* MediaItem.RequestMetadata#searchQuery}. Once the controller is {@linkplain #prepare() prepared},
* the controller triggers one of the following callbacks depending on the provided information and
* the value of {@link #getPlayWhenReady()}:
*
* <ul>
* <li>{@link MediaSessionCompat.Callback#onPrepareFromUri onPrepareFromUri}
* <li>{@link MediaSessionCompat.Callback#onPlayFromUri onPlayFromUri}
* <li>{@link MediaSessionCompat.Callback#onPrepareFromMediaId onPrepareFromMediaId}
* <li>{@link MediaSessionCompat.Callback#onPlayFromMediaId onPlayFromMediaId}
* <li>{@link MediaSessionCompat.Callback#onPrepareFromSearch onPrepareFromSearch}
* <li>{@link MediaSessionCompat.Callback#onPlayFromSearch onPlayFromSearch}
* </ul>
*
* Other playlist change methods, like {@link #addMediaItem} or {@link #removeMediaItem}, trigger
* the {@link MediaSessionCompat.Callback#onAddQueueItem onAddQueueItem} and {@link
* MediaSessionCompat.Callback#onRemoveQueueItem} onRemoveQueueItem} callbacks. Check {@link
* #getAvailableCommands()} to see if playlist modifications are {@linkplain
* androidx.media3.common.Player.Command#COMMAND_CHANGE_MEDIA_ITEMS supported} by the legacy
* session.
*/
public class MediaController implements Player {
@ -478,13 +507,6 @@ public class MediaController implements Player {
return impl.isConnected();
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, then this will be grouped together with
* previously called {@link #setMediaUri}. See {@link #setMediaUri} for details.
*/
@Override
public void play() {
verifyApplicationThread();
@ -505,13 +527,6 @@ public class MediaController implements Player {
impl.pause();
}
/**
* {@inheritDoc}
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, then this will be grouped together with
* previously called {@link #setMediaUri}. See {@link #setMediaUri} for details.
*/
@Override
public void prepare() {
verifyApplicationThread();
@ -980,44 +995,6 @@ public class MediaController implements Player {
* <p>The {@link Player.Listener#onTimelineChanged} and/or {@link
* Player.Listener#onMediaItemTransition} would be called when it's completed.
*
* <p>Interoperability: When connected to {@link
* android.support.v4.media.session.MediaSessionCompat}, this call will be grouped together with
* later {@link #prepare} or {@link #play}, depending on the uri pattern as follows:
*
* <table>
* <caption>Uri patterns and following API calls for MediaControllerCompat methods</caption>
* <tr>
* <th>Uri patterns</th><th>Following API calls</th><th>Method</th>
* </tr><tr>
* <td rowspan="2">{@code androidx://media3-session/setMediaUri?uri=[uri]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
* </tr><tr>
* <td rowspan="2">{@code androidx://media3-session/setMediaUri?id=[mediaId]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId}
* </tr><tr>
* <td rowspan="2">{@code androidx://media3-session/setMediaUri?query=[query]}</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch}
* </tr><tr>
* <td rowspan="2">Does not match with any pattern above</td>
* <td>{@link #prepare}</td>
* <td>{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
* </tr><tr>
* <td>{@link #play}</td>
* <td>{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
* </tr></table>
*
* <p>Returned {@link ListenableFuture} will return {@link SessionResult#RESULT_SUCCESS} when it's
* handled together with {@link #prepare} or {@link #play}. If this API is called multiple times
* without prepare or play, then {@link SessionResult#RESULT_INFO_SKIPPED} will be returned for
@ -1027,15 +1004,6 @@ public class MediaController implements Player {
* @param extras A {@link Bundle} to send extra information. May be empty.
* @return A {@link ListenableFuture} of {@link SessionResult} representing the pending
* completion.
* @see MediaConstants#MEDIA_URI_AUTHORITY
* @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_MEDIA_ID
* @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_MEDIA_ID
* @see MediaConstants#MEDIA_URI_PATH_PREPARE_FROM_SEARCH
* @see MediaConstants#MEDIA_URI_PATH_PLAY_FROM_SEARCH
* @see MediaConstants#MEDIA_URI_PATH_SET_MEDIA_URI
* @see MediaConstants#MEDIA_URI_QUERY_ID
* @see MediaConstants#MEDIA_URI_QUERY_QUERY
* @see MediaConstants#MEDIA_URI_QUERY_URI
*/
public ListenableFuture<SessionResult> setMediaUri(Uri uri, Bundle extras) {
verifyApplicationThread();

View File

@ -31,16 +31,12 @@ import static androidx.media3.common.Player.PLAY_WHEN_READY_CHANGE_REASON_USER_R
import static androidx.media3.common.Player.STATE_IDLE;
import static androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED;
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.session.MediaConstants.ARGUMENT_CAPTIONING_ENABLED;
import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_ID;
import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_QUERY;
import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_URI;
import static androidx.media3.session.MediaConstants.MEDIA_URI_SET_MEDIA_URI_PREFIX;
import static androidx.media3.session.MediaConstants.SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED;
import static androidx.media3.session.MediaUtils.POSITION_DIFF_TOLERANCE_MS;
import static androidx.media3.session.MediaUtils.calculateBufferedPercentage;
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
import static java.lang.Math.max;
import static java.lang.Math.min;
@ -60,7 +56,6 @@ import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.MediaSessionCompat.QueueItem;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import android.util.Pair;
import android.view.Surface;
import android.view.SurfaceHolder;
@ -112,30 +107,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
private static final long AGGREGATES_CALLBACKS_WITHIN_TIMEOUT_MS = 500L;
private static final int VOLUME_FLAGS = AudioManager.FLAG_SHOW_UI;
final Context context;
/* package */ final Context context;
/* package */ final MediaController instance;
private final SessionToken token;
final MediaController instance;
private final ListenerSet<Listener> listeners;
private final ControllerCompatCallback controllerCompatCallback;
@Nullable private MediaControllerCompat controllerCompat;
@Nullable private MediaBrowserCompat browserCompat;
private boolean released;
private boolean connected;
@Nullable private SetMediaUriRequest pendingSetMediaUriRequest;
private LegacyPlayerInfo legacyPlayerInfo;
private LegacyPlayerInfo pendingLegacyPlayerInfo;
private ControllerInfo controllerInfo;
public MediaControllerImplLegacy(Context context, MediaController instance, SessionToken token) {
@ -177,6 +161,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Override
public void stop() {
if (controllerInfo.playerInfo.playbackState == STATE_IDLE) {
return;
}
PlayerInfo maskedPlayerInfo =
controllerInfo.playerInfo.copyWithSessionPositionInfo(
createSessionPositionInfo(
@ -244,6 +231,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Override
public void play() {
if (controllerInfo.playerInfo.playWhenReady) {
return;
}
ControllerInfo maskedControllerInfo =
new ControllerInfo(
controllerInfo.playerInfo.copyWithPlayWhenReady(
@ -258,36 +248,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* discontinuityReason= */ null,
/* mediaItemTransitionReason= */ null);
if (pendingSetMediaUriRequest == null) {
if (isPrepared() && hasMedia()) {
controllerCompat.getTransportControls().play();
} else {
switch (pendingSetMediaUriRequest.type) {
case MEDIA_URI_QUERY_ID:
controllerCompat
.getTransportControls()
.playFromMediaId(pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras);
break;
case MEDIA_URI_QUERY_QUERY:
controllerCompat
.getTransportControls()
.playFromSearch(pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras);
break;
case MEDIA_URI_QUERY_URI:
controllerCompat
.getTransportControls()
.playFromUri(
Uri.parse(pendingSetMediaUriRequest.value), pendingSetMediaUriRequest.extras);
break;
default:
throw new IllegalStateException("Unexpected type " + pendingSetMediaUriRequest.type);
}
pendingSetMediaUriRequest.result.set(new SessionResult(RESULT_SUCCESS));
pendingSetMediaUriRequest = null;
}
}
@Override
public void pause() {
if (!controllerInfo.playerInfo.playWhenReady) {
return;
}
ControllerInfo maskedControllerInfo =
new ControllerInfo(
controllerInfo.playerInfo.copyWithPlayWhenReady(
@ -302,11 +272,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* discontinuityReason= */ null,
/* mediaItemTransitionReason= */ null);
controllerCompat.getTransportControls().pause();
if (isPrepared() && hasMedia()) {
controllerCompat.getTransportControls().pause();
}
}
@Override
public void prepare() {
if (controllerInfo.playerInfo.playbackState != STATE_IDLE) {
return;
}
ControllerInfo maskedControllerInfo =
new ControllerInfo(
controllerInfo.playerInfo.copyWithPlaybackState(
@ -322,32 +297,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* discontinuityReason= */ null,
/* mediaItemTransitionReason= */ null);
if (pendingSetMediaUriRequest == null) {
controllerCompat.getTransportControls().prepare();
} else {
switch (pendingSetMediaUriRequest.type) {
case MEDIA_URI_QUERY_ID:
controllerCompat
.getTransportControls()
.prepareFromMediaId(
pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras);
break;
case MEDIA_URI_QUERY_QUERY:
controllerCompat
.getTransportControls()
.prepareFromSearch(pendingSetMediaUriRequest.value, pendingSetMediaUriRequest.extras);
break;
case MEDIA_URI_QUERY_URI:
controllerCompat
.getTransportControls()
.prepareFromUri(
Uri.parse(pendingSetMediaUriRequest.value), pendingSetMediaUriRequest.extras);
break;
default:
throw new IllegalStateException("Unexpected type " + pendingSetMediaUriRequest.type);
}
pendingSetMediaUriRequest.result.set(new SessionResult(RESULT_SUCCESS));
pendingSetMediaUriRequest = null;
if (hasMedia()) {
initializeLegacyPlaylist();
}
}
@ -655,63 +606,71 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
}
@Override
public void setMediaItem(MediaItem unusedMediaItem) {
Log.w(TAG, "Session doesn't support setting media items");
public void setMediaItem(MediaItem mediaItem) {
setMediaItem(mediaItem, /* startPositionMs= */ C.TIME_UNSET);
}
@Override
public void setMediaItem(MediaItem unusedMediaItem, long unusedStartPositionMs) {
Log.w(TAG, "Session doesn't support setting media items");
public void setMediaItem(MediaItem mediaItem, long startPositionMs) {
setMediaItems(ImmutableList.of(mediaItem), /* startIndex= */ 0, startPositionMs);
}
@Override
public void setMediaItem(MediaItem unusedMediaItem, boolean unusedResetPosition) {
Log.w(TAG, "Session doesn't support setting media items");
public void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
setMediaItem(mediaItem);
}
@Override
public void setMediaItems(List<MediaItem> unusedMediaItems) {
Log.w(TAG, "Session doesn't support setting media items");
public void setMediaItems(List<MediaItem> mediaItems) {
setMediaItems(mediaItems, /* startIndex= */ 0, /* startPositionMs= */ C.TIME_UNSET);
}
@Override
public void setMediaItems(List<MediaItem> unusedMediaItems, boolean unusedResetPosition) {
Log.w(TAG, "Session doesn't support setting media items");
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
setMediaItems(mediaItems);
}
@Override
public void setMediaItems(
List<MediaItem> unusedMediaItems, int unusedStartIndex, long unusedStartPositionMs) {
Log.w(TAG, "Session doesn't support setting media items");
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
if (mediaItems.isEmpty()) {
clearMediaItems();
return;
}
QueueTimeline newQueueTimeline =
QueueTimeline.DEFAULT.copyWithNewMediaItems(/* index= */ 0, mediaItems);
if (startPositionMs == C.TIME_UNSET) {
// Assume a default start position of 0 until we know more.
startPositionMs = 0;
}
PlayerInfo maskedPlayerInfo =
controllerInfo.playerInfo.copyWithTimelineAndSessionPositionInfo(
newQueueTimeline,
createSessionPositionInfo(
createPositionInfo(startIndex, mediaItems.get(startIndex), startPositionMs),
/* isPlayingAd= */ false,
/* durationMs= */ C.TIME_UNSET,
/* bufferedPositionMs= */ 0,
/* bufferedPercentage= */ 0,
/* totalBufferedDurationMs= */ 0));
ControllerInfo maskedControllerInfo =
new ControllerInfo(
maskedPlayerInfo,
controllerInfo.availableSessionCommands,
controllerInfo.availablePlayerCommands,
controllerInfo.customLayout);
updateStateMaskedControllerInfo(
maskedControllerInfo,
/* discontinuityReason= */ null,
/* mediaItemTransitionReason= */ null);
if (isPrepared()) {
initializeLegacyPlaylist();
}
}
@Override
public ListenableFuture<SessionResult> setMediaUri(Uri uri, Bundle extras) {
if (pendingSetMediaUriRequest != null) {
Log.w(
TAG,
"SetMediaUri() is called multiple times without prepare() nor play()."
+ " Previous call will be skipped.");
pendingSetMediaUriRequest.result.set(new SessionResult(RESULT_INFO_SKIPPED));
pendingSetMediaUriRequest = null;
}
SettableFuture<SessionResult> result = SettableFuture.create();
if (uri.toString().startsWith(MEDIA_URI_SET_MEDIA_URI_PREFIX)
&& uri.getQueryParameterNames().size() == 1) {
String queryParameterName = uri.getQueryParameterNames().iterator().next();
if (TextUtils.equals(queryParameterName, MEDIA_URI_QUERY_ID)
|| TextUtils.equals(queryParameterName, MEDIA_URI_QUERY_QUERY)
|| TextUtils.equals(queryParameterName, MEDIA_URI_QUERY_URI)) {
pendingSetMediaUriRequest =
new SetMediaUriRequest(
queryParameterName, uri.getQueryParameter(queryParameterName), extras, result);
}
}
if (pendingSetMediaUriRequest == null) {
pendingSetMediaUriRequest =
new SetMediaUriRequest(MEDIA_URI_QUERY_URI, uri.toString(), extras, result);
}
return result;
Log.w(TAG, "Session doesn't support setMediaUri");
return Futures.immediateCancelledFuture();
}
@Override
@ -744,9 +703,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
if (mediaItems.isEmpty()) {
return;
}
index = min(index, getCurrentTimeline().getWindowCount());
QueueTimeline queueTimeline = (QueueTimeline) controllerInfo.playerInfo.timeline;
if (queueTimeline.isEmpty()) {
// Handle initial items in setMediaItems to ensure initial legacy session commands are called.
setMediaItems(mediaItems);
return;
}
index = min(index, getCurrentTimeline().getWindowCount());
QueueTimeline newQueueTimeline = queueTimeline.copyWithNewMediaItems(index, mediaItems);
int currentMediaItemIndex = getCurrentMediaItemIndex();
int newCurrentMediaItemIndex =
@ -765,10 +729,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* discontinuityReason= */ null,
/* mediaItemTransitionReason= */ null);
for (int i = 0; i < mediaItems.size(); i++) {
MediaItem mediaItem = mediaItems.get(i);
controllerCompat.addQueueItem(
MediaUtils.convertToMediaDescriptionCompat(mediaItem), index + i);
if (isPrepared()) {
for (int i = 0; i < mediaItems.size(); i++) {
MediaItem mediaItem = mediaItems.get(i);
controllerCompat.addQueueItem(
MediaUtils.convertToMediaDescriptionCompat(mediaItem), index + i);
}
}
}
@ -815,8 +781,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* discontinuityReason= */ null,
/* mediaItemTransitionReason= */ null);
for (int i = fromIndex; i < toIndex && i < legacyPlayerInfo.queue.size(); i++) {
controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(i).getDescription());
if (isPrepared()) {
for (int i = fromIndex; i < toIndex && i < legacyPlayerInfo.queue.size(); i++) {
controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(i).getDescription());
}
}
}
@ -876,14 +844,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* discontinuityReason= */ null,
/* mediaItemTransitionReason= */ null);
ArrayList<QueueItem> moveItems = new ArrayList<>();
for (int i = 0; i < (toIndex - fromIndex); i++) {
moveItems.add(legacyPlayerInfo.queue.get(fromIndex));
controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(fromIndex).getDescription());
}
for (int i = 0; i < moveItems.size(); i++) {
QueueItem item = moveItems.get(i);
controllerCompat.addQueueItem(item.getDescription(), i + newIndex);
if (isPrepared()) {
ArrayList<QueueItem> moveItems = new ArrayList<>();
for (int i = 0; i < (toIndex - fromIndex); i++) {
moveItems.add(legacyPlayerInfo.queue.get(fromIndex));
controllerCompat.removeQueueItem(legacyPlayerInfo.queue.get(fromIndex).getDescription());
}
for (int i = 0; i < moveItems.size(); i++) {
QueueItem item = moveItems.get(i);
controllerCompat.addQueueItem(item.getDescription(), i + newIndex);
}
}
}
@ -1294,6 +1264,91 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
});
}
private boolean isPrepared() {
return controllerInfo.playerInfo.playbackState != STATE_IDLE;
}
private boolean hasMedia() {
return !controllerInfo.playerInfo.timeline.isEmpty();
}
private void initializeLegacyPlaylist() {
Window window = new Window();
checkState(isPrepared() && hasMedia());
QueueTimeline queueTimeline = (QueueTimeline) controllerInfo.playerInfo.timeline;
// Set the current item first as these calls are expected to replace the current playlist.
int currentIndex = controllerInfo.playerInfo.sessionPositionInfo.positionInfo.mediaItemIndex;
MediaItem currentMediaItem = queueTimeline.getWindow(currentIndex, window).mediaItem;
if (queueTimeline.getQueueId(currentIndex) != QueueItem.UNKNOWN_ID) {
// Current item is already known to the session. Just prepare or play.
if (controllerInfo.playerInfo.playWhenReady) {
controllerCompat.getTransportControls().play();
} else {
controllerCompat.getTransportControls().prepare();
}
} else if (currentMediaItem.requestMetadata.mediaUri != null) {
if (controllerInfo.playerInfo.playWhenReady) {
controllerCompat
.getTransportControls()
.playFromUri(
currentMediaItem.requestMetadata.mediaUri,
getOrEmptyBundle(currentMediaItem.requestMetadata.extras));
} else {
controllerCompat
.getTransportControls()
.prepareFromUri(
currentMediaItem.requestMetadata.mediaUri,
getOrEmptyBundle(currentMediaItem.requestMetadata.extras));
}
} else if (currentMediaItem.requestMetadata.searchQuery != null) {
if (controllerInfo.playerInfo.playWhenReady) {
controllerCompat
.getTransportControls()
.playFromSearch(
currentMediaItem.requestMetadata.searchQuery,
getOrEmptyBundle(currentMediaItem.requestMetadata.extras));
} else {
controllerCompat
.getTransportControls()
.prepareFromSearch(
currentMediaItem.requestMetadata.searchQuery,
getOrEmptyBundle(currentMediaItem.requestMetadata.extras));
}
} else {
if (controllerInfo.playerInfo.playWhenReady) {
controllerCompat
.getTransportControls()
.playFromMediaId(
currentMediaItem.mediaId,
getOrEmptyBundle(currentMediaItem.requestMetadata.extras));
} else {
controllerCompat
.getTransportControls()
.prepareFromMediaId(
currentMediaItem.mediaId,
getOrEmptyBundle(currentMediaItem.requestMetadata.extras));
}
}
// Seek to non-zero start positon if needed.
if (controllerInfo.playerInfo.sessionPositionInfo.positionInfo.positionMs != 0) {
controllerCompat
.getTransportControls()
.seekTo(controllerInfo.playerInfo.sessionPositionInfo.positionInfo.positionMs);
}
// Add all other items to the playlist if supported.
if (getAvailableCommands().contains(Player.COMMAND_CHANGE_MEDIA_ITEMS)) {
for (int i = 0; i < queueTimeline.getWindowCount(); i++) {
if (i == currentIndex || queueTimeline.getQueueId(i) != QueueItem.UNKNOWN_ID) {
// Skip the current item (added above) and all items already known to the session.
continue;
}
MediaItem mediaItem = queueTimeline.getWindow(/* windowIndex= */ i, window).mediaItem;
controllerCompat.addQueueItem(
MediaUtils.convertToMediaDescriptionCompat(mediaItem), /* index= */ i);
}
}
}
private void handleNewLegacyParameters(
boolean notifyConnected, LegacyPlayerInfo newLegacyPlayerInfo) {
if (released || !connected) {
@ -1938,6 +1993,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
return state;
}
private static Bundle getOrEmptyBundle(@Nullable Bundle bundle) {
return bundle == null ? Bundle.EMPTY : bundle;
}
private static long getActiveQueueId(@Nullable PlaybackStateCompat playbackStateCompat) {
return playbackStateCompat == null
? QueueItem.UNKNOWN_ID
@ -2088,22 +2147,6 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
/* contentBufferedPositionMs= */ bufferedPositionMs);
}
private static final class SetMediaUriRequest {
public final String type;
public final String value;
public final Bundle extras;
public final SettableFuture<SessionResult> result;
public SetMediaUriRequest(
String type, String value, Bundle extras, SettableFuture<SessionResult> result) {
this.type = type;
this.value = value;
this.extras = extras;
this.result = result;
}
}
// Media 1.0 variables
private static final class LegacyPlayerInfo {

View File

@ -88,7 +88,7 @@ import java.util.List;
* <li><a href="#ThreadingModel">Threading Model</a>
* <li><a href="#KeyEvents">Media Key Events Mapping</a>
* <li><a href="#MultipleSessions">Supporting Multiple Sessions</a>
* <li><a href="#CompatibilitySession">Backward Compatibility with Legacy Session APIs</a>
* <li><a href="#BackwardCompatibility">Backward Compatibility with Legacy Session APIs</a>
* <li><a href="#CompatibilityController">Backward Compatibility with Legacy Controller APIs</a>
* </ol>
*
@ -201,10 +201,10 @@ import java.util.List;
*
* <h2 id="CompatibilityController">Backward Compatibility with Legacy Controller APIs</h2>
*
* <p>In addition to {@link MediaController}, session also supports connection from the legacy
* controller APIs - {@link android.media.session.MediaController framework controller} and {@link
* MediaControllerCompat AndroidX controller compat}. However, {@link ControllerInfo} may not be
* precise for legacy controllers. See {@link ControllerInfo} for the details.
* <p>In addition to {@link MediaController}, the session also supports connections from the legacy
* controller APIs - {@linkplain android.media.session.MediaController framework controller} and
* {@linkplain MediaControllerCompat AndroidX controller compat}. However, {@link ControllerInfo}
* may not be precise for legacy controllers. See {@link ControllerInfo} for the details.
*
* <p>Unknown package name nor UID doesn't mean that you should disallow connection nor commands.
* For SDK levels where such issues happen, session tokens could only be obtained by trusted

View File

@ -30,6 +30,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH;
@ -1058,7 +1059,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
COMMAND_SEEK_TO_NEXT,
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_GET_CURRENT_MEDIA_ITEM);
COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_SET_MEDIA_ITEM);
boolean includePlaylistCommands = (sessionFlags & FLAG_HANDLES_QUEUE_COMMANDS) != 0;
if (includePlaylistCommands) {
playerCommandsBuilder.add(COMMAND_CHANGE_MEDIA_ITEMS);

View File

@ -79,11 +79,11 @@ import java.util.Map;
newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem);
}
public QueueTimeline copyWithNewMediaItems(int addToIndex, List<MediaItem> newMediaItems) {
public QueueTimeline copyWithNewMediaItems(int index, List<MediaItem> newMediaItems) {
ImmutableList.Builder<MediaItem> newMediaItemsBuilder = new ImmutableList.Builder<>();
newMediaItemsBuilder.addAll(mediaItems.subList(0, addToIndex));
newMediaItemsBuilder.addAll(mediaItems.subList(0, index));
newMediaItemsBuilder.addAll(newMediaItems);
newMediaItemsBuilder.addAll(mediaItems.subList(addToIndex, mediaItems.size()));
newMediaItemsBuilder.addAll(mediaItems.subList(index, mediaItems.size()));
return new QueueTimeline(
newMediaItemsBuilder.build(), unmodifiableMediaItemToQueueIdMap, fakeMediaItem);
}

View File

@ -29,7 +29,6 @@ import static androidx.media3.common.Player.STATE_BUFFERING;
import static androidx.media3.common.Player.STATE_READY;
import static androidx.media3.session.MediaConstants.ARGUMENT_CAPTIONING_ENABLED;
import static androidx.media3.session.MediaConstants.SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED;
import static androidx.media3.session.SessionResult.RESULT_INFO_SKIPPED;
import static androidx.media3.session.SessionResult.RESULT_SUCCESS;
import static androidx.media3.test.session.common.CommonConstants.DEFAULT_TEST_NAME;
import static androidx.media3.test.session.common.CommonConstants.METADATA_ALBUM_TITLE;
@ -37,10 +36,8 @@ import static androidx.media3.test.session.common.CommonConstants.METADATA_ARTIS
import static androidx.media3.test.session.common.CommonConstants.METADATA_DESCRIPTION;
import static androidx.media3.test.session.common.CommonConstants.METADATA_TITLE;
import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME;
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 com.google.common.truth.Truth.assertWithMessage;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.app.PendingIntent;
@ -85,8 +82,6 @@ import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
@ -567,77 +562,6 @@ public class MediaControllerWithMediaSessionCompatTest {
assertThat(isPlayingAdRef.get()).isTrue();
}
@Test
public void setMediaUri_resultSetAfterPrepare() throws Exception {
MediaController controller = controllerTestRule.createController(session.getSessionToken());
Uri testUri = Uri.parse("androidx://test");
ListenableFuture<SessionResult> future =
threadTestRule
.getHandler()
.postAndSync(() -> controller.setMediaUri(testUri, /* extras= */ Bundle.EMPTY));
SessionResult result;
try {
result = future.get(NO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertWithMessage("TimeoutException is expected").fail();
} catch (TimeoutException e) {
// expected.
}
threadTestRule.getHandler().postAndSync(controller::prepare);
result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
}
@Test
public void setMediaUri_resultSetAfterPlay() throws Exception {
MediaController controller = controllerTestRule.createController(session.getSessionToken());
Uri testUri = Uri.parse("androidx://test");
ListenableFuture<SessionResult> future =
threadTestRule
.getHandler()
.postAndSync(() -> controller.setMediaUri(testUri, /* extras= */ Bundle.EMPTY));
SessionResult result;
try {
result = future.get(NO_RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertWithMessage("TimeoutException is expected").fail();
} catch (TimeoutException e) {
// expected.
}
threadTestRule.getHandler().postAndSync(controller::play);
result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertThat(result.resultCode).isEqualTo(RESULT_SUCCESS);
}
@Test
public void setMediaUris_multipleCalls_previousCallReturnsResultInfoSkipped() throws Exception {
MediaController controller = controllerTestRule.createController(session.getSessionToken());
Uri testUri1 = Uri.parse("androidx://test1");
Uri testUri2 = Uri.parse("androidx://test2");
ListenableFuture<SessionResult> future1 =
threadTestRule
.getHandler()
.postAndSync(() -> controller.setMediaUri(testUri1, /* extras= */ Bundle.EMPTY));
ListenableFuture<SessionResult> future2 =
threadTestRule
.getHandler()
.postAndSync(() -> controller.setMediaUri(testUri2, /* extras= */ Bundle.EMPTY));
threadTestRule.getHandler().postAndSync(controller::prepare);
SessionResult result1 = future1.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
SessionResult result2 = future2.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
assertThat(result1.resultCode).isEqualTo(RESULT_INFO_SKIPPED);
assertThat(result2.resultCode).isEqualTo(RESULT_SUCCESS);
}
@Test
public void seekToDefaultPosition_withMediaItemIndex_updatesExpectedMediaItemIndex()
throws Exception {

View File

@ -16,13 +16,6 @@
package androidx.media3.session;
import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS;
import static androidx.media3.session.MediaConstants.MEDIA_URI_AUTHORITY;
import static androidx.media3.session.MediaConstants.MEDIA_URI_PATH_SET_MEDIA_URI;
import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_ID;
import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_QUERY;
import static androidx.media3.session.MediaConstants.MEDIA_URI_QUERY_URI;
import static androidx.media3.session.MediaConstants.MEDIA_URI_SCHEME;
import static androidx.media3.test.session.common.TestUtils.NO_RESPONSE_TIMEOUT_MS;
import static androidx.media3.test.session.common.TestUtils.VOLUME_CHANGE_TIMEOUT_MS;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@ -78,6 +71,9 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
// The maximum time to wait for an operation.
private static final long TIMEOUT_MS = 3000L;
// Timeout used where the test expects no operation.
private static final long NOOP_TIMEOUT_MS = 500L;
@ClassRule public static MainLooperTestRule mainLooperTestRule = new MainLooperTestRule();
@Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG);
@ -122,6 +118,11 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
@Test
public void play() throws Exception {
List<MediaItem> testList = MediaTestUtils.createMediaItems(/* size= */ 2);
List<QueueItem> testQueue = MediaUtils.convertToQueueItemList(testList);
session.setQueue(testQueue);
session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
@ -132,6 +133,11 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
@Test
public void pause() throws Exception {
List<MediaItem> testList = MediaTestUtils.createMediaItems(/* size= */ 2);
List<QueueItem> testQueue = MediaUtils.convertToQueueItemList(testList);
session.setQueue(testQueue);
session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
@ -142,20 +148,31 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
@Test
public void prepare() throws Exception {
List<MediaItem> testList = MediaTestUtils.createMediaItems(/* size= */ 2);
List<QueueItem> testQueue = MediaUtils.convertToQueueItemList(testList);
session.setQueue(testQueue);
session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
controller.prepare();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPrepareCalled).isEqualTo(true);
}
@Test
public void stop() throws Exception {
List<MediaItem> testList = MediaTestUtils.createMediaItems(/* size= */ 2);
List<QueueItem> testQueue = MediaUtils.convertToQueueItemList(testList);
session.setQueue(testQueue);
session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
RemoteMediaController controller = createControllerAndWaitConnection();
controller.prepare();
sessionCallback.reset(1);
controller.stop();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onStopCalled).isEqualTo(true);
}
@ -314,6 +331,7 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
session.setQueue(testQueue);
session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(size);
@ -338,6 +356,7 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
session.setQueue(MediaUtils.convertToQueueItemList(testList));
session.setFlags(FLAG_HANDLES_QUEUE_COMMANDS);
setPlaybackState(PlaybackStateCompat.STATE_BUFFERING);
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(count);
@ -634,269 +653,6 @@ public class MediaSessionCompatCallbackWithMediaControllerTest {
assertThat(MediaUtils.convertToRating(sessionCallback.rating)).isEqualTo(rating);
}
@Test
public void setMediaUri_ignored() throws Exception {
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
controller.setMediaUri(Uri.parse("androidx://test?test=xx"), /* extras= */ Bundle.EMPTY);
assertThat(sessionCallback.await(NO_RESPONSE_TIMEOUT_MS)).isFalse();
}
@Test
public void setMediaUri_followedByPrepare_callsPrepareFromMediaId() throws Exception {
String testMediaId = "anyMediaId";
Bundle testExtras = new Bundle();
testExtras.putString("testKey", "testValue");
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
controller.setMediaUri(
new Uri.Builder()
.scheme(MEDIA_URI_SCHEME)
.authority(MEDIA_URI_AUTHORITY)
.path(MEDIA_URI_PATH_SET_MEDIA_URI)
.appendQueryParameter(MEDIA_URI_QUERY_ID, testMediaId)
.build(),
testExtras);
controller.prepare();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPrepareFromMediaIdCalled).isTrue();
assertThat(sessionCallback.mediaId).isEqualTo(testMediaId);
assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue();
assertThat(sessionCallback.query).isNull();
assertThat(sessionCallback.uri).isNull();
assertThat(sessionCallback.onPrepareCalled).isFalse();
}
@Test
public void setMediaUri_followedByPrepare_callsPrepareFromSearch() throws Exception {
String testSearchQuery = "anyQuery";
Bundle testExtras = new Bundle();
testExtras.putString("testKey", "testValue");
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
controller.setMediaUri(
new Uri.Builder()
.scheme(MEDIA_URI_SCHEME)
.authority(MEDIA_URI_AUTHORITY)
.path(MEDIA_URI_PATH_SET_MEDIA_URI)
.appendQueryParameter(MEDIA_URI_QUERY_QUERY, testSearchQuery)
.build(),
testExtras);
controller.prepare();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPrepareFromSearchCalled).isTrue();
assertThat(sessionCallback.query).isEqualTo(testSearchQuery);
assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue();
assertThat(sessionCallback.mediaId).isNull();
assertThat(sessionCallback.uri).isNull();
assertThat(sessionCallback.onPrepareCalled).isFalse();
}
@Test
public void setMediaUri_followedByPrepare_callsPrepareFromUri() throws Exception {
Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media");
Bundle testExtras = new Bundle();
testExtras.putString("testKey", "testValue");
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
controller.setMediaUri(
new Uri.Builder()
.scheme(MEDIA_URI_SCHEME)
.authority(MEDIA_URI_AUTHORITY)
.path(MEDIA_URI_PATH_SET_MEDIA_URI)
.appendQueryParameter(MEDIA_URI_QUERY_URI, testMediaUri.toString())
.build(),
testExtras);
controller.prepare();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPrepareFromUriCalled).isTrue();
assertThat(sessionCallback.uri).isEqualTo(testMediaUri);
assertThat(sessionCallback.mediaId).isNull();
assertThat(sessionCallback.query).isNull();
assertThat(sessionCallback.onPrepareCalled).isFalse();
}
@Test
public void setMediaUri_withoutFormattingFollowedByPrepare_callsPrepareFromUri()
throws Exception {
Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media");
Bundle testExtras = new Bundle();
testExtras.putString("testKey", "testValue");
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
controller.setMediaUri(testMediaUri, testExtras);
controller.prepare();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPrepareFromUriCalled).isTrue();
assertThat(sessionCallback.uri).isEqualTo(testMediaUri);
assertThat(sessionCallback.mediaId).isNull();
assertThat(sessionCallback.query).isNull();
assertThat(sessionCallback.onPrepareCalled).isFalse();
}
@Test
public void setMediaUri_followedByPlay_callsPlayFromMediaId() throws Exception {
String testMediaId = "anyMediaId";
Bundle testExtras = new Bundle();
testExtras.putString("testKey", "testValue");
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
controller.setMediaUri(
new Uri.Builder()
.scheme(MEDIA_URI_SCHEME)
.authority(MEDIA_URI_AUTHORITY)
.path(MEDIA_URI_PATH_SET_MEDIA_URI)
.appendQueryParameter(MEDIA_URI_QUERY_ID, testMediaId)
.build(),
testExtras);
controller.play();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPlayFromMediaIdCalled).isTrue();
assertThat(sessionCallback.mediaId).isEqualTo(testMediaId);
assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue();
assertThat(sessionCallback.query).isNull();
assertThat(sessionCallback.uri).isNull();
assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0);
}
@Test
public void setMediaUri_followedByPlay_callsPlayFromSearch() throws Exception {
String testSearchQuery = "anyQuery";
Bundle testExtras = new Bundle();
testExtras.putString("testKey", "testValue");
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
controller.setMediaUri(
new Uri.Builder()
.scheme(MEDIA_URI_SCHEME)
.authority(MEDIA_URI_AUTHORITY)
.path(MEDIA_URI_PATH_SET_MEDIA_URI)
.appendQueryParameter(MEDIA_URI_QUERY_QUERY, testSearchQuery)
.build(),
testExtras);
controller.play();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPlayFromSearchCalled).isTrue();
assertThat(sessionCallback.query).isEqualTo(testSearchQuery);
assertThat(TestUtils.equals(testExtras, sessionCallback.extras)).isTrue();
assertThat(sessionCallback.mediaId).isNull();
assertThat(sessionCallback.uri).isNull();
assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0);
}
@Test
public void setMediaUri_followedByPlay_callsPlayFromUri() throws Exception {
Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media");
Bundle testExtras = new Bundle();
testExtras.putString("testKey", "testValue");
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
controller.setMediaUri(
new Uri.Builder()
.scheme(MEDIA_URI_SCHEME)
.authority(MEDIA_URI_AUTHORITY)
.path(MEDIA_URI_PATH_SET_MEDIA_URI)
.appendQueryParameter(MEDIA_URI_QUERY_URI, testMediaUri.toString())
.build(),
testExtras);
controller.play();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPlayFromUriCalled).isTrue();
assertThat(sessionCallback.uri).isEqualTo(testMediaUri);
assertThat(sessionCallback.mediaId).isNull();
assertThat(sessionCallback.query).isNull();
assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0);
}
@Test
public void setMediaUri_withoutFormattingFollowedByPlay_callsPlayFromUri() throws Exception {
Uri testMediaUri = Uri.parse("androidx://jetpack/test?query=android%20media");
Bundle testExtras = new Bundle();
testExtras.putString("testKey", "testValue");
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(1);
controller.setMediaUri(testMediaUri, testExtras);
controller.play();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPlayFromUriCalled).isTrue();
assertThat(sessionCallback.uri).isEqualTo(testMediaUri);
assertThat(sessionCallback.mediaId).isNull();
assertThat(sessionCallback.query).isNull();
assertThat(sessionCallback.onPlayCalledCount).isEqualTo(0);
}
@Test
public void setMediaUri_followedByPrepareTwice_callsPrepareFromUriAndPrepare() throws Exception {
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(2);
controller.setMediaUri(Uri.parse("androidx://test"), /* extras= */ Bundle.EMPTY);
controller.prepare();
controller.prepare();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPrepareFromUriCalled).isTrue();
assertThat(sessionCallback.onPrepareCalled).isTrue();
}
@Test
public void setMediaUri_followedByPlayTwice_callsPlayFromUriAndPlay() throws Exception {
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(2);
controller.setMediaUri(Uri.parse("androidx://test"), /* extras= */ Bundle.EMPTY);
controller.play();
controller.play();
assertThat(sessionCallback.await(TIMEOUT_MS)).isTrue();
assertThat(sessionCallback.onPlayFromUriCalled).isTrue();
assertThat(sessionCallback.onPlayCalledCount).isEqualTo(1);
}
@Test
public void setMediaUri_multipleCalls_skipped() throws Exception {
RemoteMediaController controller = createControllerAndWaitConnection();
sessionCallback.reset(2);
Uri testUri1 = Uri.parse("androidx://test1");
Uri testUri2 = Uri.parse("androidx://test2");
controller.setMediaUri(testUri1, /* extras= */ Bundle.EMPTY);
controller.setMediaUri(testUri2, /* extras= */ Bundle.EMPTY);
controller.prepare();
assertThat(sessionCallback.await(TIMEOUT_MS)).isFalse();
assertThat(sessionCallback.onPrepareFromUriCalled).isTrue();
assertThat(sessionCallback.uri).isEqualTo(testUri2);
}
@Test
public void seekToNext_callsOnSkipToNext() throws Exception {
RemoteMediaController controller = createControllerAndWaitConnection();