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:
* IMA extension:
* Session:
* Add `MediaSession.Callback.onPlayerInteractionFinished` to inform
sessions when a series of player interactions from a specific controller
finished.
* UI:
* Add customisation of various icons in `PlayerControlView` through xml
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.session.MediaSession.ControllerInfo;
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.MoreExecutors;
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) {
@Nullable ConnectedControllerRecord<T> info = controllerRecords.get(controllerInfo);
if (info != null) {
info.commandQueuePlayerCommands =
info.commandQueuePlayerCommands.buildUpon().add(command).build();
info.commandQueue.add(asyncCommand);
}
}
@ -245,7 +249,21 @@ import org.checkerframework.checker.nullness.qual.NonNull;
public void flushCommandQueue(ControllerInfo controllerInfo) {
synchronized (lock) {
@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;
}
info.commandQueueIsFlushing = true;
@ -299,6 +317,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
public SessionCommands sessionCommands;
public Player.Commands playerCommands;
public boolean commandQueueIsFlushing;
public Player.Commands commandQueuePlayerCommands;
public ConnectedControllerRecord(
T controllerKey,
@ -310,6 +329,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
this.sessionCommands = sessionCommands;
this.playerCommands = playerCommands;
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) {
// Do not send a flush command queue message as we are actively waiting for task.
flushCommandQueueHandler.sendFlushCommandQueueMessage();
ListenableFuture<SessionResult> future =
dispatchRemoteSessionTask(iSession, task, /* addToPendingMaskingOperations= */ true);
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.
*
* @param session The session for this event.
* @param controller The controller information.
* @param controller The {@linkplain ControllerInfo controller} information.
* @return The {@link ConnectionResult}.
*/
default ConnectionResult onConnect(MediaSession session, ControllerInfo controller) {
@ -1316,7 +1316,7 @@ public class MediaSession {
* isn't connected yet in {@link #onConnect}.
*
* @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) {}
@ -1328,7 +1328,7 @@ public class MediaSession {
* controller APIs.
*
* @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) {}
@ -1356,7 +1356,7 @@ public class MediaSession {
* Futures#immediateFuture(Object)}.
*
* @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 rating The new rating from the controller.
* @see SessionCommand#COMMAND_CODE_SESSION_SET_RATING
@ -1379,7 +1379,7 @@ public class MediaSession {
* Futures#immediateFuture(Object)}.
*
* @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.
* @see SessionCommand#COMMAND_CODE_SESSION_SET_RATING
*/
@ -1407,7 +1407,7 @@ public class MediaSession {
* Futures#immediateFuture(Object)}.
*
* @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 args A {@link Bundle} for additional arguments. May be empty.
* @return The result of handling the custom command.
@ -1469,7 +1469,7 @@ public class MediaSession {
* as appropriate once the {@link MediaItem} has been resolved.
*
* @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}.
* @return A {@link ListenableFuture} for the list of resolved {@link MediaItem media items}
* that are playable by the underlying {@link Player}.
@ -1535,7 +1535,7 @@ public class MediaSession {
* as appropriate once the {@link MediaItem} has been resolved.
*
* @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 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
@ -1578,8 +1578,9 @@ public class MediaSession {
* {@link Player#COMMAND_CHANGE_MEDIA_ITEMS} available.
*
* @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
* living controller created only for issuing a play command for resuming playback.
* @param controller The {@linkplain ControllerInfo controller} that requests the 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.
*/
@UnstableApi
@ -1604,7 +1605,8 @@ public class MediaSession {
* to your session.
*
* @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.
* @return True if the event was handled, false otherwise.
*/
@ -1613,6 +1615,27 @@ public class MediaSession {
MediaSession session, ControllerInfo controllerInfo, Intent intent) {
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. */

View File

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

View File

@ -341,7 +341,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
controller ->
Util.handlePlayPauseButtonAction(
sessionImpl.getPlayerWrapper(), sessionImpl.shouldPlayIfSuppressed()),
remoteUserInfo);
remoteUserInfo,
/* callOnPlayerInteractionFinished= */ true);
}
@Override
@ -349,7 +350,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand(
COMMAND_PREPARE,
controller -> sessionImpl.getPlayerWrapper().prepare(),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
@Override
@ -379,8 +381,11 @@ import org.checkerframework.checker.initialization.qual.Initialized;
public void onPlay() {
dispatchSessionTaskWithPlayerCommand(
COMMAND_PLAY_PAUSE,
sessionImpl::handleMediaControllerPlayRequest,
sessionCompat.getCurrentControllerInfo());
controller ->
sessionImpl.handleMediaControllerPlayRequest(
controller, /* callOnPlayerInteractionFinished= */ true),
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ false);
}
@Override
@ -411,7 +416,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand(
COMMAND_PLAY_PAUSE,
controller -> Util.handlePauseButtonAction(sessionImpl.getPlayerWrapper()),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
@Override
@ -419,7 +425,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand(
COMMAND_STOP,
controller -> sessionImpl.getPlayerWrapper().stop(),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
@Override
@ -427,7 +434,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
controller -> sessionImpl.getPlayerWrapper().seekTo(pos),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
@Override
@ -436,12 +444,14 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_TO_NEXT,
controller -> sessionImpl.getPlayerWrapper().seekToNext(),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} else {
dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
controller -> sessionImpl.getPlayerWrapper().seekToNextMediaItem(),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
}
@ -451,12 +461,14 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_TO_PREVIOUS,
controller -> sessionImpl.getPlayerWrapper().seekToPrevious(),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
} else {
dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
controller -> sessionImpl.getPlayerWrapper().seekToPreviousMediaItem(),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
}
@ -468,7 +480,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand(
COMMAND_SET_SPEED_AND_PITCH,
controller -> sessionImpl.getPlayerWrapper().setPlaybackSpeed(speed),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
@Override
@ -484,7 +497,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
// see: {@link MediaUtils#convertToQueueItem}.
playerWrapper.seekToDefaultPosition((int) queueId);
},
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
@Override
@ -492,7 +506,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_FORWARD,
controller -> sessionImpl.getPlayerWrapper().seekForward(),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
@Override
@ -500,7 +515,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
dispatchSessionTaskWithPlayerCommand(
COMMAND_SEEK_BACK,
controller -> sessionImpl.getPlayerWrapper().seekBack(),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
@Override
@ -543,7 +559,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
.getPlayerWrapper()
.setRepeatMode(
LegacyConversions.convertToRepeatMode(playbackStateCompatRepeatMode)),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
@Override
@ -554,7 +571,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
sessionImpl
.getPlayerWrapper()
.setShuffleModeEnabled(LegacyConversions.convertToShuffleModeEnabled(shuffleMode)),
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
@Override
@ -595,7 +613,8 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
}
},
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ true);
}
public ControllerCb getControllerLegacyCbForBroadcast() {
@ -611,7 +630,10 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
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()) {
return;
}
@ -673,6 +695,10 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
})
.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) {
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());
},
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ false);
}
private void handleOnAddQueueItem(@Nullable MediaDescriptionCompat description, int index) {
@ -872,6 +905,11 @@ import org.checkerframework.checker.initialization.qual.Initialized;
} else {
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());
},
sessionCompat.getCurrentControllerInfo());
sessionCompat.getCurrentControllerInfo(),
/* callOnPlayerInteractionFinished= */ false);
}
private static void sendCustomCommandResultWhenReady(

View File

@ -327,13 +327,18 @@ import java.util.concurrent.ExecutionException;
return;
}
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
.callWithControllerForCurrentRequestSet(
controller, () -> task.run(sessionImpl, controller, sequenceNumber))
.run();
connectedControllersManager.addToCommandQueue(
controller, command, Futures::immediateVoidFuture);
} else {
connectedControllersManager.addToCommandQueue(
controller, () -> task.run(sessionImpl, controller, sequenceNumber));
controller, command, () -> task.run(sessionImpl, controller, sequenceNumber));
}
});
} finally {
@ -724,7 +729,8 @@ import java.util.concurrent.ExecutionException;
if (impl == null || impl.isReleased()) {
return;
}
impl.handleMediaControllerPlayRequest(controller);
impl.handleMediaControllerPlayRequest(
controller, /* callOnPlayerInteractionFinished= */ false);
}));
}

View File

@ -15,6 +15,7 @@
*/
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.SessionResult.RESULT_ERROR_INVALID_STATE;
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.Rating;
import androidx.media3.common.StarRating;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuilder;
import androidx.media3.session.MediaSession.ControllerInfo;
@ -1553,6 +1555,119 @@ public class MediaSessionCallbackTest {
.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) {
try {
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.media3.common.Player.COMMAND_PLAY_PAUSE;
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_IDLE;
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.RatingCompat;
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.view.KeyEvent;
@ -51,6 +53,7 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.Rating;
import androidx.media3.common.StarRating;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.session.MediaSession.ControllerInfo;
@ -255,7 +258,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
public void play_whileIdleWithoutPrepareCommandAvailable_callsJustPlay() throws Exception {
player.playbackState = STATE_IDLE;
player.commands =
new Player.Commands.Builder().addAllCommands().remove(Player.COMMAND_PREPARE).build();
new Player.Commands.Builder().addAllCommands().remove(COMMAND_PREPARE).build();
session =
new MediaSession.Builder(context, player)
.setId("play")
@ -776,7 +779,7 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
player.commands =
new Player.Commands.Builder()
.addAllCommands()
.removeAll(Player.COMMAND_SET_MEDIA_ITEM, Player.COMMAND_CHANGE_MEDIA_ITEMS)
.removeAll(COMMAND_SET_MEDIA_ITEM, Player.COMMAND_CHANGE_MEDIA_ITEMS)
.build();
session = new MediaSession.Builder(context, player).setId("dispatchMediaButtonEvent").build();
controller =
@ -1979,6 +1982,212 @@ public class MediaSessionCallbackWithMediaControllerCompatTest {
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 {
@Override

View File

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