mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Use proxy controller to maintain platform session and notification
With this change, the notification controller that is connected by `MediaNotificationManager`, is used as a proxy controller of the System UI controller. An app can use the proxy at connection time and during the lifetime of the session for configuration of the platform session and the media notification on all API levels. This includes using custom layout and available player and session commands of the proxy to maintain the platform session (actions, custom actions, session extras) and the `MediaNotification.Provider`. The legacy System UI controller is hidden from the public API, instead the app interacts with the Media3 proxy: - System UI is hidden from `MediaSession.getConnectedControllers()`. - Calls from System UI to methods of `MediaSession.Callback`/ `MediaLibrarySession.Callback` are mapped to the `ControllerInfo` of the proxy controller. - When `getControllerForCurrentRequest()` is called during an operation of System UI the proxy `ControllerInfo` is returned. PiperOrigin-RevId: 567606117
This commit is contained in:
parent
4f4335943c
commit
742410d517
@ -67,6 +67,9 @@
|
||||
`android.media.session.MediaSession.setMediaButtonBroadcastReceiver()`
|
||||
above API 31 to avoid problems with deprecated API on Samsung devices
|
||||
([#167](https://github.com/androidx/media/issues/167)).
|
||||
* Use the media notification controller as proxy to set available commands
|
||||
and custom layout used to populate the notification and the platform
|
||||
session.
|
||||
* UI:
|
||||
* Downloads:
|
||||
* OkHttp Extension:
|
||||
|
@ -33,7 +33,6 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DataSourceBitmapLoader
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.*
|
||||
import androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED
|
||||
import androidx.media3.session.MediaSession.ConnectionResult
|
||||
import androidx.media3.session.MediaSession.ControllerInfo
|
||||
import com.google.common.collect.ImmutableList
|
||||
@ -45,7 +44,7 @@ class PlaybackService : MediaLibraryService() {
|
||||
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private lateinit var customCommands: List<CommandButton>
|
||||
private lateinit var customLayoutCommandButtons: List<CommandButton>
|
||||
|
||||
companion object {
|
||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
|
||||
@ -60,7 +59,7 @@ class PlaybackService : MediaLibraryService() {
|
||||
@OptIn(UnstableApi::class) // MediaSessionService.setListener
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
customCommands =
|
||||
customLayoutCommandButtons =
|
||||
listOf(
|
||||
getShuffleCommandButton(
|
||||
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY)
|
||||
@ -100,33 +99,47 @@ class PlaybackService : MediaLibraryService() {
|
||||
// ConnectionResult.AcceptedResultBuilder
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onConnect(session: MediaSession, controller: ControllerInfo): ConnectionResult {
|
||||
if (session.isMediaNotificationController(controller)) {
|
||||
// Set the required available session commands and the custom layout for the notification
|
||||
// on all API levels.
|
||||
val availableSessionCommands =
|
||||
ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
|
||||
for (commandButton in customCommands) {
|
||||
// Add custom command to available session commands.
|
||||
// Add the session commands of all command buttons.
|
||||
customLayoutCommandButtons.forEach { commandButton ->
|
||||
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
||||
}
|
||||
// Select the buttons to display.
|
||||
val customLayout =
|
||||
ImmutableList.of(customLayoutCommandButtons[if (player.shuffleModeEnabled) 1 else 0])
|
||||
return ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(availableSessionCommands.build())
|
||||
.setCustomLayout(customLayout)
|
||||
.build()
|
||||
}
|
||||
// Default commands without custom layout for common controllers.
|
||||
return ConnectionResult.AcceptedResultBuilder(session).build()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) // MediaSession.isMediaNotificationController
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
if (!session.isMediaNotificationController(controller)) {
|
||||
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED))
|
||||
}
|
||||
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
|
||||
// Enable shuffling.
|
||||
player.shuffleModeEnabled = true
|
||||
// Change the custom layout to contain the `Disable shuffling` command.
|
||||
session.setCustomLayout(ImmutableList.of(customCommands[1]))
|
||||
session.setCustomLayout(controller, ImmutableList.of(customLayoutCommandButtons[1]))
|
||||
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
|
||||
// Disable shuffling.
|
||||
player.shuffleModeEnabled = false
|
||||
// Change the custom layout to contain the `Enable shuffling` command.
|
||||
session.setCustomLayout(ImmutableList.of(customCommands[0]))
|
||||
session.setCustomLayout(controller, ImmutableList.of(customLayoutCommandButtons[0]))
|
||||
}
|
||||
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
@ -136,12 +149,6 @@ class PlaybackService : MediaLibraryService() {
|
||||
browser: ControllerInfo,
|
||||
params: LibraryParams?
|
||||
): ListenableFuture<LibraryResult<MediaItem>> {
|
||||
if (params != null && params.isRecent) {
|
||||
// The service currently does not support playback resumption. Tell System UI by returning
|
||||
// an error of type 'RESULT_ERROR_NOT_SUPPORTED' for a `params.isRecent` request. See
|
||||
// https://github.com/androidx/media/issues/355
|
||||
return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED))
|
||||
}
|
||||
return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params))
|
||||
}
|
||||
|
||||
@ -234,7 +241,6 @@ class PlaybackService : MediaLibraryService() {
|
||||
mediaLibrarySession =
|
||||
MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||
.setSessionActivity(getSingleTopActivity())
|
||||
.setCustomLayout(ImmutableList.of(customCommands[0]))
|
||||
.setBitmapLoader(CacheBitmapLoader(DataSourceBitmapLoader(/* context= */ this)))
|
||||
.build()
|
||||
}
|
||||
@ -270,10 +276,6 @@ class PlaybackService : MediaLibraryService() {
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
|
||||
/* Do nothing. */
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) // MediaSessionService.Listener
|
||||
private inner class MediaSessionServiceListener : Listener {
|
||||
|
||||
|
@ -215,6 +215,7 @@ public final class CommandButton implements Bundleable {
|
||||
sessionCommand, playerCommand, iconResId, displayName, new Bundle(extras), isEnabled);
|
||||
}
|
||||
|
||||
/** Checks the given command button for equality while ignoring {@link #extras}. */
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj) {
|
||||
|
@ -128,6 +128,13 @@ import java.util.concurrent.Future;
|
||||
|
||||
public void notifyChildrenChanged(
|
||||
ControllerInfo browser, String parentId, int itemCount, @Nullable LibraryParams params) {
|
||||
if (isMediaNotificationControllerConnected() && isMediaNotificationController(browser)) {
|
||||
ControllerInfo systemUiBrowser = getSystemUiControllerInfo();
|
||||
if (systemUiBrowser == null) {
|
||||
return;
|
||||
}
|
||||
browser = systemUiBrowser;
|
||||
}
|
||||
dispatchRemoteControllerTaskWithoutReturn(
|
||||
browser,
|
||||
(callback, seq) -> {
|
||||
@ -140,6 +147,13 @@ import java.util.concurrent.Future;
|
||||
|
||||
public void notifySearchResultChanged(
|
||||
ControllerInfo browser, String query, int itemCount, @Nullable LibraryParams params) {
|
||||
if (isMediaNotificationControllerConnected() && isMediaNotificationController(browser)) {
|
||||
ControllerInfo systemUiBrowser = getSystemUiControllerInfo();
|
||||
if (systemUiBrowser == null) {
|
||||
return;
|
||||
}
|
||||
browser = systemUiBrowser;
|
||||
}
|
||||
dispatchRemoteControllerTaskWithoutReturn(
|
||||
browser, (callback, seq) -> callback.onSearchResultChanged(seq, query, itemCount, params));
|
||||
}
|
||||
@ -163,7 +177,7 @@ import java.util.concurrent.Future;
|
||||
params));
|
||||
}
|
||||
ListenableFuture<LibraryResult<MediaItem>> future =
|
||||
callback.onGetLibraryRoot(instance, browser, params);
|
||||
callback.onGetLibraryRoot(instance, resolveControllerInfoForCallback(browser), params);
|
||||
future.addListener(
|
||||
() -> {
|
||||
@Nullable LibraryResult<MediaItem> result = tryGetFutureResult(future);
|
||||
@ -204,7 +218,8 @@ import java.util.concurrent.Future;
|
||||
params));
|
||||
}
|
||||
ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> future =
|
||||
callback.onGetChildren(instance, browser, parentId, page, pageSize, params);
|
||||
callback.onGetChildren(
|
||||
instance, resolveControllerInfoForCallback(browser), parentId, page, pageSize, params);
|
||||
future.addListener(
|
||||
() -> {
|
||||
@Nullable LibraryResult<ImmutableList<MediaItem>> result = tryGetFutureResult(future);
|
||||
@ -220,7 +235,7 @@ import java.util.concurrent.Future;
|
||||
public ListenableFuture<LibraryResult<MediaItem>> onGetItemOnHandler(
|
||||
ControllerInfo browser, String mediaId) {
|
||||
ListenableFuture<LibraryResult<MediaItem>> future =
|
||||
callback.onGetItem(instance, browser, mediaId);
|
||||
callback.onGetItem(instance, resolveControllerInfoForCallback(browser), mediaId);
|
||||
future.addListener(
|
||||
() -> {
|
||||
@Nullable LibraryResult<MediaItem> result = tryGetFutureResult(future);
|
||||
@ -251,7 +266,8 @@ import java.util.concurrent.Future;
|
||||
// so we explicitly null-check the result to fail early if an app accidentally returns null.
|
||||
ListenableFuture<LibraryResult<Void>> future =
|
||||
checkNotNull(
|
||||
callback.onSubscribe(instance, browser, parentId, params),
|
||||
callback.onSubscribe(
|
||||
instance, resolveControllerInfoForCallback(browser), parentId, params),
|
||||
"onSubscribe must return non-null future");
|
||||
|
||||
// When error happens, remove from the subscription list.
|
||||
@ -270,7 +286,7 @@ import java.util.concurrent.Future;
|
||||
public ListenableFuture<LibraryResult<Void>> onUnsubscribeOnHandler(
|
||||
ControllerInfo browser, String parentId) {
|
||||
ListenableFuture<LibraryResult<Void>> future =
|
||||
callback.onUnsubscribe(instance, browser, parentId);
|
||||
callback.onUnsubscribe(instance, resolveControllerInfoForCallback(browser), parentId);
|
||||
|
||||
future.addListener(
|
||||
() -> removeSubscription(checkStateNotNull(browser.getControllerCb()), parentId),
|
||||
@ -282,7 +298,7 @@ import java.util.concurrent.Future;
|
||||
public ListenableFuture<LibraryResult<Void>> onSearchOnHandler(
|
||||
ControllerInfo browser, String query, @Nullable LibraryParams params) {
|
||||
ListenableFuture<LibraryResult<Void>> future =
|
||||
callback.onSearch(instance, browser, query, params);
|
||||
callback.onSearch(instance, resolveControllerInfoForCallback(browser), query, params);
|
||||
future.addListener(
|
||||
() -> {
|
||||
@Nullable LibraryResult<Void> result = tryGetFutureResult(future);
|
||||
@ -301,7 +317,8 @@ import java.util.concurrent.Future;
|
||||
int pageSize,
|
||||
@Nullable LibraryParams params) {
|
||||
ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> future =
|
||||
callback.onGetSearchResult(instance, browser, query, page, pageSize, params);
|
||||
callback.onGetSearchResult(
|
||||
instance, resolveControllerInfoForCallback(browser), query, page, pageSize, params);
|
||||
future.addListener(
|
||||
() -> {
|
||||
@Nullable LibraryResult<ImmutableList<MediaItem>> result = tryGetFutureResult(future);
|
||||
@ -410,6 +427,10 @@ import java.util.concurrent.Future;
|
||||
ControllerInfo controller, @Nullable LibraryParams params) {
|
||||
SettableFuture<LibraryResult<ImmutableList<MediaItem>>> settableFuture =
|
||||
SettableFuture.create();
|
||||
controller =
|
||||
isMediaNotificationControllerConnected()
|
||||
? checkNotNull(getMediaNotificationControllerInfo())
|
||||
: controller;
|
||||
ListenableFuture<MediaSession.MediaItemsWithStartPosition> future =
|
||||
callback.onPlaybackResumption(instance, controller);
|
||||
Futures.addCallback(
|
||||
|
@ -32,7 +32,6 @@ import android.os.Looper;
|
||||
import android.os.RemoteException;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
import android.view.KeyEvent;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.Nullable;
|
||||
@ -775,9 +774,7 @@ public class MediaSession {
|
||||
/**
|
||||
* Returns whether the given media controller info belongs to the media notification controller.
|
||||
*
|
||||
* <p>Use this method for instance in {@link Callback#onConnect(MediaSession, ControllerInfo)} to
|
||||
* recognize the media notification controller and provide a {@link ConnectionResult} with a
|
||||
* custom layout specific for this controller.
|
||||
* <p>See {@link #getMediaNotificationControllerInfo()}.
|
||||
*
|
||||
* @param controllerInfo The controller info.
|
||||
* @return Whether the controller info belongs to the media notification controller.
|
||||
@ -792,8 +789,28 @@ public class MediaSession {
|
||||
*
|
||||
* <p>Use this controller info to set {@linkplain #setAvailableCommands(ControllerInfo,
|
||||
* SessionCommands, Player.Commands) available commands} and {@linkplain
|
||||
* #setCustomLayout(ControllerInfo, List) custom layout} that are applied to the media
|
||||
* notification.
|
||||
* #setCustomLayout(ControllerInfo, List) custom layout} that are consistently applied to the
|
||||
* media notification on all API levels.
|
||||
*
|
||||
* <p>Available {@linkplain SessionCommands session commands} of the media notification controller
|
||||
* are used to enable or disable buttons of the custom layout before it is passed to the
|
||||
* {@linkplain MediaNotification.Provider#createNotification(MediaSession, ImmutableList,
|
||||
* MediaNotification.ActionFactory, MediaNotification.Provider.Callback) notification provider}.
|
||||
* Disabled command buttons are not converted to notification actions when using {@link
|
||||
* DefaultMediaNotificationProvider}. This affects the media notification displayed by System UI
|
||||
* below API 33.
|
||||
*
|
||||
* <p>The available session commands of the media notification controller are used to maintain
|
||||
* custom actions of the platform session (see {@code PlaybackStateCompat.getCustomActions()}).
|
||||
* Command buttons of the custom layout are disabled or enabled according to the available session
|
||||
* commands. Disabled command buttons are not converted to custom actions of the platform session.
|
||||
* This affects the media notification displayed by System UI <a
|
||||
* href="https://developer.android.com/about/versions/13/behavior-changes-13#playback-controls">starting
|
||||
* with API 33</a>.
|
||||
*
|
||||
* <p>The available {@linkplain Player.Commands player commands} are intersected with the actual
|
||||
* available commands of the underlying player to determine the playback actions of the platform
|
||||
* session (see {@code PlaybackStateCompat.getActions()}).
|
||||
*/
|
||||
@UnstableApi
|
||||
@Nullable
|
||||
@ -815,7 +832,8 @@ public class MediaSession {
|
||||
* <p>On the controller side, {@link
|
||||
* MediaController.Listener#onCustomLayoutChanged(MediaController, List)} is only called if the
|
||||
* new custom layout is different to the custom layout the {@link
|
||||
* MediaController#getCustomLayout() controller already has available}.
|
||||
* MediaController#getCustomLayout() controller already has available}. Note that this comparison
|
||||
* uses {@link CommandButton#equals} and therefore ignores {@link CommandButton#extras}.
|
||||
*
|
||||
* <p>It's up to controller's decision how to represent the layout in its own UI.
|
||||
*
|
||||
@ -836,22 +854,17 @@ public class MediaSession {
|
||||
/**
|
||||
* Sets the custom layout that can initially be set when building the session.
|
||||
*
|
||||
* <p>Calling this method broadcasts the custom layout to all connected Media3 controllers and
|
||||
* converts the {@linkplain CommandButton command buttons} to {@linkplain
|
||||
* PlaybackStateCompat.CustomAction custom actions of the playback state} of the platform media
|
||||
* session (see {@code
|
||||
* PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}). The {@link
|
||||
* CommandButton#isEnabled} flag is set according to the available commands of the controller and
|
||||
* overrides a value that has been set by the app. The platform media session won't see any
|
||||
* commands that are disabled.
|
||||
* <p>Calling this method broadcasts the custom layout to all connected Media3 controllers,
|
||||
* including the {@linkplain #getMediaNotificationControllerInfo() media notification controller}.
|
||||
*
|
||||
* <p>On the controller side, {@link
|
||||
* MediaController.Listener#onCustomLayoutChanged(MediaController, List)} is only called if the
|
||||
* new custom layout is different to the custom layout the {@linkplain
|
||||
* MediaController#getCustomLayout() controller already has available}.
|
||||
* <p>On the controller side, the {@linkplain CommandButton#isEnabled enabled} flag is set
|
||||
* according to the available commands of the controller which overrides a value that has been set
|
||||
* by the session.
|
||||
*
|
||||
* <p>When converting, the {@linkplain SessionCommand#customExtras custom extras of the session
|
||||
* command} is used for the extras of the legacy custom action.
|
||||
* <p>{@link MediaController.Listener#onCustomLayoutChanged(MediaController, List)} is only called
|
||||
* if the new custom layout is different to the custom layout the {@linkplain
|
||||
* MediaController#getCustomLayout() controller already has available}. Note that {@link Bundle
|
||||
* extras} are ignored when comparing {@linkplain CommandButton command buttons}.
|
||||
*
|
||||
* <p>Controllers that connect after calling this method will have the new custom layout available
|
||||
* with the initial connection result. A custom layout specific to a controller can be set when
|
||||
|
@ -130,6 +130,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
// Should be only accessed on the application looper
|
||||
private long sessionPositionUpdateDelayMs;
|
||||
private boolean isMediaNotificationControllerConnected;
|
||||
private ImmutableList<CommandButton> customLayout;
|
||||
|
||||
public MediaSessionImpl(
|
||||
@ -191,10 +192,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
sessionLegacyStub =
|
||||
new MediaSessionLegacyStub(/* session= */ thisRef, sessionUri, applicationHandler);
|
||||
|
||||
PlayerWrapper playerWrapper = new PlayerWrapper(player, playIfSuppressed);
|
||||
// For PlayerWrapper, use the same default commands as the proxy controller gets when the app
|
||||
// doesn't overrides the default commands in `onConnect`. When the default is overridden by the
|
||||
// app in `onConnect`, the default set here will be overridden with these values.
|
||||
MediaSession.ConnectionResult connectionResult =
|
||||
new MediaSession.ConnectionResult.AcceptedResultBuilder(instance).build();
|
||||
PlayerWrapper playerWrapper =
|
||||
new PlayerWrapper(
|
||||
player,
|
||||
playIfSuppressed,
|
||||
customLayout,
|
||||
connectionResult.availableSessionCommands,
|
||||
connectionResult.availablePlayerCommands);
|
||||
this.playerWrapper = playerWrapper;
|
||||
this.playerWrapper.setCustomLayout(customLayout);
|
||||
postOrRun(
|
||||
applicationHandler,
|
||||
() ->
|
||||
@ -212,13 +222,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
return;
|
||||
}
|
||||
setPlayerInternal(
|
||||
/* oldPlayerWrapper= */ playerWrapper, new PlayerWrapper(player, playIfSuppressed));
|
||||
/* oldPlayerWrapper= */ playerWrapper,
|
||||
new PlayerWrapper(
|
||||
player,
|
||||
playIfSuppressed,
|
||||
playerWrapper.getCustomLayout(),
|
||||
playerWrapper.getAvailableSessionCommands(),
|
||||
playerWrapper.getAvailablePlayerCommands()));
|
||||
}
|
||||
|
||||
private void setPlayerInternal(
|
||||
@Nullable PlayerWrapper oldPlayerWrapper, PlayerWrapper newPlayerWrapper) {
|
||||
playerWrapper = newPlayerWrapper;
|
||||
playerWrapper.setCustomLayout(customLayout);
|
||||
if (oldPlayerWrapper != null) {
|
||||
oldPlayerWrapper.removeListener(checkStateNotNull(this.playerListener));
|
||||
}
|
||||
@ -295,14 +310,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
public List<ControllerInfo> getConnectedControllers() {
|
||||
List<ControllerInfo> controllers = new ArrayList<>();
|
||||
controllers.addAll(sessionStub.getConnectedControllersManager().getConnectedControllers());
|
||||
if (isMediaNotificationControllerConnected) {
|
||||
ImmutableList<ControllerInfo> legacyControllers =
|
||||
sessionLegacyStub.getConnectedControllersManager().getConnectedControllers();
|
||||
for (int i = 0; i < legacyControllers.size(); i++) {
|
||||
ControllerInfo legacyController = legacyControllers.get(i);
|
||||
if (!isSystemUiController(legacyController)) {
|
||||
controllers.add(legacyController);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
controllers.addAll(
|
||||
sessionLegacyStub.getConnectedControllersManager().getConnectedControllers());
|
||||
}
|
||||
return controllers;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ControllerInfo getControllerForCurrentRequest() {
|
||||
return controllerForCurrentRequest;
|
||||
return controllerForCurrentRequest != null
|
||||
? resolveControllerInfoForCallback(controllerForCurrentRequest)
|
||||
: null;
|
||||
}
|
||||
|
||||
public boolean isConnected(ControllerInfo controller) {
|
||||
@ -372,17 +400,39 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
return null;
|
||||
}
|
||||
|
||||
public ListenableFuture<SessionResult> setCustomLayout(
|
||||
ControllerInfo controller, List<CommandButton> layout) {
|
||||
return dispatchRemoteControllerTask(
|
||||
controller, (controller1, seq) -> controller1.setCustomLayout(seq, layout));
|
||||
/** Returns whether the media notification controller is connected. */
|
||||
protected boolean isMediaNotificationControllerConnected() {
|
||||
return isMediaNotificationControllerConnected;
|
||||
}
|
||||
|
||||
public void setCustomLayout(List<CommandButton> layout) {
|
||||
customLayout = ImmutableList.copyOf(layout);
|
||||
/**
|
||||
* Sets the custom layout for the given {@link MediaController}.
|
||||
*
|
||||
* @param controller The controller.
|
||||
* @param customLayout The custom layout.
|
||||
* @return The session result from the controller.
|
||||
*/
|
||||
public ListenableFuture<SessionResult> setCustomLayout(
|
||||
ControllerInfo controller, ImmutableList<CommandButton> customLayout) {
|
||||
if (isMediaNotificationController(controller)) {
|
||||
playerWrapper.setCustomLayout(customLayout);
|
||||
sessionLegacyStub.updateLegacySessionPlaybackStateCompat();
|
||||
}
|
||||
return dispatchRemoteControllerTask(
|
||||
controller, (controller1, seq) -> controller1.setCustomLayout(seq, customLayout));
|
||||
}
|
||||
|
||||
/** Sets the custom layout of the session and sends the custom layout to all controllers. */
|
||||
public void setCustomLayout(ImmutableList<CommandButton> customLayout) {
|
||||
this.customLayout = customLayout;
|
||||
playerWrapper.setCustomLayout(customLayout);
|
||||
dispatchRemoteControllerTaskWithoutReturn(
|
||||
(controller, seq) -> controller.setCustomLayout(seq, layout));
|
||||
(controller, seq) -> controller.setCustomLayout(seq, customLayout));
|
||||
}
|
||||
|
||||
/** Returns the custom layout. */
|
||||
public ImmutableList<CommandButton> getCustomLayout() {
|
||||
return customLayout;
|
||||
}
|
||||
|
||||
public void setSessionExtras(Bundle sessionExtras) {
|
||||
@ -408,6 +458,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
public void setAvailableCommands(
|
||||
ControllerInfo controller, SessionCommands sessionCommands, Player.Commands playerCommands) {
|
||||
if (sessionStub.getConnectedControllersManager().isConnected(controller)) {
|
||||
if (isMediaNotificationController(controller)) {
|
||||
playerWrapper.setAvailableCommands(sessionCommands, playerCommands);
|
||||
sessionLegacyStub.updateLegacySessionPlaybackStateCompat();
|
||||
ControllerInfo systemUiControllerInfo = getSystemUiControllerInfo();
|
||||
if (systemUiControllerInfo != null) {
|
||||
// Set the available commands of the proxy controller to the ConnectedControllerRecord of
|
||||
// the hidden System UI controller.
|
||||
sessionLegacyStub
|
||||
.getConnectedControllersManager()
|
||||
.updateCommandsFromSession(systemUiControllerInfo, sessionCommands, playerCommands);
|
||||
}
|
||||
}
|
||||
sessionStub
|
||||
.getConnectedControllersManager()
|
||||
.updateCommandsFromSession(controller, sessionCommands, playerCommands);
|
||||
@ -482,45 +544,103 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
}
|
||||
|
||||
public MediaSession.ConnectionResult onConnectOnHandler(ControllerInfo controller) {
|
||||
return checkNotNull(
|
||||
callback.onConnect(instance, controller), "Callback.onConnect must return non-null future");
|
||||
if (isMediaNotificationControllerConnected && isSystemUiController(controller)) {
|
||||
// Hide System UI and provide the connection result from the `PlayerWrapper` state.
|
||||
return new MediaSession.ConnectionResult.AcceptedResultBuilder(instance)
|
||||
.setAvailableSessionCommands(playerWrapper.getAvailableSessionCommands())
|
||||
.setAvailablePlayerCommands(playerWrapper.getAvailablePlayerCommands())
|
||||
.setCustomLayout(playerWrapper.getCustomLayout())
|
||||
.build();
|
||||
}
|
||||
MediaSession.ConnectionResult connectionResult =
|
||||
checkNotNull(
|
||||
callback.onConnect(instance, controller),
|
||||
"Callback.onConnect must return non-null future");
|
||||
if (isMediaNotificationController(controller)) {
|
||||
isMediaNotificationControllerConnected = true;
|
||||
playerWrapper.setAvailableCommands(
|
||||
connectionResult.availableSessionCommands, connectionResult.availablePlayerCommands);
|
||||
playerWrapper.setCustomLayout(
|
||||
connectionResult.customLayout != null
|
||||
? connectionResult.customLayout
|
||||
: instance.getCustomLayout());
|
||||
sessionLegacyStub.updateLegacySessionPlaybackStateCompat();
|
||||
}
|
||||
return connectionResult;
|
||||
}
|
||||
|
||||
public void onPostConnectOnHandler(ControllerInfo controller) {
|
||||
if (isMediaNotificationControllerConnected && isSystemUiController(controller)) {
|
||||
// Hide System UI. Apps can use the media notification controller to maintain the platform
|
||||
// session
|
||||
return;
|
||||
}
|
||||
callback.onPostConnect(instance, controller);
|
||||
}
|
||||
|
||||
public void onDisconnectedOnHandler(ControllerInfo controller) {
|
||||
if (isMediaNotificationControllerConnected) {
|
||||
if (isSystemUiController(controller)) {
|
||||
// Hide System UI controller. Apps can use the media notification controller to maintain the
|
||||
// platform session.
|
||||
return;
|
||||
} else if (isMediaNotificationController(controller)) {
|
||||
isMediaNotificationControllerConnected = false;
|
||||
}
|
||||
}
|
||||
callback.onDisconnected(instance, controller);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Calling deprecated callback method.
|
||||
public @SessionResult.Code int onPlayerCommandRequestOnHandler(
|
||||
ControllerInfo controller, @Player.Command int playerCommand) {
|
||||
return callback.onPlayerCommandRequest(instance, controller, playerCommand);
|
||||
return callback.onPlayerCommandRequest(
|
||||
instance, resolveControllerInfoForCallback(controller), playerCommand);
|
||||
}
|
||||
|
||||
public ListenableFuture<SessionResult> onSetRatingOnHandler(
|
||||
ControllerInfo controller, String mediaId, Rating rating) {
|
||||
return checkNotNull(
|
||||
callback.onSetRating(instance, controller, mediaId, rating),
|
||||
callback.onSetRating(
|
||||
instance, resolveControllerInfoForCallback(controller), mediaId, rating),
|
||||
"Callback.onSetRating must return non-null future");
|
||||
}
|
||||
|
||||
public ListenableFuture<SessionResult> onSetRatingOnHandler(
|
||||
ControllerInfo controller, Rating rating) {
|
||||
return checkNotNull(
|
||||
callback.onSetRating(instance, controller, rating),
|
||||
callback.onSetRating(instance, resolveControllerInfoForCallback(controller), rating),
|
||||
"Callback.onSetRating must return non-null future");
|
||||
}
|
||||
|
||||
public ListenableFuture<SessionResult> onCustomCommandOnHandler(
|
||||
ControllerInfo browser, SessionCommand command, Bundle extras) {
|
||||
ControllerInfo controller, SessionCommand command, Bundle extras) {
|
||||
return checkNotNull(
|
||||
callback.onCustomCommand(instance, browser, command, extras),
|
||||
callback.onCustomCommand(
|
||||
instance, resolveControllerInfoForCallback(controller), command, extras),
|
||||
"Callback.onCustomCommandOnHandler must return non-null future");
|
||||
}
|
||||
|
||||
protected ListenableFuture<List<MediaItem>> onAddMediaItemsOnHandler(
|
||||
ControllerInfo controller, List<MediaItem> mediaItems) {
|
||||
return checkNotNull(
|
||||
callback.onAddMediaItems(
|
||||
instance, resolveControllerInfoForCallback(controller), mediaItems),
|
||||
"Callback.onAddMediaItems must return a non-null future");
|
||||
}
|
||||
|
||||
protected ListenableFuture<MediaItemsWithStartPosition> onSetMediaItemsOnHandler(
|
||||
ControllerInfo controller, List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
|
||||
return checkNotNull(
|
||||
callback.onSetMediaItems(
|
||||
instance,
|
||||
resolveControllerInfoForCallback(controller),
|
||||
mediaItems,
|
||||
startIndex,
|
||||
startPositionMs),
|
||||
"Callback.onSetMediaItems must return a non-null future");
|
||||
}
|
||||
|
||||
public void connectFromService(
|
||||
IMediaController caller,
|
||||
int controllerVersion,
|
||||
@ -555,20 +675,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
return applicationHandler;
|
||||
}
|
||||
|
||||
protected ListenableFuture<List<MediaItem>> onAddMediaItemsOnHandler(
|
||||
ControllerInfo controller, List<MediaItem> mediaItems) {
|
||||
return checkNotNull(
|
||||
callback.onAddMediaItems(instance, controller, mediaItems),
|
||||
"Callback.onAddMediaItems must return a non-null future");
|
||||
}
|
||||
|
||||
protected ListenableFuture<MediaItemsWithStartPosition> onSetMediaItemsOnHandler(
|
||||
ControllerInfo controller, List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
|
||||
return checkNotNull(
|
||||
callback.onSetMediaItems(instance, controller, mediaItems, startIndex, startPositionMs),
|
||||
"Callback.onSetMediaItems must return a non-null future");
|
||||
}
|
||||
|
||||
protected boolean isReleased() {
|
||||
synchronized (lock) {
|
||||
return closed;
|
||||
@ -580,10 +686,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
return sessionActivity;
|
||||
}
|
||||
|
||||
protected ImmutableList<CommandButton> getCustomLayout() {
|
||||
return customLayout;
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
protected void setSessionActivity(PendingIntent sessionActivity) {
|
||||
if (Objects.equals(this.sessionActivity, sessionActivity)) {
|
||||
@ -603,6 +705,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
}
|
||||
}
|
||||
|
||||
protected ControllerInfo resolveControllerInfoForCallback(ControllerInfo controller) {
|
||||
return isMediaNotificationControllerConnected && isSystemUiController(controller)
|
||||
? checkNotNull(getMediaNotificationControllerInfo())
|
||||
: controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the service binder from the MediaBrowserServiceCompat. Should be only called by the thread
|
||||
* with a Looper.
|
||||
@ -693,7 +801,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
@Nullable
|
||||
ListenableFuture<MediaItemsWithStartPosition> future =
|
||||
checkNotNull(
|
||||
callback.onPlaybackResumption(instance, controller),
|
||||
callback.onPlaybackResumption(instance, resolveControllerInfoForCallback(controller)),
|
||||
"Callback.onPlaybackResumption must return a non-null future");
|
||||
// Use a direct executor when an immediate future is returned to execute the player setup in the
|
||||
// caller's looper event on the application thread.
|
||||
|
@ -823,6 +823,15 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
||||
connectionTimeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
public void updateLegacySessionPlaybackStateCompat() {
|
||||
postOrRun(
|
||||
sessionImpl.getApplicationHandler(),
|
||||
() ->
|
||||
sessionImpl
|
||||
.getSessionCompat()
|
||||
.setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat()));
|
||||
}
|
||||
|
||||
private void handleMediaRequest(MediaItem mediaItem, boolean play) {
|
||||
dispatchSessionTaskWithPlayerCommand(
|
||||
COMMAND_SET_MEDIA_ITEM,
|
||||
@ -1081,16 +1090,12 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
||||
|
||||
@Override
|
||||
public void onPlayerError(int seq, @Nullable PlaybackException playerError) {
|
||||
sessionImpl
|
||||
.getSessionCompat()
|
||||
.setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat());
|
||||
updateLegacySessionPlaybackStateCompat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCustomLayout(int seq, List<CommandButton> layout) {
|
||||
sessionImpl
|
||||
.getSessionCompat()
|
||||
.setPlaybackState(sessionImpl.getPlayerWrapper().createPlaybackStateCompat());
|
||||
updateLegacySessionPlaybackStateCompat();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -21,6 +21,7 @@ import static androidx.media3.common.util.Util.msToUs;
|
||||
import static androidx.media3.common.util.Util.postOrRun;
|
||||
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_MEDIA_ID_COMPAT;
|
||||
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_PLAYBACK_SPEED_COMPAT;
|
||||
import static androidx.media3.session.MediaUtils.intersect;
|
||||
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
@ -70,12 +71,43 @@ import java.util.List;
|
||||
@Nullable private String legacyErrorMessage;
|
||||
@Nullable private Bundle legacyErrorExtras;
|
||||
private ImmutableList<CommandButton> customLayout;
|
||||
private SessionCommands availableSessionCommands;
|
||||
private Commands availablePlayerCommands;
|
||||
|
||||
public PlayerWrapper(Player player, boolean playIfSuppressed) {
|
||||
public PlayerWrapper(
|
||||
Player player,
|
||||
boolean playIfSuppressed,
|
||||
ImmutableList<CommandButton> customLayout,
|
||||
SessionCommands availableSessionCommands,
|
||||
Commands availablePlayerCommands) {
|
||||
super(player);
|
||||
this.playIfSuppressed = playIfSuppressed;
|
||||
this.customLayout = customLayout;
|
||||
this.availableSessionCommands = availableSessionCommands;
|
||||
this.availablePlayerCommands = availablePlayerCommands;
|
||||
legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT;
|
||||
customLayout = ImmutableList.of();
|
||||
}
|
||||
|
||||
public void setAvailableCommands(
|
||||
SessionCommands availableSessionCommands, Commands availablePlayerCommands) {
|
||||
this.availableSessionCommands = availableSessionCommands;
|
||||
this.availablePlayerCommands = availablePlayerCommands;
|
||||
}
|
||||
|
||||
public SessionCommands getAvailableSessionCommands() {
|
||||
return availableSessionCommands;
|
||||
}
|
||||
|
||||
public Commands getAvailablePlayerCommands() {
|
||||
return availablePlayerCommands;
|
||||
}
|
||||
|
||||
public void setCustomLayout(ImmutableList<CommandButton> customLayout) {
|
||||
this.customLayout = customLayout;
|
||||
}
|
||||
|
||||
/* package */ ImmutableList<CommandButton> getCustomLayout() {
|
||||
return customLayout;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,11 +136,6 @@ import java.util.List;
|
||||
return legacyStatusCode;
|
||||
}
|
||||
|
||||
/** Sets the custom layout. */
|
||||
public void setCustomLayout(ImmutableList<CommandButton> customLayout) {
|
||||
this.customLayout = customLayout;
|
||||
}
|
||||
|
||||
/** Clears the legacy error status. */
|
||||
public void clearLegacyErrorStatus() {
|
||||
legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT;
|
||||
@ -974,7 +1001,7 @@ import java.util.List;
|
||||
int state = MediaUtils.convertToPlaybackStateCompatState(/* player= */ this, playIfSuppressed);
|
||||
// Always advertise ACTION_SET_RATING.
|
||||
long actions = PlaybackStateCompat.ACTION_SET_RATING;
|
||||
Commands availableCommands = getAvailableCommands();
|
||||
Commands availableCommands = intersect(availablePlayerCommands, getAvailableCommands());
|
||||
for (int i = 0; i < availableCommands.size(); i++) {
|
||||
actions |= convertCommandToPlaybackStateActions(availableCommands.get(i));
|
||||
}
|
||||
@ -1006,7 +1033,9 @@ import java.util.List;
|
||||
CommandButton commandButton = customLayout.get(i);
|
||||
if (commandButton.sessionCommand != null) {
|
||||
SessionCommand sessionCommand = commandButton.sessionCommand;
|
||||
if (sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM) {
|
||||
if (sessionCommand.commandCode == SessionCommand.COMMAND_CODE_CUSTOM
|
||||
&& CommandButton.isEnabled(
|
||||
commandButton, availableSessionCommands, availablePlayerCommands)) {
|
||||
builder.addCustomAction(
|
||||
new PlaybackStateCompat.CustomAction.Builder(
|
||||
sessionCommand.customAction,
|
||||
|
@ -157,6 +157,7 @@ public final class SessionCommand implements Bundleable {
|
||||
customExtras = new Bundle(checkNotNull(extras));
|
||||
}
|
||||
|
||||
/** Checks the given session command for equality while ignoring extras. */
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (!(obj instanceof SessionCommand)) {
|
||||
|
@ -22,6 +22,7 @@ import static org.mockito.Mockito.when;
|
||||
import android.os.Looper;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
@ -42,7 +43,13 @@ public class PlayerWrapperTest {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
playerWrapper = new PlayerWrapper(player, /* playIfSuppressed= */ true);
|
||||
playerWrapper =
|
||||
new PlayerWrapper(
|
||||
player,
|
||||
/* playIfSuppressed= */ true,
|
||||
ImmutableList.of(),
|
||||
SessionCommands.EMPTY,
|
||||
Player.Commands.EMPTY);
|
||||
when(player.isCommandAvailable(anyInt())).thenReturn(true);
|
||||
when(player.getApplicationLooper()).thenReturn(Looper.myLooper());
|
||||
}
|
||||
|
@ -30,7 +30,8 @@ public class MediaSessionConstants {
|
||||
"onTracksChanged_videoToAudioTransition";
|
||||
public static final String TEST_SET_SHOW_PLAY_BUTTON_IF_SUPPRESSED_TO_FALSE =
|
||||
"testSetShowPlayButtonIfSuppressedToFalse";
|
||||
|
||||
public static final String TEST_MEDIA_CONTROLLER_COMPAT_CALLBACK_WITH_MEDIA_SESSION_TEST =
|
||||
"MediaControllerCompatCallbackWithMediaSessionTest";
|
||||
// Bundle keys
|
||||
public static final String KEY_AVAILABLE_SESSION_COMMANDS = "availableSessionCommands";
|
||||
public static final String KEY_CONTROLLER = "controllerKey";
|
||||
|
@ -45,6 +45,7 @@ dependencies {
|
||||
implementation 'androidx.test:core:' + androidxTestCoreVersion
|
||||
implementation project(modulePrefix + 'test-data')
|
||||
androidTestImplementation project(modulePrefix + 'lib-exoplayer')
|
||||
androidTestImplementation project(modulePrefix + 'test-utils')
|
||||
androidTestImplementation 'androidx.test.ext:junit:' + androidxTestJUnitVersion
|
||||
androidTestImplementation 'androidx.test.ext:truth:' + androidxTestTruthVersion
|
||||
androidTestImplementation 'androidx.test:core:' + androidxTestCoreVersion
|
||||
|
@ -20,6 +20,7 @@ import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_MEDIA_ID
|
||||
import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_USER_RATING;
|
||||
import static androidx.media3.common.Player.STATE_ENDED;
|
||||
import static androidx.media3.common.Player.STATE_READY;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_MEDIA_CONTROLLER_COMPAT_CALLBACK_WITH_MEDIA_SESSION_TEST;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_SET_SHOW_PLAY_BUTTON_IF_SUPPRESSED_TO_FALSE;
|
||||
import static androidx.media3.test.session.common.TestUtils.LONG_TIMEOUT_MS;
|
||||
import static androidx.media3.test.session.common.TestUtils.TIMEOUT_MS;
|
||||
@ -78,10 +79,10 @@ import org.junit.runner.RunWith;
|
||||
@LargeTest
|
||||
public class MediaControllerCompatCallbackWithMediaSessionTest {
|
||||
|
||||
private static final String TAG = "MCCCallbackTestWithMS2";
|
||||
private static final float EPSILON = 1e-6f;
|
||||
private static final String SESSION_ID =
|
||||
TEST_MEDIA_CONTROLLER_COMPAT_CALLBACK_WITH_MEDIA_SESSION_TEST;
|
||||
|
||||
@Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(TAG);
|
||||
@Rule public final HandlerThreadTestRule threadTestRule = new HandlerThreadTestRule(SESSION_ID);
|
||||
|
||||
private Context context;
|
||||
private TestHandler handler;
|
||||
@ -92,7 +93,10 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
|
||||
public void setUp() throws Exception {
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
handler = threadTestRule.getHandler();
|
||||
session = new RemoteMediaSession(TAG, context, null);
|
||||
Bundle tokenExtras = new Bundle();
|
||||
tokenExtras.putBoolean(
|
||||
MediaSessionProviderService.KEY_ENABLE_FAKE_MEDIA_NOTIFICATION_MANAGER_CONTROLLER, true);
|
||||
session = new RemoteMediaSession(SESSION_ID, context, tokenExtras);
|
||||
controllerCompat = new MediaControllerCompat(context, session.getCompatToken());
|
||||
}
|
||||
|
||||
@ -181,7 +185,7 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
|
||||
public void getError_withPlayerErrorAfterConnected_returnsError() throws Exception {
|
||||
PlaybackException testPlayerError =
|
||||
new PlaybackException(
|
||||
/* messaage= */ "testremote",
|
||||
/* message= */ "testremote",
|
||||
/* cause= */ null,
|
||||
PlaybackException.ERROR_CODE_REMOTE_ERROR);
|
||||
Bundle playerConfig =
|
||||
@ -207,7 +211,7 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
|
||||
public void playerError_notified() throws Exception {
|
||||
PlaybackException testPlayerError =
|
||||
new PlaybackException(
|
||||
/* messaage= */ "player error",
|
||||
/* message= */ "player error",
|
||||
/* cause= */ null,
|
||||
PlaybackException.ERROR_CODE_UNSPECIFIED);
|
||||
|
||||
@ -937,53 +941,46 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
|
||||
|
||||
@Test
|
||||
public void setCustomLayout_onPlaybackStateCompatChangedCalled() throws Exception {
|
||||
List<CommandButton> buttons = new ArrayList<>();
|
||||
Bundle extras1 = new Bundle();
|
||||
extras1.putString("key", "value-1");
|
||||
CommandButton button1 =
|
||||
new CommandButton.Builder()
|
||||
.setSessionCommand(new SessionCommand("action1", extras1))
|
||||
.setDisplayName("actionName1")
|
||||
.setIconResId(1)
|
||||
.build();
|
||||
SessionCommand command1 = new SessionCommand("command1", extras1);
|
||||
Bundle extras2 = new Bundle();
|
||||
extras2.putString("key", "value-2");
|
||||
CommandButton button2 =
|
||||
SessionCommand command2 = new SessionCommand("command2", extras2);
|
||||
ImmutableList<CommandButton> customLayout =
|
||||
ImmutableList.of(
|
||||
new CommandButton.Builder()
|
||||
.setSessionCommand(new SessionCommand("action2", extras2))
|
||||
.setDisplayName("actionName2")
|
||||
.setSessionCommand(command1)
|
||||
.setDisplayName("command1")
|
||||
.setIconResId(1)
|
||||
.build()
|
||||
.copyWithIsEnabled(true),
|
||||
new CommandButton.Builder()
|
||||
.setSessionCommand(command2)
|
||||
.setDisplayName("command2")
|
||||
.setIconResId(2)
|
||||
.build();
|
||||
buttons.add(button1);
|
||||
buttons.add(button2);
|
||||
List<String> receivedActions = new ArrayList<>();
|
||||
List<String> receivedDisplayNames = new ArrayList<>();
|
||||
List<String> receivedBundleValues = new ArrayList<>();
|
||||
List<Integer> receivedIconResIds = new ArrayList<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
.build()
|
||||
.copyWithIsEnabled(true));
|
||||
List<ImmutableList<CommandButton>> reportedCustomLayouts = new ArrayList<>();
|
||||
CountDownLatch latch1 = new CountDownLatch(2);
|
||||
MediaControllerCompat.Callback callback =
|
||||
new MediaControllerCompat.Callback() {
|
||||
@Override
|
||||
public void onPlaybackStateChanged(PlaybackStateCompat state) {
|
||||
List<PlaybackStateCompat.CustomAction> layout = state.getCustomActions();
|
||||
for (PlaybackStateCompat.CustomAction action : layout) {
|
||||
receivedActions.add(action.getAction());
|
||||
receivedDisplayNames.add(String.valueOf(action.getName()));
|
||||
receivedBundleValues.add(action.getExtras().getString("key"));
|
||||
receivedIconResIds.add(action.getIcon());
|
||||
}
|
||||
latch.countDown();
|
||||
reportedCustomLayouts.add(MediaUtils.convertToCustomLayout(state));
|
||||
latch1.countDown();
|
||||
}
|
||||
};
|
||||
controllerCompat.registerCallback(callback, handler);
|
||||
|
||||
session.setCustomLayout(buttons);
|
||||
session.setCustomLayout(customLayout);
|
||||
session.setAvailableCommands(
|
||||
SessionCommands.EMPTY.buildUpon().add(command1).add(command2).build(),
|
||||
Player.Commands.EMPTY);
|
||||
|
||||
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
assertThat(receivedActions).containsExactly("action1", "action2").inOrder();
|
||||
assertThat(receivedDisplayNames).containsExactly("actionName1", "actionName2").inOrder();
|
||||
assertThat(receivedIconResIds).containsExactly(1, 2).inOrder();
|
||||
assertThat(receivedBundleValues).containsExactly("value-1", "value-2").inOrder();
|
||||
assertThat(latch1.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
assertThat(reportedCustomLayouts.get(0)).containsExactly(customLayout.get(0));
|
||||
assertThat(reportedCustomLayouts.get(1)).isEqualTo(customLayout);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -1403,6 +1400,6 @@ public class MediaControllerCompatCallbackWithMediaSessionTest {
|
||||
PlaybackStateCompat state, PlaybackException playerError) {
|
||||
assertThat(state.getState()).isEqualTo(PlaybackStateCompat.STATE_ERROR);
|
||||
assertThat(state.getErrorCode()).isEqualTo(PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR);
|
||||
assertThat(state.getErrorMessage()).isEqualTo(playerError.getMessage());
|
||||
assertThat(state.getErrorMessage().toString()).isEqualTo(playerError.getMessage());
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,8 @@ import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.common.util.Consumer;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.session.MediaSession.ConnectionResult;
|
||||
import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuilder;
|
||||
import androidx.media3.test.session.R;
|
||||
import androidx.media3.test.session.common.HandlerThreadTestRule;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
@ -51,6 +53,7 @@ import androidx.test.filters.LargeTest;
|
||||
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.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@ -1459,18 +1462,23 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
|
||||
.setIconResId(R.drawable.media3_notification_pause)
|
||||
.setSessionCommand(command2)
|
||||
.build());
|
||||
MediaSession mediaSession = createMediaSession(player, /* callback= */ null, customLayout);
|
||||
MediaSession.Callback callback =
|
||||
new MediaSession.Callback() {
|
||||
@Override
|
||||
public ConnectionResult onConnect(
|
||||
MediaSession session, MediaSession.ControllerInfo controller) {
|
||||
return new AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(
|
||||
ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(command1).build())
|
||||
.build();
|
||||
}
|
||||
};
|
||||
MediaSession mediaSession = createMediaSession(player, callback, customLayout);
|
||||
connectMediaNotificationController(mediaSession);
|
||||
MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession);
|
||||
|
||||
// Wait until a playback state is sent to the controller.
|
||||
PlaybackStateCompat firstPlaybackState =
|
||||
getFirstPlaybackState(controllerCompat, threadTestRule.getHandler());
|
||||
|
||||
assertThat(MediaUtils.convertToCustomLayout(firstPlaybackState))
|
||||
.containsExactly(
|
||||
customLayout.get(0).copyWithIsEnabled(true),
|
||||
customLayout.get(1).copyWithIsEnabled(true))
|
||||
.inOrder();
|
||||
assertThat(MediaUtils.convertToCustomLayout(controllerCompat.getPlaybackState()))
|
||||
.containsExactly(customLayout.get(0).copyWithIsEnabled(true));
|
||||
mediaSession.release();
|
||||
releasePlayer(player);
|
||||
}
|
||||
@ -1497,11 +1505,23 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
|
||||
.setIconResId(R.drawable.media3_notification_pause)
|
||||
.setSessionCommand(command2)
|
||||
.build());
|
||||
MediaSession mediaSession = createMediaSession(player);
|
||||
MediaSession.Callback callback =
|
||||
new MediaSession.Callback() {
|
||||
@Override
|
||||
public ConnectionResult onConnect(
|
||||
MediaSession session, MediaSession.ControllerInfo controller) {
|
||||
return new ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(
|
||||
ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(command1).build())
|
||||
.build();
|
||||
}
|
||||
};
|
||||
MediaSession mediaSession = createMediaSession(player, callback);
|
||||
connectMediaNotificationController(mediaSession);
|
||||
MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession);
|
||||
ImmutableList<CommandButton> initialCustomLayout =
|
||||
MediaUtils.convertToCustomLayout(controllerCompat.getPlaybackState());
|
||||
AtomicReference<List<CommandButton>> reportedCustomLayout = new AtomicReference<>();
|
||||
// Wait until a playback state is sent to the controller.
|
||||
getFirstPlaybackState(controllerCompat, threadTestRule.getHandler());
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
controllerCompat.registerCallback(
|
||||
new MediaControllerCompat.Callback() {
|
||||
@ -1516,14 +1536,97 @@ public class MediaControllerCompatPlaybackStateCompatActionsWithMediaSessionTest
|
||||
getInstrumentation().runOnMainSync(() -> mediaSession.setCustomLayout(customLayout));
|
||||
|
||||
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
assertThat(initialCustomLayout).isEmpty();
|
||||
assertThat(reportedCustomLayout.get())
|
||||
.containsExactly(
|
||||
customLayout.get(0).copyWithIsEnabled(true),
|
||||
customLayout.get(1).copyWithIsEnabled(true));
|
||||
.containsExactly(customLayout.get(0).copyWithIsEnabled(true));
|
||||
mediaSession.release();
|
||||
releasePlayer(player);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
playerWithCustomLayout_setCustomLayoutForMediaNotificationController_playbackStateChangedWithCustomActionsChanged()
|
||||
throws Exception {
|
||||
Player player = createDefaultPlayer();
|
||||
Bundle extras1 = new Bundle();
|
||||
extras1.putString("key1", "value1");
|
||||
Bundle extras2 = new Bundle();
|
||||
extras1.putString("key2", "value2");
|
||||
SessionCommand command1 = new SessionCommand("command1", extras1);
|
||||
SessionCommand command2 = new SessionCommand("command2", extras2);
|
||||
ImmutableList<CommandButton> customLayout =
|
||||
ImmutableList.of(
|
||||
new CommandButton.Builder()
|
||||
.setDisplayName("button1")
|
||||
.setIconResId(R.drawable.media3_notification_play)
|
||||
.setSessionCommand(command1)
|
||||
.build(),
|
||||
new CommandButton.Builder()
|
||||
.setDisplayName("button2")
|
||||
.setIconResId(R.drawable.media3_notification_pause)
|
||||
.setSessionCommand(command2)
|
||||
.build());
|
||||
MediaSession.Callback callback =
|
||||
new MediaSession.Callback() {
|
||||
@Override
|
||||
public ConnectionResult onConnect(
|
||||
MediaSession session, MediaSession.ControllerInfo controller) {
|
||||
return new ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(
|
||||
ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon().add(command1).build())
|
||||
.build();
|
||||
}
|
||||
};
|
||||
MediaSession mediaSession = createMediaSession(player, callback);
|
||||
connectMediaNotificationController(mediaSession);
|
||||
MediaControllerCompat controllerCompat = createMediaControllerCompat(mediaSession);
|
||||
ImmutableList<CommandButton> initialCustomLayout =
|
||||
MediaUtils.convertToCustomLayout(controllerCompat.getPlaybackState());
|
||||
AtomicReference<List<CommandButton>> reportedCustomLayout = new AtomicReference<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
controllerCompat.registerCallback(
|
||||
new MediaControllerCompat.Callback() {
|
||||
@Override
|
||||
public void onPlaybackStateChanged(PlaybackStateCompat state) {
|
||||
reportedCustomLayout.set(MediaUtils.convertToCustomLayout(state));
|
||||
latch.countDown();
|
||||
}
|
||||
},
|
||||
threadTestRule.getHandler());
|
||||
|
||||
getInstrumentation()
|
||||
.runOnMainSync(
|
||||
() ->
|
||||
mediaSession.setCustomLayout(
|
||||
mediaSession.getMediaNotificationControllerInfo(), customLayout));
|
||||
|
||||
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
assertThat(initialCustomLayout).isEmpty();
|
||||
assertThat(reportedCustomLayout.get())
|
||||
.containsExactly(customLayout.get(0).copyWithIsEnabled(true));
|
||||
mediaSession.release();
|
||||
releasePlayer(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect a controller that mimics the media notification controller that is connected by {@link
|
||||
* MediaNotificationManager} when the session is running in the service.
|
||||
*/
|
||||
private void connectMediaNotificationController(MediaSession mediaSession)
|
||||
throws InterruptedException {
|
||||
CountDownLatch connectionLatch = new CountDownLatch(1);
|
||||
Bundle connectionHints = new Bundle();
|
||||
connectionHints.putBoolean(MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER, true);
|
||||
ListenableFuture<MediaController> mediaNotificationControllerFuture =
|
||||
new MediaController.Builder(
|
||||
ApplicationProvider.getApplicationContext(), mediaSession.getToken())
|
||||
.setConnectionHints(connectionHints)
|
||||
.buildAsync();
|
||||
mediaNotificationControllerFuture.addListener(
|
||||
connectionLatch::countDown, MoreExecutors.directExecutor());
|
||||
assertThat(connectionLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
}
|
||||
|
||||
private PlaybackStateCompat getFirstPlaybackState(
|
||||
MediaControllerCompat mediaControllerCompat, Handler handler) throws InterruptedException {
|
||||
LinkedBlockingDeque<PlaybackStateCompat> playbackStateCompats = new LinkedBlockingDeque<>();
|
||||
|
@ -27,18 +27,24 @@ import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.session.MediaSession.ControllerInfo;
|
||||
import androidx.media3.test.session.R;
|
||||
import androidx.media3.test.session.common.HandlerThreadTestRule;
|
||||
import androidx.media3.test.session.common.MainLooperTestRule;
|
||||
import androidx.media3.test.session.common.TestHandler;
|
||||
import androidx.media3.test.session.common.TestUtils;
|
||||
import androidx.media3.test.utils.TestExoPlayerBuilder;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.MediumTest;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import java.util.ArrayList;
|
||||
@ -145,6 +151,100 @@ public class MediaSessionServiceTest {
|
||||
service.blockUntilAllControllersUnbind(TIMEOUT_MS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onCreate_mediaNotificationManagerController_correctSessionStateFromOnConnect()
|
||||
throws Exception {
|
||||
SessionCommand command1 = new SessionCommand("command1", Bundle.EMPTY);
|
||||
SessionCommand command2 = new SessionCommand("command2", Bundle.EMPTY);
|
||||
SessionCommand command3 = new SessionCommand("command3", Bundle.EMPTY);
|
||||
CommandButton button1 =
|
||||
new CommandButton.Builder()
|
||||
.setDisplayName("button1")
|
||||
.setIconResId(R.drawable.media3_notification_small_icon)
|
||||
.setSessionCommand(command1)
|
||||
.build();
|
||||
CommandButton button2 =
|
||||
new CommandButton.Builder()
|
||||
.setDisplayName("button2")
|
||||
.setIconResId(R.drawable.media3_notification_small_icon)
|
||||
.setSessionCommand(command2)
|
||||
.build();
|
||||
CommandButton button3 =
|
||||
new CommandButton.Builder()
|
||||
.setDisplayName("button3")
|
||||
.setIconResId(R.drawable.media3_notification_small_icon)
|
||||
.setSessionCommand(command3)
|
||||
.build();
|
||||
Bundle testHints = new Bundle();
|
||||
testHints.putString("test_key", "test_value");
|
||||
List<ControllerInfo> controllerInfoList = new ArrayList<>();
|
||||
CountDownLatch latch = new CountDownLatch(2);
|
||||
TestHandler handler = new TestHandler(Looper.getMainLooper());
|
||||
ExoPlayer player =
|
||||
handler.postAndSync(
|
||||
() -> {
|
||||
ExoPlayer exoPlayer = new TestExoPlayerBuilder(context).build();
|
||||
exoPlayer.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample.mp4"));
|
||||
exoPlayer.prepare();
|
||||
return exoPlayer;
|
||||
});
|
||||
MediaSession mediaSession =
|
||||
new MediaSession.Builder(ApplicationProvider.getApplicationContext(), player)
|
||||
.setCustomLayout(Lists.newArrayList(button1, button2))
|
||||
.setCallback(
|
||||
new MediaSession.Callback() {
|
||||
@Override
|
||||
public MediaSession.ConnectionResult onConnect(
|
||||
MediaSession session, ControllerInfo controller) {
|
||||
controllerInfoList.add(controller);
|
||||
if (session.isMediaNotificationController(controller)) {
|
||||
latch.countDown();
|
||||
return new MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(
|
||||
SessionCommands.EMPTY.buildUpon().add(command1).add(command3).build())
|
||||
.setAvailablePlayerCommands(Player.Commands.EMPTY)
|
||||
.setCustomLayout(ImmutableList.of(button1, button3))
|
||||
.build();
|
||||
}
|
||||
latch.countDown();
|
||||
return new MediaSession.ConnectionResult.AcceptedResultBuilder(session).build();
|
||||
}
|
||||
})
|
||||
.build();
|
||||
TestServiceRegistry.getInstance().setOnGetSessionHandler(controllerInfo -> mediaSession);
|
||||
MediaControllerCompat mediaControllerCompat =
|
||||
new MediaControllerCompat(
|
||||
ApplicationProvider.getApplicationContext(), mediaSession.getSessionCompat());
|
||||
ImmutableList<CommandButton> initialCustomLayoutInControllerCompat =
|
||||
MediaUtils.convertToCustomLayout(mediaControllerCompat.getPlaybackState());
|
||||
|
||||
// Start the service by creating a remote controller.
|
||||
RemoteMediaController remoteController =
|
||||
controllerTestRule.createRemoteController(token, /* waitForConnection= */ true, testHints);
|
||||
|
||||
assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue();
|
||||
assertThat(
|
||||
controllerInfoList
|
||||
.get(0)
|
||||
.getConnectionHints()
|
||||
.getBoolean(
|
||||
MediaNotificationManager.KEY_MEDIA_NOTIFICATION_MANAGER,
|
||||
/* defaultValue= */ false))
|
||||
.isTrue();
|
||||
assertThat(TestUtils.equals(controllerInfoList.get(1).getConnectionHints(), testHints))
|
||||
.isTrue();
|
||||
assertThat(mediaControllerCompat.getPlaybackState().getActions())
|
||||
.isEqualTo(PlaybackStateCompat.ACTION_SET_RATING);
|
||||
assertThat(remoteController.getCustomLayout()).containsExactly(button1, button2).inOrder();
|
||||
assertThat(initialCustomLayoutInControllerCompat).isEmpty();
|
||||
assertThat(MediaUtils.convertToCustomLayout(mediaControllerCompat.getPlaybackState()))
|
||||
.containsExactly(button1.copyWithIsEnabled(true), button3.copyWithIsEnabled(true))
|
||||
.inOrder();
|
||||
mediaSession.release();
|
||||
((MockMediaSessionService) TestServiceRegistry.getInstance().getServiceInstance())
|
||||
.blockUntilAllControllersUnbind(TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether {@link MediaSessionService#onGetSession(ControllerInfo)} is called when
|
||||
* controller tries to connect, with the proper arguments.
|
||||
|
@ -16,6 +16,7 @@
|
||||
package androidx.media3.session;
|
||||
|
||||
import static androidx.media3.common.Player.COMMAND_GET_TRACKS;
|
||||
import static androidx.media3.session.MediaSession.ConnectionResult.accept;
|
||||
import static androidx.media3.test.session.common.CommonConstants.ACTION_MEDIA3_SESSION;
|
||||
import static androidx.media3.test.session.common.CommonConstants.KEY_AUDIO_ATTRIBUTES;
|
||||
import static androidx.media3.test.session.common.CommonConstants.KEY_AVAILABLE_COMMANDS;
|
||||
@ -63,6 +64,7 @@ import static androidx.media3.test.session.common.MediaSessionConstants.TEST_CON
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET_CUSTOM_LAYOUT;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_GET_SESSION_ACTIVITY;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_IS_SESSION_COMMAND_AVAILABLE;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_MEDIA_CONTROLLER_COMPAT_CALLBACK_WITH_MEDIA_SESSION_TEST;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_ON_TRACKS_CHANGED_VIDEO_TO_AUDIO_TRANSITION;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_ON_VIDEO_SIZE_CHANGED;
|
||||
import static androidx.media3.test.session.common.MediaSessionConstants.TEST_SET_SHOW_PLAY_BUTTON_IF_SUPPRESSED_TO_FALSE;
|
||||
@ -101,6 +103,7 @@ import androidx.media3.test.session.common.TestHandler;
|
||||
import androidx.media3.test.session.common.TestHandler.TestRunnable;
|
||||
import androidx.media3.test.session.common.TestUtils;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -113,6 +116,8 @@ import java.util.concurrent.Callable;
|
||||
*/
|
||||
public class MediaSessionProviderService extends Service {
|
||||
|
||||
public static final String KEY_ENABLE_FAKE_MEDIA_NOTIFICATION_MANAGER_CONTROLLER =
|
||||
"key_enable_fake_media_notification_manager_controller";
|
||||
private static final String TAG = "MSProviderService";
|
||||
|
||||
private Map<String, MediaSession> sessionMap = new HashMap<>();
|
||||
@ -164,15 +169,18 @@ public class MediaSessionProviderService extends Service {
|
||||
|
||||
@Override
|
||||
public void create(String sessionId, Bundle tokenExtras) throws RemoteException {
|
||||
if (tokenExtras == null) {
|
||||
tokenExtras = Bundle.EMPTY;
|
||||
}
|
||||
boolean useFakeMediaNotificationManagerController =
|
||||
tokenExtras.getBoolean(
|
||||
KEY_ENABLE_FAKE_MEDIA_NOTIFICATION_MANAGER_CONTROLLER, /* defaultValue= */ false);
|
||||
MockPlayer mockPlayer =
|
||||
new MockPlayer.Builder().setApplicationLooper(handler.getLooper()).build();
|
||||
MediaSession.Builder builder =
|
||||
new MediaSession.Builder(MediaSessionProviderService.this, mockPlayer).setId(sessionId);
|
||||
|
||||
if (tokenExtras != null) {
|
||||
builder.setExtras(tokenExtras);
|
||||
}
|
||||
|
||||
switch (sessionId) {
|
||||
case TEST_GET_SESSION_ACTIVITY:
|
||||
{
|
||||
@ -194,7 +202,7 @@ public class MediaSessionProviderService extends Service {
|
||||
@Override
|
||||
public MediaSession.ConnectionResult onConnect(
|
||||
MediaSession session, ControllerInfo controller) {
|
||||
return MediaSession.ConnectionResult.accept(
|
||||
return accept(
|
||||
new SessionCommands.Builder()
|
||||
.add(new SessionCommand("command1", Bundle.EMPTY))
|
||||
.add(new SessionCommand("command2", Bundle.EMPTY))
|
||||
@ -216,8 +224,7 @@ public class MediaSessionProviderService extends Service {
|
||||
@Override
|
||||
public MediaSession.ConnectionResult onConnect(
|
||||
MediaSession session, ControllerInfo controller) {
|
||||
return MediaSession.ConnectionResult.accept(
|
||||
availableSessionCommands, Player.Commands.EMPTY);
|
||||
return accept(availableSessionCommands, Player.Commands.EMPTY);
|
||||
}
|
||||
});
|
||||
break;
|
||||
@ -244,8 +251,7 @@ public class MediaSessionProviderService extends Service {
|
||||
@Override
|
||||
public MediaSession.ConnectionResult onConnect(
|
||||
MediaSession session, ControllerInfo controller) {
|
||||
return MediaSession.ConnectionResult.accept(
|
||||
availableSessionCommands, Player.Commands.EMPTY);
|
||||
return accept(availableSessionCommands, Player.Commands.EMPTY);
|
||||
}
|
||||
});
|
||||
break;
|
||||
@ -272,8 +278,7 @@ public class MediaSessionProviderService extends Service {
|
||||
.getBoolean(KEY_COMMAND_GET_TASKS_UNAVAILABLE, /* defaultValue= */ false)) {
|
||||
commandBuilder.remove(COMMAND_GET_TRACKS);
|
||||
}
|
||||
return MediaSession.ConnectionResult.accept(
|
||||
SessionCommands.EMPTY, commandBuilder.build());
|
||||
return accept(SessionCommands.EMPTY, commandBuilder.build());
|
||||
}
|
||||
});
|
||||
break;
|
||||
@ -290,6 +295,31 @@ public class MediaSessionProviderService extends Service {
|
||||
builder.setShowPlayButtonIfPlaybackIsSuppressed(false);
|
||||
break;
|
||||
}
|
||||
case TEST_MEDIA_CONTROLLER_COMPAT_CALLBACK_WITH_MEDIA_SESSION_TEST:
|
||||
{
|
||||
builder.setCallback(
|
||||
new MediaSession.Callback() {
|
||||
@Override
|
||||
public MediaSession.ConnectionResult onConnect(
|
||||
MediaSession session, ControllerInfo controller) {
|
||||
MediaSession.ConnectionResult connectionResult =
|
||||
MediaSession.Callback.super.onConnect(session, controller);
|
||||
SessionCommands availableSessionCommands =
|
||||
connectionResult.availableSessionCommands;
|
||||
if (session.isMediaNotificationController(controller)) {
|
||||
availableSessionCommands =
|
||||
connectionResult
|
||||
.availableSessionCommands
|
||||
.buildUpon()
|
||||
.add(new SessionCommand("command1", Bundle.EMPTY))
|
||||
.build();
|
||||
}
|
||||
return accept(
|
||||
availableSessionCommands, connectionResult.availablePlayerCommands);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: // fall out
|
||||
}
|
||||
|
||||
@ -297,6 +327,22 @@ public class MediaSessionProviderService extends Service {
|
||||
() -> {
|
||||
MediaSession session = builder.build();
|
||||
session.setSessionPositionUpdateDelayMs(0L);
|
||||
if (useFakeMediaNotificationManagerController) {
|
||||
Bundle connectionHints = new Bundle();
|
||||
connectionHints.putBoolean("androidx.media3.session.MediaNotificationManager", true);
|
||||
//noinspection unused
|
||||
ListenableFuture<MediaController> unusedFuture =
|
||||
new MediaController.Builder(getApplicationContext(), session.getToken())
|
||||
.setListener(
|
||||
new MediaController.Listener() {
|
||||
@Override
|
||||
public void onDisconnected(MediaController controller) {
|
||||
controller.release();
|
||||
}
|
||||
})
|
||||
.setConnectionHints(connectionHints)
|
||||
.buildAsync();
|
||||
}
|
||||
sessionMap.put(sessionId, session);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user