/* * Copyright 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.media2.session; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; import static androidx.media2.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED; import static androidx.media2.session.SessionResult.RESULT_SUCCESS; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; 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.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.concurrent.ListenableFuture; import androidx.core.content.ContextCompat; import androidx.core.util.ObjectsCompat; import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media2.common.CallbackMediaItem; import androidx.media2.common.MediaItem; import androidx.media2.common.MediaMetadata; import androidx.media2.common.Rating; import androidx.media2.common.SessionPlayer; import androidx.media2.common.SessionPlayer.BuffState; import androidx.media2.common.SessionPlayer.PlayerResult; import androidx.media2.common.SessionPlayer.PlayerState; import androidx.media2.common.SessionPlayer.TrackInfo; import androidx.media2.common.SubtitleData; import androidx.media2.common.UriMediaItem; import androidx.media2.common.VideoSize; import androidx.media2.session.MediaController.PlaybackInfo; import androidx.media2.session.MediaLibraryService.LibraryParams; import androidx.media2.session.SessionResult.ResultCode; import androidx.versionedparcelable.ParcelField; import androidx.versionedparcelable.VersionedParcelable; import androidx.versionedparcelable.VersionedParcelize; import java.util.HashMap; import java.util.List; import java.util.concurrent.Executor; /** * Allows a media app to expose its transport controls and playback information in a process to * other processes including the Android framework and other apps. Common use cases are as follows. *
* A MediaSession should be created when an app wants to publish media playback information or * handle media keys. In general an app only needs one session for all playback, though multiple * sessions can be created to provide finer grain controls of media. *
* If you want to support background playback, {@link MediaSessionService} is preferred * instead. With it, your playback can be revived even after playback is finished. See * {@link MediaSessionService} for details. *
* Topic covered here: *
* ** A session can be obtained by {@link Builder}. The owner of the session may pass its session token * to other processes to allow them to create a {@link MediaController} to interact with the * session. *
* When a session receive transport control commands, the session sends the commands directly to * the the underlying media player set by {@link Builder} or {@link #updatePlayer}. *
* When an app is finished performing playback it must call {@link #close()} to clean up the session * and notify any controllers. The app is responsible for closing the underlying player after * closing the session. * is closed. *
* {@link MediaSession} objects are thread safe, but should be used on the thread on the looper. * *
* Here's the table of per key event. *
Key code | {@link MediaSession} API |
---|---|
{@link KeyEvent#KEYCODE_MEDIA_PLAY} | *{@link SessionPlayer#play()} |
{@link KeyEvent#KEYCODE_MEDIA_PAUSE} | *{@link SessionPlayer#pause()} |
{@link KeyEvent#KEYCODE_MEDIA_NEXT} | *{@link SessionPlayer#skipToNextPlaylistItem()} |
{@link KeyEvent#KEYCODE_MEDIA_PREVIOUS} | *{@link SessionPlayer#skipToPreviousPlaylistItem()} |
{@link KeyEvent#KEYCODE_MEDIA_STOP} | *{@link SessionPlayer#pause()} and then * {@link SessionPlayer#seekTo(long)} with 0 |
{@link KeyEvent#KEYCODE_MEDIA_FAST_FORWARD} | *{@link SessionCallback#onFastForward} |
{@link KeyEvent#KEYCODE_MEDIA_REWIND} | *{@link SessionCallback#onRewind} |
|
*
|
*
* When the session is closed, it returns the lastly set player.
*
* @return player.
*/
public @NonNull SessionPlayer getPlayer() {
return mImpl.getPlayer();
}
/**
* Gets the session ID
*
* @return
*/
public @NonNull String getId() {
return mImpl.getId();
}
/**
* Returns the {@link SessionToken} for creating {@link MediaController}.
*/
public @NonNull SessionToken getToken() {
return mImpl.getToken();
}
@NonNull Context getContext() {
return mImpl.getContext();
}
@NonNull Executor getCallbackExecutor() {
return mImpl.getCallbackExecutor();
}
@NonNull SessionCallback getCallback() {
return mImpl.getCallback();
}
/**
* Returns the list of connected controller.
*
* @return list of {@link ControllerInfo}
*/
public @NonNull List
* It's up to controller's decision how to represent the layout in its own UI.
* Here are some examples.
*
* Note:
* This API can be called in the
* {@link SessionCallback#onConnect(MediaSession, ControllerInfo)}.
*
* @param controller controller to specify layout.
* @param layout ordered list of layout.
*/
public @NonNull ListenableFuture
* This is synchronous call. Changes in the allowed commands take effect immediately regardless
* of the controller notified about the change through
* {@link MediaController.ControllerCallback
* #onAllowedCommandsChanged(MediaController, SessionCommandGroup)}
*
* @param controller controller to change allowed commands
* @param commands new allowed commands
*/
public void setAllowedCommands(@NonNull ControllerInfo controller,
@NonNull SessionCommandGroup commands) {
if (controller == null) {
throw new NullPointerException("controller shouldn't be null");
}
if (commands == null) {
throw new NullPointerException("commands shouldn't be null");
}
mImpl.setAllowedCommands(controller, commands);
}
/**
* Broadcasts custom command to all connected controllers.
*
* This is synchronous call and doesn't wait for result from the controller. Use
* {@link #sendCustomCommand(ControllerInfo, SessionCommand, Bundle)} for getting the result.
*
* A command is not accepted if it is not a custom command.
*
* @param command a command
* @param args optional argument
* @see #sendCustomCommand(ControllerInfo, SessionCommand, Bundle)
*/
public void broadcastCustomCommand(@NonNull SessionCommand command, @Nullable Bundle args) {
if (command == null) {
throw new NullPointerException("command shouldn't be null");
}
if (command.getCommandCode() != SessionCommand.COMMAND_CODE_CUSTOM) {
throw new IllegalArgumentException("command should be a custom command");
}
mImpl.broadcastCustomCommand(command, args);
}
/**
* Send custom command to a specific controller.
*
* A command is not accepted if it is not a custom command.
*
* @param command a command
* @param args optional argument
* @see #broadcastCustomCommand(SessionCommand, Bundle)
*/
public @NonNull ListenableFuture
* If it's not set, the session will accept all controllers and all incoming commands by
* default.
*/
public abstract static class SessionCallback {
ForegroundServiceEventCallback mForegroundServiceEventCallback;
/**
* Called when a controller is created for this session. Return allowed commands for
* controller. By default it allows all connection requests and commands.
*
* You can reject the connection by return {@code null}. In that case, the controller
* receives {@link MediaController.ControllerCallback#onDisconnected(MediaController)} and
* cannot be used.
*
* The controller hasn't connected yet in this method, so calls to the controller
* (e.g. {@link #sendCustomCommand}, {@link #setCustomLayout}) would be ignored. Override
* {@link #onPostConnect} for the custom initialization for the controller instead.
*
* @param session the session for this event
* @param controller controller information.
* @return allowed commands. Can be {@code null} to reject connection.
* @see #onPostConnect(MediaSession, ControllerInfo)
*/
public @Nullable SessionCommandGroup onConnect(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
SessionCommandGroup commands = new SessionCommandGroup.Builder()
.addAllPredefinedCommands(SessionCommand.COMMAND_VERSION_1)
.build();
return commands;
}
/**
* Called immediately after a controller is connected. This is a convenient method to add
* custom initialization between the session and a controller.
*
* Note that calls to the controller (e.g. {@link #sendCustomCommand},
* {@link #setCustomLayout}) work here but don't work in {@link #onConnect} because the
* controller hasn't connected yet in {@link #onConnect}.
*
* @param session the session for this event
* @param controller controller information.
*/
public void onPostConnect(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
}
/**
* Called when a controller is disconnected
*
* @param session the session for this event
* @param controller controller information
*/
public void onDisconnected(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {}
/**
* Called when a controller sent a command which will be sent directly to one of the
* following:
*
* Return {@link SessionResult#RESULT_SUCCESS} to proceed the command. If something
* else is returned, command wouldn't be sent and the controller would receive the code with
* it.
*
* @param session the session for this event
* @param controller controller information.
* @param command a command. This method will be called for every single command.
* @return {@code RESULT_SUCCESS} if you want to proceed with incoming command.
* Another code for ignore.
* @see SessionCommand#COMMAND_CODE_PLAYER_PLAY
* @see SessionCommand#COMMAND_CODE_PLAYER_PAUSE
* @see SessionCommand#COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_PREPARE
* @see SessionCommand#COMMAND_CODE_PLAYER_SEEK_TO
* @see SessionCommand#COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_SET_SHUFFLE_MODE
* @see SessionCommand#COMMAND_CODE_PLAYER_SET_REPEAT_MODE
* @see SessionCommand#COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_GET_PLAYLIST
* @see SessionCommand#COMMAND_CODE_PLAYER_SET_PLAYLIST
* @see SessionCommand#COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA
* @see SessionCommand#COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA
* @see SessionCommand#COMMAND_CODE_VOLUME_SET_VOLUME
* @see SessionCommand#COMMAND_CODE_VOLUME_ADJUST_VOLUME
*/
public @ResultCode int onCommandRequest(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull SessionCommand command) {
return RESULT_SUCCESS;
}
/**
* Called when a controller has sent a command with a {@link MediaItem} to add a new media
* item to this session. Being specific, this will be called for following APIs.
*
* If the given media ID is valid, you should return the media item with the given media ID.
* If the ID doesn't match, an {@link RuntimeException} will be thrown.
* You may return {@code null} if the given item is invalid. Here's the behavior when it
* happens.
*
* This will be called on the same thread where {@link #onCommandRequest} and commands with
* the media controller will be executed.
*
* Default implementation returns the {@code null}.
*
* @param session the session for this event
* @param controller controller information
* @param mediaId non-empty media id for creating item with
* @return translated media item for player with the mediaId. Can be {@code null} to ignore.
* @see MediaMetadata#METADATA_KEY_MEDIA_ID
*/
public @Nullable MediaItem onCreateMediaItem(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String mediaId) {
return null;
}
/**
* Called when a controller set rating of a media item through
* {@link MediaController#setRating(String, Rating)}.
*
* To allow setting user rating for a {@link MediaItem}, the media item's metadata
* should have {@link Rating} with the key {@link MediaMetadata#METADATA_KEY_USER_RATING},
* in order to provide possible rating style for controller. Controller will follow the
* rating style.
*
* @param session the session for this event
* @param controller controller information
* @param mediaId non-empty media id
* @param rating new rating from the controller
* @see SessionCommand#COMMAND_CODE_SESSION_SET_RATING
*/
public @ResultCode int onSetRating(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String mediaId,
@NonNull Rating rating) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller sent a custom command through
* {@link MediaController#sendCustomCommand(SessionCommand, Bundle)}.
*
* Interoperability: This would be also called by {@link
* android.support.v4.media.MediaBrowserCompat
* #sendCustomAction(String, Bundle, CustomActionCallback)}. If so, extra from
* sendCustomAction will be considered as args and customCommand would have null extra.
*
* @param session the session for this event
* @param controller controller information
* @param customCommand custom command.
* @param args optional arguments
* @return result of handling custom command. A runtime exception will be thrown if
* {@code null} is returned.
* @see SessionCommand#COMMAND_CODE_CUSTOM
*/
public @NonNull SessionResult onCustomCommand(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull SessionCommand customCommand,
@Nullable Bundle args) {
return new SessionResult(RESULT_ERROR_NOT_SUPPORTED, null);
}
/**
* Called when a controller requested to play a specific mediaId through
* {@link MediaController#playFromMediaId(String, Bundle)}.
*
* @param session the session for this event
* @param controller controller information
* @param mediaId non-empty media id
* @param extras optional extra bundle
* @see SessionCommand#COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPlayFromMediaId(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String mediaId,
@Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller requested to begin playback from a search query through
* {@link MediaController#playFromSearch(String, Bundle)}
*
* @param session the session for this event
* @param controller controller information
* @param query non-empty search query.
* @param extras optional extra bundle
* @see SessionCommand#COMMAND_CODE_SESSION_PLAY_FROM_SEARCH
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPlayFromSearch(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String query,
@Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller requested to play a specific media item represented by a URI
* through {@link MediaController#playFromUri(Uri, Bundle)}
*
* @param session the session for this event
* @param controller controller information
* @param uri uri
* @param extras optional extra bundle
* @see SessionCommand#COMMAND_CODE_SESSION_PLAY_FROM_URI
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPlayFromUri(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull Uri uri,
@Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller requested to prepare for playing a specific mediaId through
* {@link MediaController#prepareFromMediaId(String, Bundle)}.
*
* During the prepare, a session should not hold audio focus in order to allow
* other sessions play seamlessly. The state of playback should be updated to
* {@link SessionPlayer#PLAYER_STATE_PAUSED} after the prepare is done.
*
* The playback of the prepared content should start in the later calls of
* {@link SessionPlayer#play()}.
*
* Override {@link #onPlayFromMediaId} to handle requests for starting
* playback without preparation.
*
* @param session the session for this event
* @param controller controller information
* @param mediaId non-empty media id
* @param extras optional extra bundle
* @see SessionCommand#COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPrepareFromMediaId(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String mediaId,
@Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller requested to prepare playback from a search query through
* {@link MediaController#prepareFromSearch(String, Bundle)}.
*
* During the prepare, a session should not hold audio focus in order to allow
* other sessions play seamlessly. The state of playback should be updated to
* {@link SessionPlayer#PLAYER_STATE_PAUSED} after the prepare is done.
*
* The playback of the prepared content should start in the later calls of
* {@link SessionPlayer#play()}.
*
* Override {@link #onPlayFromSearch} to handle requests for starting playback without
* preparation.
*
* @param session the session for this event
* @param controller controller information
* @param query non-empty search query
* @param extras optional extra bundle
* @see SessionCommand#COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPrepareFromSearch(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String query,
@Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller requested to prepare a specific media item represented by a URI
* through {@link MediaController#prepareFromUri(Uri, Bundle)}.
*
* During the prepare, a session should not hold audio focus in order to allow
* other sessions play seamlessly. The state of playback should be updated to
* {@link SessionPlayer#PLAYER_STATE_PAUSED} after the prepare is done.
*
* The playback of the prepared content should start in the later calls of
* {@link SessionPlayer#play()}.
*
* Override {@link #onPlayFromUri} to handle requests for starting playback without
* preparation.
*
* @param session the session for this event
* @param controller controller information
* @param uri uri
* @param extras optional extra bundle
* @see SessionCommand#COMMAND_CODE_SESSION_PREPARE_FROM_URI
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPrepareFromUri(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull Uri uri, @Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller called {@link MediaController#fastForward()}.
*
* It's recommended to increase the playback speed when this method is called.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_FAST_FORWARD
*/
public @ResultCode int onFastForward(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller called {@link MediaController#rewind()}.
*
* It's recommended to decrease the playback speed when this method is called.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_REWIND
*/
public @ResultCode int onRewind(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller called {@link MediaController#skipForward()}.
*
* It's recommended to seek forward within the current media item when this method
* is called.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_SKIP_FORWARD
*/
public @ResultCode int onSkipForward(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller called {@link MediaController#skipBackward()}.
*
* It's recommended to seek backward within the current media item when this method
* is called.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_SKIP_BACKWARD
*/
public @ResultCode int onSkipBackward(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when the player state is changed. Used internally for setting the
* {@link MediaSessionService} as foreground/background.
*/
final void onPlayerStateChanged(MediaSession session, @PlayerState int state) {
if (mForegroundServiceEventCallback != null) {
mForegroundServiceEventCallback.onPlayerStateChanged(session, state);
}
}
final void onSessionClosed(MediaSession session) {
if (mForegroundServiceEventCallback != null) {
mForegroundServiceEventCallback.onSessionClosed(session);
}
}
void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
mForegroundServiceEventCallback = callback;
}
abstract static class ForegroundServiceEventCallback {
public void onPlayerStateChanged(MediaSession session, @PlayerState int state) {}
public void onSessionClosed(MediaSession session) {}
}
}
/**
* Builder for {@link MediaSession}.
*
* Any incoming event from the {@link MediaController} will be handled on the callback executor.
* If it's not set, {@link ContextCompat#getMainExecutor(Context)} will be used by default.
*/
public static final class Builder extends BuilderBase
* It's up to the controller's decision to respect or ignore this customization request.
*/
@VersionedParcelize
public static final class CommandButton implements VersionedParcelable {
@ParcelField(1)
SessionCommand mCommand;
@ParcelField(2)
int mIconResId;
@ParcelField(3)
CharSequence mDisplayName;
@ParcelField(4)
Bundle mExtras;
@ParcelField(5)
boolean mEnabled;
/**
* Used for VersionedParcelable
*/
CommandButton() {
}
CommandButton(@Nullable SessionCommand command, int iconResId,
@Nullable CharSequence displayName, Bundle extras, boolean enabled) {
mCommand = command;
mIconResId = iconResId;
mDisplayName = displayName;
mExtras = extras;
mEnabled = enabled;
}
/**
* Get command associated with this button. Can be {@code null} if the button isn't enabled
* and only providing placeholder.
*
* @return command or {@code null}
*/
public @Nullable SessionCommand getCommand() {
return mCommand;
}
/**
* Resource id of the button in this package. Can be {@code 0} if the command is predefined
* and custom icon isn't needed.
*
* @return resource id of the icon. Can be {@code 0}.
*/
public int getIconResId() {
return mIconResId;
}
/**
* Display name of the button. Can be {@code null} or empty if the command is predefined
* and custom name isn't needed.
*
* @return custom display name. Can be {@code null} or empty.
*/
public @Nullable CharSequence getDisplayName() {
return mDisplayName;
}
/**
* Extra information of the button. It's private information between session and controller.
*
* @return
*/
public @Nullable Bundle getExtras() {
return mExtras;
}
/**
* Return whether it's enabled.
*
* @return {@code true} if enabled. {@code false} otherwise.
*/
public boolean isEnabled() {
return mEnabled;
}
/**
* Builder for {@link CommandButton}.
*/
public static final class Builder {
private SessionCommand mCommand;
private int mIconResId;
private CharSequence mDisplayName;
private Bundle mExtras;
private boolean mEnabled;
/**
* Sets the {@link SessionCommand} that would be sent to the session when the button
* is clicked.
*
* @param command session command
*/
public @NonNull Builder setCommand(@Nullable SessionCommand command) {
mCommand = command;
return this;
}
/**
* Sets the bitmap-type (e.g. PNG) icon resource id of the button.
*
* None bitmap type (e.g. VectorDrawabale) may cause unexpected behavior when it's sent
* to {@link MediaController} app, so please avoid using it especially for the older
* platform (API < 21).
*
* @param resId resource id of the button
*/
public @NonNull Builder setIconResId(int resId) {
mIconResId = resId;
return this;
}
/**
* Sets the display name of the button.
*
* @param displayName display name of the button
*/
public @NonNull Builder setDisplayName(@Nullable CharSequence displayName) {
mDisplayName = displayName;
return this;
}
/**
* Sets whether the button is enabled. Can be {@code false} to indicate that the button
* should be shown but isn't clickable.
*
* @param enabled {@code true} if the button is enabled and ready.
* {@code false} otherwise.
*/
public @NonNull Builder setEnabled(boolean enabled) {
mEnabled = enabled;
return this;
}
/**
* Sets the extras of the button.
*
* @param extras extras information of the button
*/
public @NonNull Builder setExtras(@Nullable Bundle extras) {
mExtras = extras;
return this;
}
/**
* Builds the {@link CommandButton}.
*
* @return a new {@link CommandButton}
*/
public @NonNull CommandButton build() {
return new CommandButton(mCommand, mIconResId, mDisplayName, mExtras, mEnabled);
}
}
}
// TODO: Drop 'Cb' from the name.
abstract static class ControllerCb {
abstract void onPlayerResult(int seq, PlayerResult result) throws RemoteException;
abstract void onSessionResult(int seq, SessionResult result) throws RemoteException;
abstract void onLibraryResult(int seq, LibraryResult result) throws RemoteException;
// Mostly matched with the methods in MediaController.ControllerCallback
abstract void setCustomLayout(int seq, @NonNull List
* APIs here should be package private, but should have documentations for developers.
* Otherwise, javadoc will generate documentation with the generic types such as follows.
*
* This class is hidden to prevent from generating test stub, which fails with
* 'unexpected bound' because it tries to auto generate stub class as follows.
*
* Use this if and only if your app supports multiple playback at the same time and also
* wants to provide external apps to have finer controls of them.
*
* @param id id of the session. Must be unique per package.
* @return
*/
// Note: This ID is not visible to the controllers. ID is introduced in order to prevent
// apps from creating multiple sessions without any clear reasons. If they create two
// sessions with the same ID in a process, then an IllegalStateException will be thrown.
@NonNull U setId(@NonNull String id) {
if (id == null) {
throw new NullPointerException("id shouldn't be null");
}
mId = id;
return (U) this;
}
/**
* Set callback for the session.
*
* @param executor callback executor
* @param callback session callback.
* @return
*/
@NonNull U setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
if (executor == null) {
throw new NullPointerException("executor shouldn't be null");
}
if (callback == null) {
throw new NullPointerException("callback shouldn't be null");
}
mCallbackExecutor = executor;
mCallback = callback;
return (U) this;
}
/**
* Set extras for the session token. If not set, {@link SessionToken#getExtras()}
* will return {@link Bundle#EMPTY}.
*
* @return The Builder to allow chaining
* @see SessionToken#getExtras()
*/
@NonNull
public U setExtras(@NonNull Bundle extras) {
if (extras == null) {
throw new NullPointerException("extras shouldn't be null");
}
mExtras = extras;
return (U) this;
}
/**
* Build {@link MediaSession}.
*
* @return a new session
* @throws IllegalStateException if the session with the same id is already exists for the
* package.
*/
@NonNull abstract T build();
}
}
layout[i]
means a CommandButton at index i in the given list
*
*
*
* Controller UX layout Layout example
* Row with 3 icons
* layout[1]
layout[0]
layout[2]
* Row with 5 icons
* layout[3]
layout[1]
layout[0]
* layout[2]
layout[4]
* Row with 5 icons and an overflow icon, and another expandable row with 5
* extra icons
* layout[3]
layout[1]
layout[0]
* layout[2]
layout[4]
* layout[3]
layout[1]
layout[0]
* layout[2]
layout[4]
*
*
*
* Override this to translate incoming {@code mediaId} to a {@link MediaItem} to be
* understood by your player. For example, a player may only understand
* {@link androidx.media2.common.FileMediaItem}, {@link UriMediaItem},
* and {@link CallbackMediaItem}. Check the documentation of the player that you're using.
*
*
*
* Controller command Behavior when {@code null} is returned
* addPlaylistItem Ignore
* replacePlaylistItem Ignore
* setPlaylist
* Ignore {@code null} items, and build a list with non-{@code null} items. Call
* {@link SessionPlayer#setPlaylist(List, MediaMetadata)} with the list
* setMediaItem Ignore U extends BuilderBase
* abstract static class BuilderBase<
* T extends MediaSession,
* U extends MediaSession.BuilderBase<
* T, U, C extends MediaSession.SessionCallback>, C>
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
abstract static class BuilderBase