Add MediaSession.Callback.onPlayerInteractionFinished

This callback is useful for advanced use cases that care about
which Player calls are batched together. It can be implemented
by triggering this new callback every time a batch of Player
interactions from a controller finished executing.

PiperOrigin-RevId: 642189491
This commit is contained in:
tonihei 2024-06-11 02:12:13 -07:00 committed by Copybara-Service
parent 32d7516237
commit acb6a89ff6
10 changed files with 487 additions and 46 deletions

View File

@ -18,6 +18,9 @@
* Muxers: * Muxers:
* IMA extension: * IMA extension:
* Session: * Session:
* Add `MediaSession.Callback.onPlayerInteractionFinished` to inform
sessions when a series of player interactions from a specific controller
finished.
* UI: * UI:
* Add customisation of various icons in `PlayerControlView` through xml * Add customisation of various icons in `PlayerControlView` through xml
attributes to allow different drawables per `PlayerView` instance, attributes to allow different drawables per `PlayerView` instance,

View File

@ -24,6 +24,7 @@ import androidx.collection.ArrayMap;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.ControllerInfo;
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.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
@ -233,10 +234,13 @@ import org.checkerframework.checker.nullness.qual.NonNull;
} }
} }
public void addToCommandQueue(ControllerInfo controllerInfo, AsyncCommand asyncCommand) { public void addToCommandQueue(
ControllerInfo controllerInfo, @Player.Command int command, AsyncCommand asyncCommand) {
synchronized (lock) { synchronized (lock) {
@Nullable ConnectedControllerRecord<T> info = controllerRecords.get(controllerInfo); @Nullable ConnectedControllerRecord<T> info = controllerRecords.get(controllerInfo);
if (info != null) { if (info != null) {
info.commandQueuePlayerCommands =
info.commandQueuePlayerCommands.buildUpon().add(command).build();
info.commandQueue.add(asyncCommand); info.commandQueue.add(asyncCommand);
} }
} }
@ -245,7 +249,21 @@ import org.checkerframework.checker.nullness.qual.NonNull;
public void flushCommandQueue(ControllerInfo controllerInfo) { public void flushCommandQueue(ControllerInfo controllerInfo) {
synchronized (lock) { synchronized (lock) {
@Nullable ConnectedControllerRecord<T> info = controllerRecords.get(controllerInfo); @Nullable ConnectedControllerRecord<T> info = controllerRecords.get(controllerInfo);
if (info == null || info.commandQueueIsFlushing || info.commandQueue.isEmpty()) { if (info == null) {
return;
}
Player.Commands commandQueuePlayerCommands = info.commandQueuePlayerCommands;
info.commandQueuePlayerCommands = Player.Commands.EMPTY;
info.commandQueue.add(
() -> {
@Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
if (sessionImpl != null) {
sessionImpl.onPlayerInteractionFinishedOnHandler(
controllerInfo, commandQueuePlayerCommands);
}
return Futures.immediateVoidFuture();
});
if (info.commandQueueIsFlushing) {
return; return;
} }
info.commandQueueIsFlushing = true; info.commandQueueIsFlushing = true;
@ -299,6 +317,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
public SessionCommands sessionCommands; public SessionCommands sessionCommands;
public Player.Commands playerCommands; public Player.Commands playerCommands;
public boolean commandQueueIsFlushing; public boolean commandQueueIsFlushing;
public Player.Commands commandQueuePlayerCommands;
public ConnectedControllerRecord( public ConnectedControllerRecord(
T controllerKey, T controllerKey,
@ -310,6 +329,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
this.sessionCommands = sessionCommands; this.sessionCommands = sessionCommands;
this.playerCommands = playerCommands; this.playerCommands = playerCommands;
this.commandQueue = new ArrayDeque<>(); this.commandQueue = new ArrayDeque<>();
this.commandQueuePlayerCommands = Player.Commands.EMPTY;
} }
} }
} }

View File

@ -314,7 +314,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
} }
private void dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture(RemoteSessionTask task) { private void dispatchRemoteSessionTaskWithPlayerCommandAndWaitForFuture(RemoteSessionTask task) {
// Do not send a flush command queue message as we are actively waiting for task. flushCommandQueueHandler.sendFlushCommandQueueMessage();
ListenableFuture<SessionResult> future = ListenableFuture<SessionResult> future =
dispatchRemoteSessionTask(iSession, task, /* addToPendingMaskingOperations= */ true); dispatchRemoteSessionTask(iSession, task, /* addToPendingMaskingOperations= */ true);
try { try {

View File

@ -1300,7 +1300,7 @@ public class MediaSession {
* callback returns quickly to avoid blocking the main thread for a long period of time. * callback returns quickly to avoid blocking the main thread for a long period of time.
* *
* @param session The session for this event. * @param session The session for this event.
* @param controller The controller information. * @param controller The {@linkplain ControllerInfo controller} information.
* @return The {@link ConnectionResult}. * @return The {@link ConnectionResult}.
*/ */
default ConnectionResult onConnect(MediaSession session, ControllerInfo controller) { default ConnectionResult onConnect(MediaSession session, ControllerInfo controller) {
@ -1316,7 +1316,7 @@ public class MediaSession {
* isn't connected yet in {@link #onConnect}. * isn't connected yet in {@link #onConnect}.
* *
* @param session The session for this event. * @param session The session for this event.
* @param controller The controller information. * @param controller The {@linkplain ControllerInfo controller} information.
*/ */
default void onPostConnect(MediaSession session, ControllerInfo controller) {} default void onPostConnect(MediaSession session, ControllerInfo controller) {}
@ -1328,7 +1328,7 @@ public class MediaSession {
* controller APIs. * controller APIs.
* *
* @param session The session for this event. * @param session The session for this event.
* @param controller The controller information. * @param controller The {@linkplain ControllerInfo controller} information.
*/ */
default void onDisconnected(MediaSession session, ControllerInfo controller) {} default void onDisconnected(MediaSession session, ControllerInfo controller) {}
@ -1356,7 +1356,7 @@ public class MediaSession {
* Futures#immediateFuture(Object)}. * Futures#immediateFuture(Object)}.
* *
* @param session The session for this event. * @param session The session for this event.
* @param controller The controller information. * @param controller The {@linkplain ControllerInfo controller} information.
* @param mediaId The media id. * @param mediaId The media id.
* @param rating The new rating from the controller. * @param rating The new rating from the controller.
* @see SessionCommand#COMMAND_CODE_SESSION_SET_RATING * @see SessionCommand#COMMAND_CODE_SESSION_SET_RATING
@ -1379,7 +1379,7 @@ public class MediaSession {
* Futures#immediateFuture(Object)}. * Futures#immediateFuture(Object)}.
* *
* @param session The session for this event. * @param session The session for this event.
* @param controller The controller information. * @param controller The {@linkplain ControllerInfo controller} information.
* @param rating The new rating from the controller. * @param rating The new rating from the controller.
* @see SessionCommand#COMMAND_CODE_SESSION_SET_RATING * @see SessionCommand#COMMAND_CODE_SESSION_SET_RATING
*/ */
@ -1407,7 +1407,7 @@ public class MediaSession {
* Futures#immediateFuture(Object)}. * Futures#immediateFuture(Object)}.
* *
* @param session The session for this event. * @param session The session for this event.
* @param controller The controller information. * @param controller The {@linkplain ControllerInfo controller} information.
* @param customCommand The custom command. * @param customCommand The custom command.
* @param args A {@link Bundle} for additional arguments. May be empty. * @param args A {@link Bundle} for additional arguments. May be empty.
* @return The result of handling the custom command. * @return The result of handling the custom command.
@ -1469,7 +1469,7 @@ public class MediaSession {
* as appropriate once the {@link MediaItem} has been resolved. * as appropriate once the {@link MediaItem} has been resolved.
* *
* @param mediaSession The session for this event. * @param mediaSession The session for this event.
* @param controller The controller information. * @param controller The {@linkplain ControllerInfo controller} information.
* @param mediaItems The list of requested {@link MediaItem media items}. * @param mediaItems The list of requested {@link MediaItem media items}.
* @return A {@link ListenableFuture} for the list of resolved {@link MediaItem media items} * @return A {@link ListenableFuture} for the list of resolved {@link MediaItem media items}
* that are playable by the underlying {@link Player}. * that are playable by the underlying {@link Player}.
@ -1535,7 +1535,7 @@ public class MediaSession {
* as appropriate once the {@link MediaItem} has been resolved. * as appropriate once the {@link MediaItem} has been resolved.
* *
* @param mediaSession The session for this event. * @param mediaSession The session for this event.
* @param controller The controller information. * @param controller The {@linkplain ControllerInfo controller} information.
* @param mediaItems The list of requested {@linkplain MediaItem media items}. * @param mediaItems The list of requested {@linkplain MediaItem media items}.
* @param startIndex The start index in the {@link MediaItem} list from which to start playing, * @param startIndex The start index in the {@link MediaItem} list from which to start playing,
* or {@link C#INDEX_UNSET C.INDEX_UNSET} to start playing from the default index in the * or {@link C#INDEX_UNSET C.INDEX_UNSET} to start playing from the default index in the
@ -1578,8 +1578,9 @@ public class MediaSession {
* {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} available. * {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} available.
* *
* @param mediaSession The media session for which playback resumption is requested. * @param mediaSession The media session for which playback resumption is requested.
* @param controller The controller that requests the playback resumption. This may be a short * @param controller The {@linkplain ControllerInfo controller} that requests the playback
* living controller created only for issuing a play command for resuming playback. * resumption. This may be a short living controller created only for issuing a play command
* for resuming playback.
* @return The {@linkplain MediaItemsWithStartPosition playlist} to resume playback with. * @return The {@linkplain MediaItemsWithStartPosition playlist} to resume playback with.
*/ */
@UnstableApi @UnstableApi
@ -1604,7 +1605,8 @@ public class MediaSession {
* to your session. * to your session.
* *
* @param session The session that received the media button event. * @param session The session that received the media button event.
* @param controllerInfo The controller to which the media button event is attributed to. * @param controllerInfo The {@linkplain ControllerInfo controller} to which the media button
* event is attributed to.
* @param intent The media button intent. * @param intent The media button intent.
* @return True if the event was handled, false otherwise. * @return True if the event was handled, false otherwise.
*/ */
@ -1613,6 +1615,27 @@ public class MediaSession {
MediaSession session, ControllerInfo controllerInfo, Intent intent) { MediaSession session, ControllerInfo controllerInfo, Intent intent) {
return false; return false;
} }
/**
* Called after all concurrent interactions with {@linkplain MediaSession#getPlayer() the
* session player} from a controller have finished.
*
* <p>A controller may call multiple {@link Player} methods within a single {@link Looper}
* message. Those {@link Player} method calls are batched together and once finished, this
* callback is called to signal that no further {@link Player} interactions coming from this
* controller are expected for now.
*
* <p>Apps can use this callback if they need to trigger different logic depending on whether
* certain methods are called together, for example just {@link Player#setMediaItems}, or {@link
* Player#setMediaItems} and {@link Player#play} together.
*
* @param session The {@link MediaSession} that received the {@link Player} calls.
* @param controllerInfo The {@linkplain ControllerInfo controller} sending the calls.
* @param playerCommands The set of {@link Player.Commands} used to send these calls.
*/
@UnstableApi
default void onPlayerInteractionFinished(
MediaSession session, ControllerInfo controllerInfo, Player.Commands playerCommands) {}
} }
/** Representation of a list of {@linkplain MediaItem media items} and where to start playing. */ /** Representation of a list of {@linkplain MediaItem media items} and where to start playing. */

View File

@ -748,6 +748,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
"Callback.onSetMediaItems must return a non-null future"); "Callback.onSetMediaItems must return a non-null future");
} }
protected void onPlayerInteractionFinishedOnHandler(
ControllerInfo controller, Player.Commands playerCommands) {
callback.onPlayerInteractionFinished(
instance, resolveControllerInfoForCallback(controller), playerCommands);
}
public void connectFromService(IMediaController caller, ControllerInfo controllerInfo) { public void connectFromService(IMediaController caller, ControllerInfo controllerInfo) {
sessionStub.connect(caller, controllerInfo); sessionStub.connect(caller, controllerInfo);
} }
@ -888,7 +894,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* *
* @param controller The controller requesting to play. * @param controller The controller requesting to play.
*/ */
/* package */ void handleMediaControllerPlayRequest(ControllerInfo controller) { /* package */ void handleMediaControllerPlayRequest(
ControllerInfo controller, boolean callOnPlayerInteractionFinished) {
if (!onPlayRequested()) { if (!onPlayRequested()) {
// Request denied, e.g. due to missing foreground service abilities. // Request denied, e.g. due to missing foreground service abilities.
return; return;
@ -899,6 +906,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
boolean canAddMediaItems = boolean canAddMediaItems =
playerWrapper.isCommandAvailable(COMMAND_SET_MEDIA_ITEM) playerWrapper.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)
|| playerWrapper.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS); || playerWrapper.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS);
ControllerInfo controllerForRequest = resolveControllerInfoForCallback(controller);
Player.Commands playCommand =
new Player.Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build();
if (hasCurrentMediaItem || !canAddMediaItems) { if (hasCurrentMediaItem || !canAddMediaItems) {
// No playback resumption needed or possible. // No playback resumption needed or possible.
if (!hasCurrentMediaItem) { if (!hasCurrentMediaItem) {
@ -908,20 +918,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+ " missing available commands"); + " missing available commands");
} }
Util.handlePlayButtonAction(playerWrapper); Util.handlePlayButtonAction(playerWrapper);
if (callOnPlayerInteractionFinished) {
onPlayerInteractionFinishedOnHandler(controllerForRequest, playCommand);
}
} else { } else {
@Nullable @Nullable
ListenableFuture<MediaItemsWithStartPosition> future = ListenableFuture<MediaItemsWithStartPosition> future =
checkNotNull( checkNotNull(
callback.onPlaybackResumption(instance, resolveControllerInfoForCallback(controller)), callback.onPlaybackResumption(instance, controllerForRequest),
"Callback.onPlaybackResumption must return a non-null future"); "Callback.onPlaybackResumption must return a non-null future");
Futures.addCallback( Futures.addCallback(
future, future,
new FutureCallback<MediaItemsWithStartPosition>() { new FutureCallback<MediaItemsWithStartPosition>() {
@Override @Override
public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) { public void onSuccess(MediaItemsWithStartPosition mediaItemsWithStartPosition) {
MediaUtils.setMediaItemsWithStartIndexAndPosition( callWithControllerForCurrentRequestSet(
playerWrapper, mediaItemsWithStartPosition); controllerForRequest,
Util.handlePlayButtonAction(playerWrapper); () -> {
MediaUtils.setMediaItemsWithStartIndexAndPosition(
playerWrapper, mediaItemsWithStartPosition);
Util.handlePlayButtonAction(playerWrapper);
if (callOnPlayerInteractionFinished) {
onPlayerInteractionFinishedOnHandler(controllerForRequest, playCommand);
}
})
.run();
} }
@Override @Override
@ -943,6 +964,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
// Play as requested even if playback resumption fails. // Play as requested even if playback resumption fails.
Util.handlePlayButtonAction(playerWrapper); Util.handlePlayButtonAction(playerWrapper);
if (callOnPlayerInteractionFinished) {
onPlayerInteractionFinishedOnHandler(controllerForRequest, playCommand);
}
} }
}, },
this::postOrRunOnApplicationHandler); this::postOrRunOnApplicationHandler);

View File

@ -341,7 +341,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
controller -> controller ->
Util.handlePlayPauseButtonAction( Util.handlePlayPauseButtonAction(
sessionImpl.getPlayerWrapper(), sessionImpl.shouldPlayIfSuppressed()), sessionImpl.getPlayerWrapper(), sessionImpl.shouldPlayIfSuppressed()),
remoteUserInfo); remoteUserInfo,
/* callOnPlayerInteractionFinished= */ true);
} }
@Override @Override
@ -349,7 +350,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_PREPARE, COMMAND_PREPARE,
controller -> sessionImpl.getPlayerWrapper().prepare(), controller -> sessionImpl.getPlayerWrapper().prepare(),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
@Override @Override
@ -379,8 +381,11 @@ import org.checkerframework.checker.initialization.qual.Initialized;
public void onPlay() { public void onPlay() {
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_PLAY_PAUSE, COMMAND_PLAY_PAUSE,
sessionImpl::handleMediaControllerPlayRequest, controller ->
sessionCompat.getCurrentControllerInfo()); sessionImpl.handleMediaControllerPlayRequest(
controller, /* callOnPlayerInteractionFinished= */ true),
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ false);
} }
@Override @Override
@ -411,7 +416,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_PLAY_PAUSE, COMMAND_PLAY_PAUSE,
controller -> Util.handlePauseButtonAction(sessionImpl.getPlayerWrapper()), controller -> Util.handlePauseButtonAction(sessionImpl.getPlayerWrapper()),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
@Override @Override
@ -419,7 +425,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_STOP, COMMAND_STOP,
controller -> sessionImpl.getPlayerWrapper().stop(), controller -> sessionImpl.getPlayerWrapper().stop(),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
@Override @Override
@ -427,7 +434,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
controller -> sessionImpl.getPlayerWrapper().seekTo(pos), controller -> sessionImpl.getPlayerWrapper().seekTo(pos),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
@Override @Override
@ -436,12 +444,14 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_TO_NEXT, COMMAND_SEEK_TO_NEXT,
controller -> sessionImpl.getPlayerWrapper().seekToNext(), controller -> sessionImpl.getPlayerWrapper().seekToNext(),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} else { } else {
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
controller -> sessionImpl.getPlayerWrapper().seekToNextMediaItem(), controller -> sessionImpl.getPlayerWrapper().seekToNextMediaItem(),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
} }
@ -451,12 +461,14 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_TO_PREVIOUS, COMMAND_SEEK_TO_PREVIOUS,
controller -> sessionImpl.getPlayerWrapper().seekToPrevious(), controller -> sessionImpl.getPlayerWrapper().seekToPrevious(),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} else { } else {
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
controller -> sessionImpl.getPlayerWrapper().seekToPreviousMediaItem(), controller -> sessionImpl.getPlayerWrapper().seekToPreviousMediaItem(),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
} }
@ -468,7 +480,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_SET_SPEED_AND_PITCH, COMMAND_SET_SPEED_AND_PITCH,
controller -> sessionImpl.getPlayerWrapper().setPlaybackSpeed(speed), controller -> sessionImpl.getPlayerWrapper().setPlaybackSpeed(speed),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
@Override @Override
@ -484,7 +497,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
// see: {@link MediaUtils#convertToQueueItem}. // see: {@link MediaUtils#convertToQueueItem}.
playerWrapper.seekToDefaultPosition((int) queueId); playerWrapper.seekToDefaultPosition((int) queueId);
}, },
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
@Override @Override
@ -492,7 +506,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_FORWARD, COMMAND_SEEK_FORWARD,
controller -> sessionImpl.getPlayerWrapper().seekForward(), controller -> sessionImpl.getPlayerWrapper().seekForward(),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
@Override @Override
@ -500,7 +515,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand( dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_BACK, COMMAND_SEEK_BACK,
controller -> sessionImpl.getPlayerWrapper().seekBack(), controller -> sessionImpl.getPlayerWrapper().seekBack(),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
@Override @Override
@ -543,7 +559,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
.getPlayerWrapper() .getPlayerWrapper()
.setRepeatMode( .setRepeatMode(
LegacyConversions.convertToRepeatMode(playbackStateCompatRepeatMode)), LegacyConversions.convertToRepeatMode(playbackStateCompatRepeatMode)),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
@Override @Override
@ -554,7 +571,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
sessionImpl sessionImpl
.getPlayerWrapper() .getPlayerWrapper()
.setShuffleModeEnabled(LegacyConversions.convertToShuffleModeEnabled(shuffleMode)), .setShuffleModeEnabled(LegacyConversions.convertToShuffleModeEnabled(shuffleMode)),
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
@Override @Override
@ -595,7 +613,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
} }
} }
}, },
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} }
public ControllerCb getControllerLegacyCbForBroadcast() { public ControllerCb getControllerLegacyCbForBroadcast() {
@ -611,7 +630,10 @@ import org.checkerframework.checker.initialization.qual.Initialized;
} }
private void dispatchSessionTaskWithPlayerCommand( private void dispatchSessionTaskWithPlayerCommand(
@Player.Command int command, SessionTask task, @Nullable RemoteUserInfo remoteUserInfo) { @Player.Command int command,
SessionTask task,
@Nullable RemoteUserInfo remoteUserInfo,
boolean callOnPlayerInteractionFinished) {
if (sessionImpl.isReleased()) { if (sessionImpl.isReleased()) {
return; return;
} }
@ -673,6 +695,10 @@ import org.checkerframework.checker.initialization.qual.Initialized;
} }
}) })
.run(); .run();
if (callOnPlayerInteractionFinished) {
sessionImpl.onPlayerInteractionFinishedOnHandler(
controller, new Player.Commands.Builder().add(command).build());
}
}); });
} }
@ -829,6 +855,12 @@ import org.checkerframework.checker.initialization.qual.Initialized;
if (play) { if (play) {
player.playIfCommandAvailable(); player.playIfCommandAvailable();
} }
sessionImpl.onPlayerInteractionFinishedOnHandler(
controller,
new Player.Commands.Builder()
.addAll(COMMAND_SET_MEDIA_ITEM, COMMAND_PREPARE)
.addIf(COMMAND_PLAY_PAUSE, play)
.build());
})); }));
} }
@ -839,7 +871,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}, },
MoreExecutors.directExecutor()); MoreExecutors.directExecutor());
}, },
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ false);
} }
private void handleOnAddQueueItem(@Nullable MediaDescriptionCompat description, int index) { private void handleOnAddQueueItem(@Nullable MediaDescriptionCompat description, int index) {
@ -872,6 +905,11 @@ import org.checkerframework.checker.initialization.qual.Initialized;
} else { } else {
sessionImpl.getPlayerWrapper().addMediaItems(index, mediaItems); sessionImpl.getPlayerWrapper().addMediaItems(index, mediaItems);
} }
sessionImpl.onPlayerInteractionFinishedOnHandler(
controller,
new Player.Commands.Builder()
.add(COMMAND_CHANGE_MEDIA_ITEMS)
.build());
})); }));
} }
@ -882,7 +920,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}, },
MoreExecutors.directExecutor()); MoreExecutors.directExecutor());
}, },
sessionCompat.getCurrentControllerInfo()); sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ false);
} }
private static void sendCustomCommandResultWhenReady( private static void sendCustomCommandResultWhenReady(

View File

@ -327,13 +327,18 @@ import java.util.concurrent.ExecutionException;
return; return;
} }
if (command == COMMAND_SET_VIDEO_SURFACE) { if (command == COMMAND_SET_VIDEO_SURFACE) {
// Call surface changes immediately to ensure they are handled within the calling
// methods stack. Also add a placeholder task to the regular command queue for proper
// task tracking (e.g. to send onPlayerInteractionFinished).
sessionImpl sessionImpl
.callWithControllerForCurrentRequestSet( .callWithControllerForCurrentRequestSet(
controller, () -> task.run(sessionImpl, controller, sequenceNumber)) controller, () -> task.run(sessionImpl, controller, sequenceNumber))
.run(); .run();
connectedControllersManager.addToCommandQueue(
controller, command, Futures::immediateVoidFuture);
} else { } else {
connectedControllersManager.addToCommandQueue( connectedControllersManager.addToCommandQueue(
controller, () -> task.run(sessionImpl, controller, sequenceNumber)); controller, command, () -> task.run(sessionImpl, controller, sequenceNumber));
} }
}); });
} finally { } finally {
@ -724,7 +729,8 @@ import java.util.concurrent.ExecutionException;
if (impl == null || impl.isReleased()) { if (impl == null || impl.isReleased()) {
return; return;
} }
impl.handleMediaControllerPlayRequest(controller); impl.handleMediaControllerPlayRequest(
controller, /* callOnPlayerInteractionFinished= */ false);
})); }));
} }

View File

@ -15,6 +15,7 @@
*/ */
package androidx.media3.session; package androidx.media3.session;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.session.MediaTestUtils.createMediaItem; import static androidx.media3.session.MediaTestUtils.createMediaItem;
import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE; import static androidx.media3.session.SessionResult.RESULT_ERROR_INVALID_STATE;
import static androidx.media3.session.SessionResult.RESULT_ERROR_PERMISSION_DENIED; import static androidx.media3.session.SessionResult.RESULT_ERROR_PERMISSION_DENIED;
@ -38,6 +39,7 @@ import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.Rating; import androidx.media3.common.Rating;
import androidx.media3.common.StarRating; import androidx.media3.common.StarRating;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuilder; import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuilder;
import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.ControllerInfo;
@ -1553,6 +1555,119 @@ public class MediaSessionCallbackTest {
.inOrder(); .inOrder();
} }
@Test
public void onPlayerInteractionFinished_withSingleControllerCall_calledWithMatchingCommand()
throws Exception {
AtomicReference<Player.Commands> onPlayerInteractionFinishedCommands = new AtomicReference<>();
ConditionVariable onPlayerInteractionFinishedCalled = new ConditionVariable();
MediaSession.Callback callback =
new MediaSession.Callback() {
@Override
public void onPlayerInteractionFinished(
MediaSession session, ControllerInfo controllerInfo, Player.Commands playerCommands) {
onPlayerInteractionFinishedCommands.set(playerCommands);
onPlayerInteractionFinishedCalled.open();
}
};
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controller.setPlayWhenReady(true);
assertThat(onPlayerInteractionFinishedCalled.block(TIMEOUT_MS)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_PLAY_WHEN_READY)).isTrue();
assertThat(onPlayerInteractionFinishedCommands.get())
.isEqualTo(new Player.Commands.Builder().add(Player.COMMAND_PLAY_PAUSE).build());
}
@Test
public void onPlayerInteractionFinished_withMultipleControllerCalls_calledWithMatchingCommands()
throws Exception {
AtomicReference<Player.Commands> onPlayerInteractionFinishedCommands = new AtomicReference<>();
ConditionVariable onPlayerInteractionFinishedCalled = new ConditionVariable();
MediaSession.Callback callback =
new MediaSession.Callback() {
@Override
public void onPlayerInteractionFinished(
MediaSession session, ControllerInfo controllerInfo, Player.Commands playerCommands) {
onPlayerInteractionFinishedCommands.set(playerCommands);
onPlayerInteractionFinishedCalled.open();
}
};
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controller.setMediaItemsPreparePlayAddItemsSeek(
ImmutableList.of(MediaItem.fromUri("https://uri1")),
ImmutableList.of(MediaItem.fromUri("https://uri2")),
/* seekIndex= */ 1);
assertThat(onPlayerInteractionFinishedCalled.block(TIMEOUT_MS)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION))
.isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SEEK_TO_WITH_MEDIA_ITEM_INDEX))
.isTrue();
assertThat(onPlayerInteractionFinishedCommands.get())
.isEqualTo(
new Player.Commands.Builder()
.addAll(
Player.COMMAND_CHANGE_MEDIA_ITEMS,
Player.COMMAND_PREPARE,
Player.COMMAND_PLAY_PAUSE,
Player.COMMAND_SEEK_TO_MEDIA_ITEM)
.build());
}
@Test
public void onPlayerInteractionFinished_withPlaybackResumption_calledWithMatchingCommands()
throws Exception {
AtomicReference<Player.Commands> onPlayerInteractionFinishedCommands = new AtomicReference<>();
ConditionVariable onPlayerInteractionFinishedCalled = new ConditionVariable();
MediaSession.Callback callback =
new MediaSession.Callback() {
@Override
public ListenableFuture<MediaSession.MediaItemsWithStartPosition> onPlaybackResumption(
MediaSession mediaSession, ControllerInfo controller) {
return Futures.immediateFuture(
new MediaSession.MediaItemsWithStartPosition(
MediaTestUtils.createMediaItems(2),
/* startIndex= */ 1,
/* startPositionMs= */ 123L));
}
@Override
public void onPlayerInteractionFinished(
MediaSession session, ControllerInfo controllerInfo, Player.Commands playerCommands) {
onPlayerInteractionFinishedCommands.set(playerCommands);
onPlayerInteractionFinishedCalled.open();
}
};
MediaSession session =
sessionTestRule.ensureReleaseAfterTest(
new MediaSession.Builder(context, player).setCallback(callback).build());
RemoteMediaController controller =
remoteControllerTestRule.createRemoteController(session.getToken());
controller.play();
assertThat(onPlayerInteractionFinishedCalled.block(TIMEOUT_MS)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX))
.isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isTrue();
assertThat(onPlayerInteractionFinishedCommands.get())
.isEqualTo(new Player.Commands.Builder().add(COMMAND_PLAY_PAUSE).build());
}
private void postToPlayerAndSync(TestHandler.TestRunnable r) { private void postToPlayerAndSync(TestHandler.TestRunnable r) {
try { try {
playerThreadTestRule.getHandler().postAndSync(r); playerThreadTestRule.getHandler().postAndSync(r);

View File

@ -18,6 +18,7 @@ package androidx.media3.session;
import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE; import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_PREPARE; import static androidx.media3.common.Player.COMMAND_PREPARE;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
import static androidx.media3.common.Player.STATE_ENDED; import static androidx.media3.common.Player.STATE_ENDED;
import static androidx.media3.common.Player.STATE_IDLE; import static androidx.media3.common.Player.STATE_IDLE;
import static androidx.media3.common.Player.STATE_READY; import static androidx.media3.common.Player.STATE_READY;
@ -39,6 +40,7 @@ import android.os.Bundle;
import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.RatingCompat; import android.support.v4.media.RatingCompat;
import android.support.v4.media.session.MediaControllerCompat; 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.MediaSessionCompat.QueueItem;
import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.media.session.PlaybackStateCompat;
import android.view.KeyEvent; import android.view.KeyEvent;
@ -51,6 +53,7 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.Rating; import androidx.media3.common.Rating;
import androidx.media3.common.StarRating; import androidx.media3.common.StarRating;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.Log; 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;
@ -255,7 +258,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
public void play_whileIdleWithoutPrepareCommandAvailable_callsJustPlay() throws Exception { public void play_whileIdleWithoutPrepareCommandAvailable_callsJustPlay() throws Exception {
player.playbackState = STATE_IDLE; player.playbackState = STATE_IDLE;
player.commands = player.commands =
new Player.Commands.Builder().addAllCommands().remove(Player.COMMAND_PREPARE).build(); new Player.Commands.Builder().addAllCommands().remove(COMMAND_PREPARE).build();
session = session =
new MediaSession.Builder(context, player) new MediaSession.Builder(context, player)
.setId("play") .setId("play")
@ -776,7 +779,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
player.commands = player.commands =
new Player.Commands.Builder() new Player.Commands.Builder()
.addAllCommands() .addAllCommands()
.removeAll(Player.COMMAND_SET_MEDIA_ITEM, Player.COMMAND_CHANGE_MEDIA_ITEMS) .removeAll(COMMAND_SET_MEDIA_ITEM, Player.COMMAND_CHANGE_MEDIA_ITEMS)
.build(); .build();
session = new MediaSession.Builder(context, player).setId("dispatchMediaButtonEvent").build(); session = new MediaSession.Builder(context, player).setId("dispatchMediaButtonEvent").build();
controller = controller =
@ -1979,6 +1982,212 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse(); assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse();
} }
@Test
public void onPlayerInteractionFinished_withSimpleControllerCall_calledWithMatchingCommand()
throws Exception {
AtomicReference<Player.Commands> onPlayerInteractionFinishedCommands = new AtomicReference<>();
ConditionVariable onPlayerInteractionFinishedCalled = new ConditionVariable();
MediaSession.Callback callback =
new TestSessionCallback() {
@Override
public void onPlayerInteractionFinished(
MediaSession session, ControllerInfo controllerInfo, Player.Commands playerCommands) {
onPlayerInteractionFinishedCommands.set(playerCommands);
onPlayerInteractionFinishedCalled.open();
}
};
session =
new MediaSession.Builder(context, player)
.setId("onPlayerInteractionFinished_simpleCall")
.setCallback(callback)
.build();
controller =
new RemoteMediaControllerCompat(
context,
MediaSessionCompat.Token.fromToken(session.getPlatformToken()),
/* waitForConnection= */ true);
controller.getTransportControls().setPlaybackSpeed(2f);
assertThat(onPlayerInteractionFinishedCalled.block(TIMEOUT_MS)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_PLAYBACK_SPEED)).isTrue();
assertThat(onPlayerInteractionFinishedCommands.get())
.isEqualTo(new Player.Commands.Builder().add(Player.COMMAND_SET_SPEED_AND_PITCH).build());
}
@Test
public void onPlayerInteractionFinished_withPlaybackResumption_calledWithMatchingCommand()
throws Exception {
AtomicReference<Player.Commands> onPlayerInteractionFinishedCommands = new AtomicReference<>();
ConditionVariable onPlayerInteractionFinishedCalled = new ConditionVariable();
MediaSession.Callback callback =
new TestSessionCallback() {
@Override
public ListenableFuture<MediaSession.MediaItemsWithStartPosition> onPlaybackResumption(
MediaSession mediaSession, ControllerInfo controller) {
return Futures.immediateFuture(
new MediaSession.MediaItemsWithStartPosition(
MediaTestUtils.createMediaItems(2),
/* startIndex= */ 1,
/* startPositionMs= */ 123L));
}
@Override
public void onPlayerInteractionFinished(
MediaSession session, ControllerInfo controllerInfo, Player.Commands playerCommands) {
onPlayerInteractionFinishedCommands.set(playerCommands);
onPlayerInteractionFinishedCalled.open();
}
};
session =
new MediaSession.Builder(context, player)
.setId("onPlayerInteractionFinished_playbackResumption")
.setCallback(callback)
.build();
controller =
new RemoteMediaControllerCompat(
context,
MediaSessionCompat.Token.fromToken(session.getPlatformToken()),
/* waitForConnection= */ true);
controller.getTransportControls().play();
assertThat(onPlayerInteractionFinishedCalled.block(TIMEOUT_MS)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_START_INDEX))
.isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isTrue();
assertThat(onPlayerInteractionFinishedCommands.get())
.isEqualTo(new Player.Commands.Builder().add(COMMAND_PLAY_PAUSE).build());
}
@Test
public void onPlayerInteractionFinished_withPlayFromUri_calledWithMatchingCommands()
throws Exception {
AtomicReference<Player.Commands> onPlayerInteractionFinishedCommands = new AtomicReference<>();
ConditionVariable onPlayerInteractionFinishedCalled = new ConditionVariable();
MediaSession.Callback callback =
new TestSessionCallback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
return Futures.immediateFuture(MediaTestUtils.createMediaItems(2));
}
@Override
public void onPlayerInteractionFinished(
MediaSession session, ControllerInfo controllerInfo, Player.Commands playerCommands) {
onPlayerInteractionFinishedCommands.set(playerCommands);
onPlayerInteractionFinishedCalled.open();
}
};
session =
new MediaSession.Builder(context, player)
.setId("onPlayerInteractionFinished_playFromUri")
.setCallback(callback)
.build();
controller =
new RemoteMediaControllerCompat(
context,
MediaSessionCompat.Token.fromToken(session.getPlatformToken()),
/* waitForConnection= */ true);
controller.getTransportControls().playFromUri(Uri.parse("https://uri"), Bundle.EMPTY);
assertThat(onPlayerInteractionFinishedCalled.block(TIMEOUT_MS)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION))
.isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isTrue();
assertThat(onPlayerInteractionFinishedCommands.get())
.isEqualTo(
new Player.Commands.Builder()
.addAll(COMMAND_SET_MEDIA_ITEM, COMMAND_PREPARE, COMMAND_PLAY_PAUSE)
.build());
}
@Test
public void onPlayerInteractionFinished_withPrepareFromUri_calledWithMatchingCommands()
throws Exception {
AtomicReference<Player.Commands> onPlayerInteractionFinishedCommands = new AtomicReference<>();
ConditionVariable onPlayerInteractionFinishedCalled = new ConditionVariable();
MediaSession.Callback callback =
new TestSessionCallback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
return Futures.immediateFuture(MediaTestUtils.createMediaItems(2));
}
@Override
public void onPlayerInteractionFinished(
MediaSession session, ControllerInfo controllerInfo, Player.Commands playerCommands) {
onPlayerInteractionFinishedCommands.set(playerCommands);
onPlayerInteractionFinishedCalled.open();
}
};
session =
new MediaSession.Builder(context, player)
.setId("onPlayerInteractionFinished_prepareFromUri")
.setCallback(callback)
.build();
controller =
new RemoteMediaControllerCompat(
context,
MediaSessionCompat.Token.fromToken(session.getPlatformToken()),
/* waitForConnection= */ true);
controller.getTransportControls().prepareFromUri(Uri.parse("https://uri"), Bundle.EMPTY);
assertThat(onPlayerInteractionFinishedCalled.block(TIMEOUT_MS)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_SET_MEDIA_ITEMS_WITH_RESET_POSITION))
.isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PREPARE)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_PLAY)).isFalse();
assertThat(onPlayerInteractionFinishedCommands.get())
.isEqualTo(
new Player.Commands.Builder().addAll(COMMAND_SET_MEDIA_ITEM, COMMAND_PREPARE).build());
}
@Test
public void onPlayerInteractionFinished_withAddQueueItem_calledWithMatchingCommand()
throws Exception {
AtomicReference<Player.Commands> onPlayerInteractionFinishedCommands = new AtomicReference<>();
ConditionVariable onPlayerInteractionFinishedCalled = new ConditionVariable();
MediaSession.Callback callback =
new TestSessionCallback() {
@Override
public ListenableFuture<List<MediaItem>> onAddMediaItems(
MediaSession mediaSession, ControllerInfo controller, List<MediaItem> mediaItems) {
return Futures.immediateFuture(MediaTestUtils.createMediaItems(2));
}
@Override
public void onPlayerInteractionFinished(
MediaSession session, ControllerInfo controllerInfo, Player.Commands playerCommands) {
onPlayerInteractionFinishedCommands.set(playerCommands);
onPlayerInteractionFinishedCalled.open();
}
};
session =
new MediaSession.Builder(context, player)
.setId("onPlayerInteractionFinished_prepareFromUri")
.setCallback(callback)
.build();
controller =
new RemoteMediaControllerCompat(
context,
MediaSessionCompat.Token.fromToken(session.getPlatformToken()),
/* waitForConnection= */ true);
controller.addQueueItem(new MediaDescriptionCompat.Builder().setMediaId("id").build());
assertThat(onPlayerInteractionFinishedCalled.block(TIMEOUT_MS)).isTrue();
assertThat(player.hasMethodBeenCalled(MockPlayer.METHOD_ADD_MEDIA_ITEMS)).isTrue();
assertThat(onPlayerInteractionFinishedCommands.get())
.isEqualTo(new Player.Commands.Builder().add(Player.COMMAND_CHANGE_MEDIA_ITEMS).build());
}
private static class TestSessionCallback implements MediaSession.Callback { private static class TestSessionCallback implements MediaSession.Callback {
@Override @Override

View File

@ -371,8 +371,10 @@ public class RemoteMediaController {
throws RemoteException { throws RemoteException {
binder.setMediaItemsPreparePlayAddItemsSeek( binder.setMediaItemsPreparePlayAddItemsSeek(
controllerId, controllerId,
BundleCollectionUtil.toBundleList(initialMediaItems, MediaItem::toBundle), BundleCollectionUtil.toBundleList(
BundleCollectionUtil.toBundleList(addedMediaItems, MediaItem::toBundle), initialMediaItems, MediaItem::toBundleIncludeLocalConfiguration),
BundleCollectionUtil.toBundleList(
addedMediaItems, MediaItem::toBundleIncludeLocalConfiguration),
seekIndex); seekIndex);
} }