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:
parent
32d7516237
commit
acb6a89ff6
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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. */
|
||||||
|
@ -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);
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user