diff options
Diffstat (limited to 'android/media/MediaSession2.java')
-rw-r--r-- | android/media/MediaSession2.java | 1525 |
1 files changed, 848 insertions, 677 deletions
diff --git a/android/media/MediaSession2.java b/android/media/MediaSession2.java index 0e90040a..2b3c2b4c 100644 --- a/android/media/MediaSession2.java +++ b/android/media/MediaSession2.java @@ -16,36 +16,37 @@ package android.media; +import static android.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN; + import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.Activity; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.media.MediaPlayerBase.PlaybackListener; -import android.media.session.MediaSession; -import android.media.session.MediaSession.Callback; -import android.media.session.PlaybackState; +import android.media.MediaPlayerBase.BuffState; +import android.media.MediaPlayerBase.PlayerState; +import android.media.MediaPlaylistAgent.RepeatMode; +import android.media.MediaPlaylistAgent.ShuffleMode; import android.media.update.ApiLoader; import android.media.update.MediaSession2Provider; +import android.media.update.MediaSession2Provider.BuilderBaseProvider; +import android.media.update.MediaSession2Provider.CommandButtonProvider; import android.media.update.MediaSession2Provider.ControllerInfoProvider; +import android.media.update.ProviderCreator; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; -import android.os.Parcelable; +import android.os.IInterface; import android.os.ResultReceiver; -import android.text.TextUtils; -import android.util.ArraySet; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; /** + * @hide * 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. * <ul> @@ -59,7 +60,7 @@ import java.util.concurrent.Executor; * sessions can be created to provide finer grain controls of media. * <p> * If you want to support background playback, {@link MediaSessionService2} is preferred - * instead. With it, your playback can be revived even after you've finished playback. See + * instead. With it, your playback can be revived even after playback is finished. See * {@link MediaSessionService2} for details. * <p> * A session can be obtained by {@link Builder}. The owner of the session may pass its session token @@ -67,7 +68,8 @@ import java.util.concurrent.Executor; * session. * <p> * 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 #setPlayer(MediaPlayerBase)}. + * the the underlying media player set by {@link Builder} or + * {@link #updatePlayer}. * <p> * When an app is finished performing playback it must call {@link #close()} to clean up the session * and notify any controllers. @@ -75,228 +77,110 @@ import java.util.concurrent.Executor; * {@link MediaSession2} objects should be used on the thread on the looper. * * @see MediaSessionService2 - * @hide */ -// TODO(jaewan): Unhide -// TODO(jaewan): Revisit comments. Currently it's borrowed from the MediaSession. -// TODO(jaewan): Should we support thread safe? It may cause tricky issue such as b/63797089 -// TODO(jaewan): Should we make APIs for MediaSessionService2 public? It's helpful for -// developers that doesn't want to override from Browser, but user may not use this -// correctly. public class MediaSession2 implements AutoCloseable { private final MediaSession2Provider mProvider; - // Note: Do not define IntDef because subclass can add more command code on top of these. - // TODO(jaewan): Shouldn't we pull out? - public static final int COMMAND_CODE_CUSTOM = 0; - public static final int COMMAND_CODE_PLAYBACK_START = 1; - public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2; - public static final int COMMAND_CODE_PLAYBACK_STOP = 3; - public static final int COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM = 4; - public static final int COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM = 5; - public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6; - public static final int COMMAND_CODE_PLAYBACK_FAST_FORWARD = 7; - public static final int COMMAND_CODE_PLAYBACK_REWIND = 8; - public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9; - public static final int COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM = 10; - - public static final int COMMAND_CODE_PLAYLIST_GET = 11; - public static final int COMMAND_CODE_PLAYLIST_ADD = 12; - public static final int COMMAND_CODE_PLAYLIST_REMOVE = 13; - - public static final int COMMAND_CODE_PLAY_FROM_MEDIA_ID = 14; - public static final int COMMAND_CODE_PLAY_FROM_URI = 15; - public static final int COMMAND_CODE_PLAY_FROM_SEARCH = 16; - - public static final int COMMAND_CODE_PREPARE_FROM_MEDIA_ID = 17; - public static final int COMMAND_CODE_PREPARE_FROM_URI = 18; - public static final int COMMAND_CODE_PREPARE_FROM_SEARCH = 19; - /** - * Define a command that a {@link MediaController2} can send to a {@link MediaSession2}. - * <p> - * If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command. - * If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and - * {@link #getCustomCommand()} shouldn't be {@code null}. + * @hide */ - // TODO(jaewan): Move this into the updatable. - public static final class Command { - private static final String KEY_COMMAND_CODE - = "android.media.media_session2.command.command_code"; - private static final String KEY_COMMAND_CUSTOM_COMMAND - = "android.media.media_session2.command.custom_command"; - private static final String KEY_COMMAND_EXTRA - = "android.media.media_session2.command.extra"; - - private final int mCommandCode; - // Nonnull if it's custom command - private final String mCustomCommand; - private final Bundle mExtra; - - public Command(int commandCode) { - mCommandCode = commandCode; - mCustomCommand = null; - mExtra = null; - } + @IntDef({ERROR_CODE_UNKNOWN_ERROR, ERROR_CODE_APP_ERROR, ERROR_CODE_NOT_SUPPORTED, + ERROR_CODE_AUTHENTICATION_EXPIRED, ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, + ERROR_CODE_CONCURRENT_STREAM_LIMIT, ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, + ERROR_CODE_NOT_AVAILABLE_IN_REGION, ERROR_CODE_CONTENT_ALREADY_PLAYING, + ERROR_CODE_SKIP_LIMIT_REACHED, ERROR_CODE_ACTION_ABORTED, ERROR_CODE_END_OF_QUEUE, + ERROR_CODE_SETUP_REQUIRED}) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCode {} - public Command(@NonNull String action, @Nullable Bundle extra) { - if (action == null) { - throw new IllegalArgumentException("action shouldn't be null"); - } - mCommandCode = COMMAND_CODE_CUSTOM; - mCustomCommand = action; - mExtra = extra; - } - - public int getCommandCode() { - return mCommandCode; - } - - public @Nullable String getCustomCommand() { - return mCustomCommand; - } - - public @Nullable Bundle getExtra() { - return mExtra; - } - - /** - * @return a new Bundle instance from the Command - * @hide - */ - public Bundle toBundle() { - Bundle bundle = new Bundle(); - bundle.putInt(KEY_COMMAND_CODE, mCommandCode); - bundle.putString(KEY_COMMAND_CUSTOM_COMMAND, mCustomCommand); - bundle.putBundle(KEY_COMMAND_EXTRA, mExtra); - return bundle; - } + /** + * This is the default error code and indicates that none of the other error codes applies. + */ + public static final int ERROR_CODE_UNKNOWN_ERROR = 0; - /** - * @return a new Command instance from the Bundle - * @hide - */ - public static Command fromBundle(Bundle command) { - int code = command.getInt(KEY_COMMAND_CODE); - if (code != COMMAND_CODE_CUSTOM) { - return new Command(code); - } else { - String customCommand = command.getString(KEY_COMMAND_CUSTOM_COMMAND); - if (customCommand == null) { - return null; - } - return new Command(customCommand, command.getBundle(KEY_COMMAND_EXTRA)); - } - } + /** + * Error code when the application state is invalid to fulfill the request. + */ + public static final int ERROR_CODE_APP_ERROR = 1; - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Command)) { - return false; - } - Command other = (Command) obj; - // TODO(jaewan): Should we also compare contents in bundle? - // It may not be possible if the bundle contains private class. - return mCommandCode == other.mCommandCode - && TextUtils.equals(mCustomCommand, other.mCustomCommand); - } + /** + * Error code when the request is not supported by the application. + */ + public static final int ERROR_CODE_NOT_SUPPORTED = 2; - @Override - public int hashCode() { - final int prime = 31; - return ((mCustomCommand != null) ? mCustomCommand.hashCode() : 0) * prime + mCommandCode; - } - } + /** + * Error code when the request cannot be performed because authentication has expired. + */ + public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = 3; /** - * Represent set of {@link Command}. + * Error code when a premium account is required for the request to succeed. */ - // TODO(jaewan): Move this to updatable - public static class CommandGroup { - private static final String KEY_COMMANDS = - "android.media.mediasession2.commandgroup.commands"; - private ArraySet<Command> mCommands = new ArraySet<>(); + public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = 4; - public CommandGroup() { - } + /** + * Error code when too many concurrent streams are detected. + */ + public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = 5; - public CommandGroup(CommandGroup others) { - mCommands.addAll(others.mCommands); - } + /** + * Error code when the content is blocked due to parental controls. + */ + public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = 6; - public void addCommand(Command command) { - mCommands.add(command); - } + /** + * Error code when the content is blocked due to being regionally unavailable. + */ + public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = 7; - public void addAllPredefinedCommands() { - // TODO(jaewan): Is there any better way than this? - mCommands.add(new Command(COMMAND_CODE_PLAYBACK_START)); - mCommands.add(new Command(COMMAND_CODE_PLAYBACK_PAUSE)); - mCommands.add(new Command(COMMAND_CODE_PLAYBACK_STOP)); - mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM)); - mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM)); - } + /** + * Error code when the requested content is already playing. + */ + public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = 8; - public void removeCommand(Command command) { - mCommands.remove(command); - } + /** + * Error code when the application cannot skip any more songs because skip limit is reached. + */ + public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9; - public boolean hasCommand(Command command) { - return mCommands.contains(command); - } + /** + * Error code when the action is interrupted due to some external event. + */ + public static final int ERROR_CODE_ACTION_ABORTED = 10; - public boolean hasCommand(int code) { - if (code == COMMAND_CODE_CUSTOM) { - throw new IllegalArgumentException("Use hasCommand(Command) for custom command"); - } - for (int i = 0; i < mCommands.size(); i++) { - if (mCommands.valueAt(i).getCommandCode() == code) { - return true; - } - } - return false; - } + /** + * Error code when the playback navigation (previous, next) is not possible because the queue + * was exhausted. + */ + public static final int ERROR_CODE_END_OF_QUEUE = 11; - /** - * @return new bundle from the CommandGroup - * @hide - */ - public Bundle toBundle() { - ArrayList<Bundle> list = new ArrayList<>(); - for (int i = 0; i < mCommands.size(); i++) { - list.add(mCommands.valueAt(i).toBundle()); - } - Bundle bundle = new Bundle(); - bundle.putParcelableArrayList(KEY_COMMANDS, list); - return bundle; - } + /** + * Error code when the session needs user's manual intervention. + */ + public static final int ERROR_CODE_SETUP_REQUIRED = 12; + /** + * Interface definition of a callback to be invoked when a {@link MediaItem2} in the playlist + * didn't have a {@link DataSourceDesc} but it's needed now for preparing or playing it. + * + * #see #setOnDataSourceMissingHelper + */ + public interface OnDataSourceMissingHelper { /** - * @return new instance of CommandGroup from the bundle - * @hide + * Called when a {@link MediaItem2} in the playlist didn't have a {@link DataSourceDesc} + * but it's needed now for preparing or playing it. Returned data source descriptor will be + * sent to the player directly to prepare or play the contents. + * <p> + * An exception may be thrown if the returned {@link DataSourceDesc} is duplicated in the + * playlist, so items cannot be differentiated. + * + * @param session the session for this event + * @param item media item from the controller + * @return a data source descriptor if the media item. Can be {@code null} if the content + * isn't available. */ - public static @Nullable CommandGroup fromBundle(Bundle commands) { - if (commands == null) { - return null; - } - List<Parcelable> list = commands.getParcelableArrayList(KEY_COMMANDS); - if (list == null) { - return null; - } - CommandGroup commandGroup = new CommandGroup(); - for (int i = 0; i < list.size(); i++) { - Parcelable parcelable = list.get(i); - if (!(parcelable instanceof Bundle)) { - continue; - } - Bundle commandBundle = (Bundle) parcelable; - Command command = Command.fromBundle(commandBundle); - if (command != null) { - commandGroup.addCommand(command); - } - } - return commandGroup; - } + @Nullable DataSourceDesc onDataSourceMissing(@NonNull MediaSession2 session, + @NonNull MediaItem2 item); } /** @@ -305,21 +189,23 @@ public class MediaSession2 implements AutoCloseable { * If it's not set, the session will accept all controllers and all incoming commands by * default. */ - // TODO(jaewan): Can we move this inside of the updatable for default implementation. - public static class SessionCallback { + // TODO(jaewan): Move this to updatable for default implementation (b/74091963) + public static abstract class SessionCallback { /** * Called when a controller is created for this session. Return allowed commands for * controller. By default it allows all connection requests and commands. * <p> * You can reject the connection by return {@code null}. In that case, controller receives - * {@link MediaController2.ControllerCallback#onDisconnected()} and cannot be usable. + * {@link MediaController2.ControllerCallback#onDisconnected(MediaController2)} and cannot + * be usable. * + * @param session the session for this event * @param controller controller information. - * @return allowed commands. Can be {@code null} to reject coonnection. + * @return allowed commands. Can be {@code null} to reject connection. */ - // TODO(jaewan): Change return type. Once we do, null is for reject. - public @Nullable CommandGroup onConnect(@NonNull ControllerInfo controller) { - CommandGroup commands = new CommandGroup(); + public @Nullable SessionCommandGroup2 onConnect(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller) { + SessionCommandGroup2 commands = new SessionCommandGroup2(); commands.addAllPredefinedCommands(); return commands; } @@ -327,213 +213,395 @@ public class MediaSession2 implements AutoCloseable { /** * Called when a controller is disconnected * + * @param session the session for this event * @param controller controller information */ - public void onDisconnected(@NonNull ControllerInfo controller) { } + public void onDisconnected(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller) { } /** - * Called when a controller sent a command to the session, and the command will be sent to - * the player directly unless you reject the request by {@code false}. + * Called when a controller sent a command that will be sent directly to the player. Return + * {@code false} here to reject the request and stop sending command to the player. * + * @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 true} if you want to accept incoming command. {@code false} otherwise. + * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PLAY + * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PAUSE + * @see SessionCommand2#COMMAND_CODE_PLAYBACK_STOP + * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_NEXT_ITEM + * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_PREV_ITEM + * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PREPARE + * @see SessionCommand2#COMMAND_CODE_SESSION_FAST_FORWARD + * @see SessionCommand2#COMMAND_CODE_SESSION_REWIND + * @see SessionCommand2#COMMAND_CODE_PLAYBACK_SEEK_TO + * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM + * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM + * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REMOVE_ITEM + * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST + * @see SessionCommand2#COMMAND_CODE_SET_VOLUME */ - // TODO(jaewan): Add more documentations (or make it clear) which commands can be filtered - // with this. - public boolean onCommandRequest(@NonNull ControllerInfo controller, - @NonNull Command command) { + public boolean onCommandRequest(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller, @NonNull SessionCommand2 command) { return true; } /** - * Called when a controller set rating on the currently playing contents. + * Called when a controller set rating of a media item through + * {@link MediaController2#setRating(String, Rating2)}. + * <p> + * To allow setting user rating for a {@link MediaItem2}, the media item's metadata + * should have {@link Rating2} 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 + * @param session the session for this event + * @param controller controller information + * @param mediaId media id from the controller + * @param rating new rating from the controller */ - public void onSetRating(@NonNull ControllerInfo controller, @NonNull Rating2 rating) { } + public void onSetRating(@NonNull MediaSession2 session, @NonNull ControllerInfo controller, + @NonNull String mediaId, @NonNull Rating2 rating) { } /** - * Called when a controller sent a custom command. + * Called when a controller sent a custom command through + * {@link MediaController2#sendCustomCommand(SessionCommand2, Bundle, ResultReceiver)}. * + * @param session the session for this event * @param controller controller information * @param customCommand custom command. * @param args optional arguments * @param cb optional result receiver */ - public void onCustomCommand(@NonNull ControllerInfo controller, - @NonNull Command customCommand, @Nullable Bundle args, - @Nullable ResultReceiver cb) { } + public void onCustomCommand(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller, @NonNull SessionCommand2 customCommand, + @Nullable Bundle args, @Nullable ResultReceiver cb) { } + + /** + * Called when a controller requested to play a specific mediaId through + * {@link MediaController2#playFromMediaId(String, Bundle)}. + * + * @param session the session for this event + * @param controller controller information + * @param mediaId media id + * @param extras optional extra bundle + * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID + */ + public void onPlayFromMediaId(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller, @NonNull String mediaId, + @Nullable Bundle extras) { } + + /** + * Called when a controller requested to begin playback from a search query through + * {@link MediaController2#playFromSearch(String, Bundle)} + * <p> + * An empty query indicates that the app may play any music. The implementation should + * attempt to make a smart choice about what to play. + * + * @param session the session for this event + * @param controller controller information + * @param query query string. Can be empty to indicate any suggested media + * @param extras optional extra bundle + * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_SEARCH + */ + public void onPlayFromSearch(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller, @NonNull String query, + @Nullable Bundle extras) { } /** - * Override to handle requests to prepare for playing a specific mediaId. + * Called when a controller requested to play a specific media item represented by a URI + * through {@link MediaController2#playFromUri(Uri, Bundle)} + * + * @param session the session for this event + * @param controller controller information + * @param uri uri + * @param extras optional extra bundle + * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_URI + */ + public void onPlayFromUri(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller, @NonNull Uri uri, + @Nullable Bundle extras) { } + + /** + * Called when a controller requested to prepare for playing a specific mediaId through + * {@link MediaController2#prepareFromMediaId(String, Bundle)}. + * <p> * During the preparation, a session should not hold audio focus in order to allow other * sessions play seamlessly. The state of playback should be updated to - * {@link PlaybackState#STATE_PAUSED} after the preparation is done. + * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done. * <p> * The playback of the prepared content should start in the later calls of * {@link MediaSession2#play()}. * <p> * Override {@link #onPlayFromMediaId} to handle requests for starting * playback without preparation. + * + * @param session the session for this event + * @param controller controller information + * @param mediaId media id to prepare + * @param extras optional extra bundle + * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID */ - public void onPlayFromMediaId(@NonNull ControllerInfo controller, - @NonNull String mediaId, @Nullable Bundle extras) { } + public void onPrepareFromMediaId(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller, @NonNull String mediaId, + @Nullable Bundle extras) { } /** - * Override to handle requests to prepare playback from a search query. An empty query - * indicates that the app may prepare any music. The implementation should attempt to make a - * smart choice about what to play. During the preparation, a session should not hold audio - * focus in order to allow other sessions play seamlessly. The state of playback should be - * updated to {@link PlaybackState#STATE_PAUSED} after the preparation is done. + * Called when a controller requested to prepare playback from a search query through + * {@link MediaController2#prepareFromSearch(String, Bundle)}. * <p> - * The playback of the prepared content should start in the later calls of - * {@link MediaSession2#play()}. + * An empty query indicates that the app may prepare any music. The implementation should + * attempt to make a smart choice about what to play. + * <p> + * The state of playback should be updated to {@link MediaPlayerBase#PLAYER_STATE_PAUSED} + * after the preparation is done. The playback of the prepared content should start in the + * later calls of {@link MediaSession2#play()}. * <p> * Override {@link #onPlayFromSearch} to handle requests for starting playback without * preparation. + * + * @param session the session for this event + * @param controller controller information + * @param query query string. Can be empty to indicate any suggested media + * @param extras optional extra bundle + * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH */ - public void onPlayFromSearch(@NonNull ControllerInfo controller, - @NonNull String query, @Nullable Bundle extras) { } + public void onPrepareFromSearch(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller, @NonNull String query, + @Nullable Bundle extras) { } /** - * Override to handle requests to prepare a specific media item represented by a URI. + * Called when a controller requested to prepare a specific media item represented by a URI + * through {@link MediaController2#prepareFromUri(Uri, Bundle)}. + * <p> * During the preparation, a session should not hold audio focus in order to allow * other sessions play seamlessly. The state of playback should be updated to - * {@link PlaybackState#STATE_PAUSED} after the preparation is done. + * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done. * <p> * The playback of the prepared content should start in the later calls of * {@link MediaSession2#play()}. * <p> * 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 SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_URI */ - public void onPlayFromUri(@NonNull ControllerInfo controller, - @NonNull String uri, @Nullable Bundle extras) { } + public void onPrepareFromUri(@NonNull MediaSession2 session, + @NonNull ControllerInfo controller, @NonNull Uri uri, @Nullable Bundle extras) { } /** - * Override to handle requests to play a specific mediaId. + * Called when a controller called {@link MediaController2#fastForward()} + * + * @param session the session for this event */ - public void onPrepareFromMediaId(@NonNull ControllerInfo controller, - @NonNull String mediaId, @Nullable Bundle extras) { } + public void onFastForward(@NonNull MediaSession2 session) { } /** - * Override to handle requests to begin playback from a search query. An - * empty query indicates that the app may play any music. The - * implementation should attempt to make a smart choice about what to - * play. + * Called when a controller called {@link MediaController2#rewind()} + * + * @param session the session for this event + */ + public void onRewind(@NonNull MediaSession2 session) { } + + /** + * Called when the player's current playing item is changed + * <p> + * When it's called, you should invalidate previous playback information and wait for later + * callbacks. + * + * @param session the controller for this event + * @param player the player for this event + * @param item new item */ - public void onPrepareFromSearch(@NonNull ControllerInfo controller, - @NonNull String query, @Nullable Bundle extras) { } + // TODO(jaewan): Use this (b/74316764) + public void onCurrentMediaItemChanged(@NonNull MediaSession2 session, + @NonNull MediaPlayerBase player, @NonNull MediaItem2 item) { } /** - * Override to handle requests to play a specific media item represented by a URI. + * Called when the player is <i>prepared</i>, i.e. it is ready to play the content + * referenced by the given data source. + * @param session the session for this event + * @param player the player for this event + * @param item the media item for which buffering is happening */ - public void prepareFromUri(@NonNull ControllerInfo controller, - @NonNull Uri uri, @Nullable Bundle extras) { } + public void onMediaPrepared(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player, + @NonNull MediaItem2 item) { } /** - * Called when a controller wants to add a {@link MediaItem2} at the specified position - * in the play queue. + * Called to indicate that the state of the player has changed. + * See {@link MediaPlayerBase#getPlayerState()} for polling the player state. + * @param session the session for this event + * @param player the player for this event + * @param state the new state of the player. + */ + public void onPlayerStateChanged(@NonNull MediaSession2 session, + @NonNull MediaPlayerBase player, @PlayerState int state) { } + + /** + * Called to report buffering events for a data source. + * + * @param session the session for this event + * @param player the player for this event + * @param item the media item for which buffering is happening. + * @param state the new buffering state. + */ + public void onBufferingStateChanged(@NonNull MediaSession2 session, + @NonNull MediaPlayerBase player, @NonNull MediaItem2 item, @BuffState int state) { } + + /** + * Called to indicate that the playback speed has changed. + * @param session the session for this event + * @param player the player for this event + * @param speed the new playback speed. + */ + public void onPlaybackSpeedChanged(@NonNull MediaSession2 session, + @NonNull MediaPlayerBase player, float speed) { } + + /** + * Called to indicate that {@link #seekTo(long)} is completed. + * + * @param session the session for this event. + * @param mpb the player that has completed seeking. + * @param position the previous seeking request. + * @see #seekTo(long) + */ + public void onSeekCompleted(@NonNull MediaSession2 session, @NonNull MediaPlayerBase mpb, + long position) { } + + /** + * Called when a playlist is changed from the {@link MediaPlaylistAgent}. * <p> - * The item from the media controller wouldn't have valid data source descriptor because - * it would have been anonymized when it's sent to the remote process. + * This is called when the underlying agent has called + * {@link MediaPlaylistAgent.PlaylistEventCallback#onPlaylistChanged(MediaPlaylistAgent, + * List, MediaMetadata2)}. + * + * @param session the session for this event + * @param playlistAgent playlist agent for this event + * @param list new playlist + * @param metadata new metadata + */ + public void onPlaylistChanged(@NonNull MediaSession2 session, + @NonNull MediaPlaylistAgent playlistAgent, @NonNull List<MediaItem2> list, + @Nullable MediaMetadata2 metadata) { } + + /** + * Called when a playlist metadata is changed. * - * @param item The media item to be inserted. - * @param index The index at which the item is to be inserted. + * @param session the session for this event + * @param playlistAgent playlist agent for this event + * @param metadata new metadata */ - public void onAddPlaylistItem(@NonNull ControllerInfo controller, - @NonNull MediaItem2 item, int index) { } + public void onPlaylistMetadataChanged(@NonNull MediaSession2 session, + @NonNull MediaPlaylistAgent playlistAgent, @Nullable MediaMetadata2 metadata) { } /** - * Called when a controller wants to remove the {@link MediaItem2} + * Called when the shuffle mode is changed. * - * @param item + * @param session the session for this event + * @param playlistAgent playlist agent for this event + * @param shuffleMode repeat mode + * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE + * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL + * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP */ - // Can we do this automatically? - public void onRemovePlaylistItem(@NonNull MediaItem2 item) { } - }; + public void onShuffleModeChanged(@NonNull MediaSession2 session, + @NonNull MediaPlaylistAgent playlistAgent, + @MediaPlaylistAgent.ShuffleMode int shuffleMode) { } + + /** + * Called when the repeat mode is changed. + * + * @param session the session for this event + * @param playlistAgent playlist agent for this event + * @param repeatMode repeat mode + * @see MediaPlaylistAgent#REPEAT_MODE_NONE + * @see MediaPlaylistAgent#REPEAT_MODE_ONE + * @see MediaPlaylistAgent#REPEAT_MODE_ALL + * @see MediaPlaylistAgent#REPEAT_MODE_GROUP + */ + public void onRepeatModeChanged(@NonNull MediaSession2 session, + @NonNull MediaPlaylistAgent playlistAgent, + @MediaPlaylistAgent.RepeatMode int repeatMode) { } + } /** - * Base builder class for MediaSession2 and its subclass. - * + * Base builder class for MediaSession2 and its subclass. Any change in this class should be + * also applied to the subclasses {@link MediaSession2.Builder} and + * {@link MediaLibraryService2.MediaLibrarySession.Builder}. + * <p> + * APIs here should be package private, but should have documentations for developers. + * Otherwise, javadoc will generate documentation with the generic types such as follows. + * <pre>U extends BuilderBase<T, U, C> setSessionCallback(Executor executor, C callback)</pre> + * <p> + * 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. + * <pre>abstract static class BuilderBase< + * T extends android.media.MediaSession2, + * U extends android.media.MediaSession2.BuilderBase< + * T, U, C extends android.media.MediaSession2.SessionCallback>, C></pre> * @hide */ static abstract class BuilderBase - <T extends MediaSession2.BuilderBase<T, C>, C extends SessionCallback> { - final Context mContext; - final MediaPlayerBase mPlayer; - String mId; - Executor mCallbackExecutor; - C mCallback; - VolumeProvider mVolumeProvider; - int mRatingType; - PendingIntent mSessionActivity; + <T extends MediaSession2, U extends BuilderBase<T, U, C>, C extends SessionCallback> { + private final BuilderBaseProvider<T, C> mProvider; + + BuilderBase(ProviderCreator<BuilderBase<T, U, C>, BuilderBaseProvider<T, C>> creator) { + mProvider = creator.createProvider(this); + } /** - * Constructor. + * Sets the underlying {@link MediaPlayerBase} for this session to dispatch incoming event + * to. * - * @param context a context - * @param player a player to handle incoming command from any controller. - * @throws IllegalArgumentException if any parameter is null, or the player is a - * {@link MediaSession2} or {@link MediaController2}. + * @param player a {@link MediaPlayerBase} that handles actual media playback in your app. */ - // TODO(jaewan): Also need executor - public BuilderBase(@NonNull Context context, @NonNull MediaPlayerBase player) { - if (context == null) { - throw new IllegalArgumentException("context shouldn't be null"); - } - if (player == null) { - throw new IllegalArgumentException("player shouldn't be null"); - } - mContext = context; - mPlayer = player; - // Ensure non-null - mId = ""; + @NonNull U setPlayer(@NonNull MediaPlayerBase player) { + mProvider.setPlayer_impl(player); + return (U) this; } /** - * Set volume provider to configure this session to use remote volume handling. - * This must be called to receive volume button events, otherwise the system - * will adjust the appropriate stream volume for this session's player. + * Sets the {@link MediaPlaylistAgent} for this session to manages playlist of the + * underlying {@link MediaPlayerBase}. The playlist agent should manage + * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}. * <p> - * Set {@code null} to reset. + * If the {@link MediaPlaylistAgent} isn't set, session will create the default playlist + * agent. * - * @param volumeProvider The provider that will handle volume changes. Can be {@code null} + * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the + * {@code player} */ - public T setVolumeProvider(@Nullable VolumeProvider volumeProvider) { - mVolumeProvider = volumeProvider; - return (T) this; + U setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) { + mProvider.setPlaylistAgent_impl(playlistAgent); + return (U) this; } /** - * Set the style of rating used by this session. Apps trying to set the - * rating should use this style. Must be one of the following: - * <ul> - * <li>{@link Rating2#RATING_NONE}</li> - * <li>{@link Rating2#RATING_3_STARS}</li> - * <li>{@link Rating2#RATING_4_STARS}</li> - * <li>{@link Rating2#RATING_5_STARS}</li> - * <li>{@link Rating2#RATING_HEART}</li> - * <li>{@link Rating2#RATING_PERCENTAGE}</li> - * <li>{@link Rating2#RATING_THUMB_UP_DOWN}</li> - * </ul> + * Sets the {@link VolumeProvider2} for this session to handle volume events. If not set, + * system will adjust the appropriate stream volume for this session's player. + * + * @param volumeProvider The provider that will receive volume button events. */ - public T setRatingType(@Rating2.Style int type) { - mRatingType = type; - return (T) this; + @NonNull U setVolumeProvider(@Nullable VolumeProvider2 volumeProvider) { + mProvider.setVolumeProvider_impl(volumeProvider); + return (U) this; } /** * Set an intent for launching UI for this Session. This can be used as a * quick link to an ongoing media screen. The intent should be for an - * activity that may be started using {@link Activity#startActivity(Intent)}. + * activity that may be started using {@link Context#startActivity(Intent)}. * * @param pi The intent to launch to show UI for this session. */ - public T setSessionActivity(@Nullable PendingIntent pi) { - mSessionActivity = pi; - return (T) this; + @NonNull U setSessionActivity(@Nullable PendingIntent pi) { + mProvider.setSessionActivity_impl(pi); + return (U) this; } /** @@ -546,32 +614,22 @@ public class MediaSession2 implements AutoCloseable { * @throws IllegalArgumentException if id is {@code null} * @return */ - public T setId(@NonNull String id) { - if (id == null) { - throw new IllegalArgumentException("id shouldn't be null"); - } - mId = id; - return (T) this; + @NonNull U setId(@NonNull String id) { + mProvider.setId_impl(id); + return (U) this; } /** - * Set {@link SessionCallback}. + * Set callback for the session. * * @param executor callback executor * @param callback session callback. * @return */ - public T setSessionCallback(@NonNull @CallbackExecutor Executor executor, + @NonNull U setSessionCallback(@NonNull @CallbackExecutor Executor executor, @NonNull C callback) { - if (executor == null) { - throw new IllegalArgumentException("executor shouldn't be null"); - } - if (callback == null) { - throw new IllegalArgumentException("callback shouldn't be null"); - } - mCallbackExecutor = executor; - mCallback = callback; - return (T) this; + mProvider.setSessionCallback_impl(executor, callback); + return (U) this; } /** @@ -581,7 +639,9 @@ public class MediaSession2 implements AutoCloseable { * @throws IllegalStateException if the session with the same id is already exists for the * package. */ - public abstract MediaSession2 build() throws IllegalStateException; + @NonNull T build() { + return mProvider.build_impl(); + } } /** @@ -590,47 +650,70 @@ public class MediaSession2 implements AutoCloseable { * Any incoming event from the {@link MediaController2} will be handled on the thread * that created session with the {@link Builder#build()}. */ - // TODO(jaewan): Move this to updatable - // TODO(jaewan): Add setRatingType() - // TODO(jaewan): Add setSessionActivity() - public static final class Builder extends BuilderBase<Builder, SessionCallback> { - public Builder(Context context, @NonNull MediaPlayerBase player) { - super(context, player); + // Override all methods just to show them with the type instead of generics in Javadoc. + // This workarounds javadoc issue described in the MediaSession2.BuilderBase. + public static final class Builder extends BuilderBase<MediaSession2, Builder, SessionCallback> { + public Builder(Context context) { + super((instance) -> ApiLoader.getProvider().createMediaSession2Builder( + context, (Builder) instance)); } @Override - public MediaSession2 build() throws IllegalStateException { - if (mCallback == null) { - mCallback = new SessionCallback(); - } - return new MediaSession2(mContext, mPlayer, mId, mCallbackExecutor, mCallback, - mVolumeProvider, mRatingType, mSessionActivity); + public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) { + return super.setPlayer(player); + } + + @Override + public Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) { + return super.setPlaylistAgent(playlistAgent); + } + + @Override + public @NonNull Builder setVolumeProvider(@Nullable VolumeProvider2 volumeProvider) { + return super.setVolumeProvider(volumeProvider); + } + + @Override + public @NonNull Builder setSessionActivity(@Nullable PendingIntent pi) { + return super.setSessionActivity(pi); + } + + @Override + public @NonNull Builder setId(@NonNull String id) { + return super.setId(id); + } + + @Override + public @NonNull Builder setSessionCallback(@NonNull Executor executor, + @Nullable SessionCallback callback) { + return super.setSessionCallback(executor, callback); + } + + @Override + public @NonNull MediaSession2 build() { + return super.build(); } } /** * Information of a controller. */ - // TODO(jaewan): Move implementation to the updatable. public static final class ControllerInfo { private final ControllerInfoProvider mProvider; /** * @hide */ - // TODO(jaewan): SystemApi - // TODO(jaewan): Also accept componentName to check notificaiton listener. - public ControllerInfo(Context context, int uid, int pid, String packageName, - IMediaSession2Callback callback) { - mProvider = ApiLoader.getProvider(context) - .createMediaSession2ControllerInfoProvider( - this, context, uid, pid, packageName, callback); + public ControllerInfo(@NonNull Context context, int uid, int pid, + @NonNull String packageName, @NonNull IInterface callback) { + mProvider = ApiLoader.getProvider().createMediaSession2ControllerInfo( + context, this, uid, pid, packageName, callback); } /** * @return package name of the controller */ - public String getPackageName() { + public @NonNull String getPackageName() { return mProvider.getPackageName_impl(); } @@ -654,10 +737,8 @@ public class MediaSession2 implements AutoCloseable { /** * @hide - * @return */ - // TODO(jaewan): SystemApi - public ControllerInfoProvider getProvider() { + public @NonNull ControllerInfoProvider getProvider() { return mProvider; } @@ -668,52 +749,28 @@ public class MediaSession2 implements AutoCloseable { @Override public boolean equals(Object obj) { - if (!(obj instanceof ControllerInfo)) { - return false; - } - ControllerInfo other = (ControllerInfo) obj; - return mProvider.equals_impl(other.mProvider); + return mProvider.equals_impl(obj); } @Override public String toString() { - // TODO(jaewan): Move this to updatable. - return "ControllerInfo {pkg=" + getPackageName() + ", uid=" + getUid() + ", trusted=" - + isTrusted() + "}"; + return mProvider.toString_impl(); } } /** - * Button for a {@link Command} that will be shown by the controller. + * Button for a {@link SessionCommand2} that will be shown by the controller. * <p> * It's up to the controller's decision to respect or ignore this customization request. */ - // TODO(jaewan): Move this to updatable. - public static class CommandButton { - private static final String KEY_COMMAND - = "android.media.media_session2.command_button.command"; - private static final String KEY_ICON_RES_ID - = "android.media.media_session2.command_button.icon_res_id"; - private static final String KEY_DISPLAY_NAME - = "android.media.media_session2.command_button.display_name"; - private static final String KEY_EXTRA - = "android.media.media_session2.command_button.extra"; - private static final String KEY_ENABLED - = "android.media.media_session2.command_button.enabled"; - - private Command mCommand; - private int mIconResId; - private String mDisplayName; - private Bundle mExtra; - private boolean mEnabled; - - private CommandButton(@Nullable Command command, int iconResId, - @Nullable String displayName, Bundle extra, boolean enabled) { - mCommand = command; - mIconResId = iconResId; - mDisplayName = displayName; - mExtra = extra; - mEnabled = enabled; + public static final class CommandButton { + private final CommandButtonProvider mProvider; + + /** + * @hide + */ + public CommandButton(CommandButtonProvider provider) { + mProvider = provider; } /** @@ -722,8 +779,9 @@ public class MediaSession2 implements AutoCloseable { * * @return command or {@code null} */ - public @Nullable Command getCommand() { - return mCommand; + public @Nullable + SessionCommand2 getCommand() { + return mProvider.getCommand_impl(); } /** @@ -733,7 +791,7 @@ public class MediaSession2 implements AutoCloseable { * @return resource id of the icon. Can be {@code 0}. */ public int getIconResId() { - return mIconResId; + return mProvider.getIconResId_impl(); } /** @@ -743,7 +801,7 @@ public class MediaSession2 implements AutoCloseable { * @return custom display name. Can be {@code null} or empty. */ public @Nullable String getDisplayName() { - return mDisplayName; + return mProvider.getDisplayName_impl(); } /** @@ -751,8 +809,8 @@ public class MediaSession2 implements AutoCloseable { * * @return */ - public @Nullable Bundle getExtra() { - return mExtra; + public @Nullable Bundle getExtras() { + return mProvider.getExtras_impl(); } /** @@ -761,181 +819,53 @@ public class MediaSession2 implements AutoCloseable { * @return {@code true} if enabled. {@code false} otherwise. */ public boolean isEnabled() { - return mEnabled; + return mProvider.isEnabled_impl(); } /** * @hide */ - // TODO(jaewan): @SystemApi - public @NonNull Bundle toBundle() { - Bundle bundle = new Bundle(); - bundle.putBundle(KEY_COMMAND, mCommand.toBundle()); - bundle.putInt(KEY_ICON_RES_ID, mIconResId); - bundle.putString(KEY_DISPLAY_NAME, mDisplayName); - bundle.putBundle(KEY_EXTRA, mExtra); - bundle.putBoolean(KEY_ENABLED, mEnabled); - return bundle; - } - - /** - * @hide - */ - // TODO(jaewan): @SystemApi - public static @Nullable CommandButton fromBundle(Bundle bundle) { - Builder builder = new Builder(); - builder.setCommand(Command.fromBundle(bundle.getBundle(KEY_COMMAND))); - builder.setIconResId(bundle.getInt(KEY_ICON_RES_ID, 0)); - builder.setDisplayName(bundle.getString(KEY_DISPLAY_NAME)); - builder.setExtra(bundle.getBundle(KEY_EXTRA)); - builder.setEnabled(bundle.getBoolean(KEY_ENABLED)); - try { - return builder.build(); - } catch (IllegalStateException e) { - // Malformed or version mismatch. Return null for now. - return null; - } + public @NonNull CommandButtonProvider getProvider() { + return mProvider; } /** * Builder for {@link CommandButton}. */ - public static class Builder { - private Command mCommand; - private int mIconResId; - private String mDisplayName; - private Bundle mExtra; - private boolean mEnabled; + public static final class Builder { + private final CommandButtonProvider.BuilderProvider mProvider; public Builder() { - mEnabled = true; + mProvider = ApiLoader.getProvider().createMediaSession2CommandButtonBuilder(this); } - public Builder setCommand(Command command) { - mCommand = command; - return this; + public @NonNull Builder setCommand(@Nullable SessionCommand2 command) { + return mProvider.setCommand_impl(command); } - public Builder setIconResId(int resId) { - mIconResId = resId; - return this; + public @NonNull Builder setIconResId(int resId) { + return mProvider.setIconResId_impl(resId); } - public Builder setDisplayName(String displayName) { - mDisplayName = displayName; - return this; + public @NonNull Builder setDisplayName(@Nullable String displayName) { + return mProvider.setDisplayName_impl(displayName); } - public Builder setEnabled(boolean enabled) { - mEnabled = enabled; - return this; + public @NonNull Builder setEnabled(boolean enabled) { + return mProvider.setEnabled_impl(enabled); } - public Builder setExtra(Bundle extra) { - mExtra = extra; - return this; + public @NonNull Builder setExtras(@Nullable Bundle extras) { + return mProvider.setExtras_impl(extras); } - public CommandButton build() { - if (mEnabled && mCommand == null) { - throw new IllegalStateException("Enabled button needs Command" - + " for controller to invoke the command"); - } - if (mCommand != null && mCommand.getCommandCode() == COMMAND_CODE_CUSTOM - && (mIconResId == 0 || TextUtils.isEmpty(mDisplayName))) { - throw new IllegalStateException("Custom commands needs icon and" - + " and name to display"); - } - return new CommandButton(mCommand, mIconResId, mDisplayName, mExtra, mEnabled); + public @NonNull CommandButton build() { + return mProvider.build_impl(); } } } /** - * Parameter for the playlist. - */ - // TODO(jaewan): add fromBundle()/toBundle() - public static class PlaylistParam { - /** - * @hide - */ - @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL, - REPEAT_MODE_GROUP}) - @Retention(RetentionPolicy.SOURCE) - public @interface RepeatMode {} - - /** - * Playback will be stopped at the end of the playing media list. - */ - public static final int REPEAT_MODE_NONE = 0; - - /** - * Playback of the current playing media item will be repeated. - */ - public static final int REPEAT_MODE_ONE = 1; - - /** - * Playing media list will be repeated. - */ - public static final int REPEAT_MODE_ALL = 2; - - /** - * Playback of the playing media group will be repeated. - * A group is a logical block of media items which is specified in the section 5.7 of the - * Bluetooth AVRCP 1.6. - */ - public static final int REPEAT_MODE_GROUP = 3; - - /** - * @hide - */ - @IntDef({SHUFFLE_MODE_NONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP}) - @Retention(RetentionPolicy.SOURCE) - public @interface ShuffleMode {} - - /** - * Media list will be played in order. - */ - public static final int SHUFFLE_MODE_NONE = 0; - - /** - * Media list will be played in shuffled order. - */ - public static final int SHUFFLE_MODE_ALL = 1; - - /** - * Media group will be played in shuffled order. - * A group is a logical block of media items which is specified in the section 5.7 of the - * Bluetooth AVRCP 1.6. - */ - public static final int SHUFFLE_MODE_GROUP = 2; - - private @RepeatMode int mRepeatMode; - private @ShuffleMode int mShuffleMode; - - private MediaMetadata2 mPlaylistMetadata; - - public PlaylistParam(@RepeatMode int repeatMode, @ShuffleMode int shuffleMode, - @Nullable MediaMetadata2 playlistMetadata) { - mRepeatMode = repeatMode; - mShuffleMode = shuffleMode; - mPlaylistMetadata = playlistMetadata; - } - - public @RepeatMode int getRepeatMode() { - return mRepeatMode; - } - - public @ShuffleMode int getShuffleMode() { - return mShuffleMode; - } - - public MediaMetadata2 getPlaylistMetadata() { - return mPlaylistMetadata; - } - } - - /** * Constructor is hidden and apps can only instantiate indirectly through {@link Builder}. * <p> * This intended behavior and here's the reasons. @@ -943,66 +873,45 @@ public class MediaSession2 implements AutoCloseable { * Whenever it happens only one session was properly setup and others were all dummies. * Android framework couldn't find the right session to dispatch media key event. * 2. Simplify session's lifecycle. - * {@link MediaSession} can be available after all of {@link MediaSession#setFlags(int)}, - * {@link MediaSession#setCallback(Callback)}, and - * {@link MediaSession#setActive(boolean)}. It was common for an app to omit one, so - * framework had to add heuristics to figure out if an app is + * {@link android.media.session.MediaSession} is available after all of + * {@link android.media.session.MediaSession#setFlags(int)}, + * {@link android.media.session.MediaSession#setCallback( + * android.media.session.MediaSession.Callback)}, + * and {@link android.media.session.MediaSession#setActive(boolean)}. + * It was common for an app to omit one, so framework had to add heuristics to figure out + * which should be the highest priority for handling media key event. * @hide */ - MediaSession2(Context context, MediaPlayerBase player, String id, Executor callbackExecutor, - SessionCallback callback, VolumeProvider volumeProvider, int ratingType, - PendingIntent sessionActivity) { + public MediaSession2(MediaSession2Provider provider) { super(); - mProvider = createProvider(context, player, id, callbackExecutor, callback, - volumeProvider, ratingType, sessionActivity); - } - - MediaSession2Provider createProvider(Context context, MediaPlayerBase player, String id, - Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider, - int ratingType, PendingIntent sessionActivity) { - return ApiLoader.getProvider(context) - .createMediaSession2(this, context, player, id, callbackExecutor, - callback, volumeProvider, ratingType, sessionActivity); + mProvider = provider; } /** * @hide */ - // TODO(jaewan): SystemApi - public MediaSession2Provider getProvider() { + public @NonNull MediaSession2Provider getProvider() { return mProvider; } /** - * Set the underlying {@link MediaPlayerBase} for this session to dispatch incoming event to. - * Events from the {@link MediaController2} will be sent directly to the underlying - * player on the {@link Handler} where the session is created on. + * Sets the underlying {@link MediaPlayerBase} and {@link MediaPlaylistAgent} for this session + * to dispatch incoming event to. * <p> - * If the new player is successfully set, {@link PlaybackListener} - * will be called to tell the current playback state of the new player. + * When a {@link MediaPlaylistAgent} is specified here, the playlist agent should manage + * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}. * <p> - * You can also specify a volume provider. If so, playback in the player is considered as - * remote playback. - * - * @param player a {@link MediaPlayerBase} that handles actual media playback in your app. - * @throws IllegalArgumentException if the player is {@code null}. - */ - public void setPlayer(@NonNull MediaPlayerBase player) { - mProvider.setPlayer_impl(player); - } - - /** - * Set the underlying {@link MediaPlayerBase} with the volume provider for remote playback. + * If the {@link MediaPlaylistAgent} isn't set, session will recreate the default playlist + * agent. * - * @param player a {@link MediaPlayerBase} that handles actual media playback in your app. - * @param volumeProvider a volume provider - * @see #setPlayer(MediaPlayerBase) - * @see Builder#setVolumeProvider(VolumeProvider) - * @throws IllegalArgumentException if a parameter is {@code null}. + * @param player a {@link MediaPlayerBase} that handles actual media playback in your app + * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the {@code player} + * @param volumeProvider a {@link VolumeProvider2}. If {@code null}, system will adjust the + * appropriate stream volume for this session's player. */ - public void setPlayer(@NonNull MediaPlayerBase player, @NonNull VolumeProvider volumeProvider) - throws IllegalArgumentException { - mProvider.setPlayer_impl(player, volumeProvider); + public void updatePlayer(@NonNull MediaPlayerBase player, + @Nullable MediaPlaylistAgent playlistAgent, @Nullable VolumeProvider2 volumeProvider) { + mProvider.updatePlayer_impl(player, playlistAgent, volumeProvider); } @Override @@ -1013,11 +922,25 @@ public class MediaSession2 implements AutoCloseable { /** * @return player */ - public @Nullable MediaPlayerBase getPlayer() { + public @NonNull MediaPlayerBase getPlayer() { return mProvider.getPlayer_impl(); } /** + * @return playlist agent + */ + public @NonNull MediaPlaylistAgent getPlaylistAgent() { + return mProvider.getPlaylistAgent_impl(); + } + + /** + * @return volume provider + */ + public @Nullable VolumeProvider2 getVolumeProvider() { + return mProvider.getVolumeProvider_impl(); + } + + /** * Returns the {@link SessionToken2} for creating {@link MediaController2}. */ public @NonNull @@ -1030,31 +953,13 @@ public class MediaSession2 implements AutoCloseable { } /** - * Sets the {@link AudioAttributes} to be used during the playback of the video. - * - * @param attributes non-null <code>AudioAttributes</code>. - */ - public void setAudioAttributes(@NonNull AudioAttributes attributes) { - mProvider.setAudioAttributes_impl(attributes); - } - - /** - * Sets which type of audio focus will be requested during the playback, or configures playback - * to not request audio focus. Valid values for focus requests are - * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT}, - * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and - * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use - * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be - * requested when playback starts. You can for instance use this when playing a silent animation - * through this class, and you don't want to affect other audio applications playing in the - * background. + * Set the {@link AudioFocusRequest} to obtain the audio focus * - * @param focusGain the type of audio focus gain that will be requested, or - * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during - * playback. + * @param afr the full request parameters */ - public void setAudioFocusRequest(int focusGain) { - mProvider.setAudioFocusRequest_impl(focusGain); + public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) { + // TODO(jaewan): implement this (b/72529899) + // mProvider.setAudioFocusRequest_impl(focusGain); } /** @@ -1071,10 +976,11 @@ public class MediaSession2 implements AutoCloseable { * expanded row: layout[5] layout[6] layout[7] layout[8] layout[9] * main row: layout[3] layout[1] layout[0] layout[2] layout[4] * <p> - * This API can be called in the {@link SessionCallback#onConnect(ControllerInfo)}. + * This API can be called in the {@link SessionCallback#onConnect( + * MediaSession2, ControllerInfo)}. * * @param controller controller to specify layout. - * @param layout oredered list of layout. + * @param layout ordered list of layout. */ public void setCustomLayout(@NonNull ControllerInfo controller, @NonNull List<CommandButton> layout) { @@ -1088,25 +994,17 @@ public class MediaSession2 implements AutoCloseable { * @param commands new allowed commands */ public void setAllowedCommands(@NonNull ControllerInfo controller, - @NonNull CommandGroup commands) { + @NonNull SessionCommandGroup2 commands) { mProvider.setAllowedCommands_impl(controller, commands); } /** - * Notify changes in metadata of previously set playlist. Controller will get the whole set of - * playlist again. - */ - public void notifyMetadataChanged() { - mProvider.notifyMetadataChanged_impl(); - } - - /** * Send custom command to all connected controllers. * * @param command a command * @param args optional argument */ - public void sendCustomCommand(@NonNull Command command, @Nullable Bundle args) { + public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) { mProvider.sendCustomCommand_impl(command, args); } @@ -1117,107 +1015,380 @@ public class MediaSession2 implements AutoCloseable { * @param args optional argument * @param receiver result receiver for the session */ - public void sendCustomCommand(@NonNull ControllerInfo controller, @NonNull Command command, - @Nullable Bundle args, @Nullable ResultReceiver receiver) { + public void sendCustomCommand(@NonNull ControllerInfo controller, + @NonNull SessionCommand2 command, @Nullable Bundle args, + @Nullable ResultReceiver receiver) { // Equivalent to the MediaController.sendCustomCommand(Action action, ResultReceiver r); mProvider.sendCustomCommand_impl(controller, command, args, receiver); } /** * Play playback + * <p> + * This calls {@link MediaPlayerBase#play()}. */ public void play() { mProvider.play_impl(); } /** - * Pause playback + * Pause playback. + * <p> + * This calls {@link MediaPlayerBase#pause()}. */ public void pause() { mProvider.pause_impl(); } /** - * Stop playback + * Stop playback, and reset the player to the initial state. + * <p> + * This calls {@link MediaPlayerBase#reset()}. */ public void stop() { mProvider.stop_impl(); } /** - * Rewind playback + * Request that the player prepare its playback. In other words, other sessions can continue + * to play during the preparation of this session. This method can be used to speed up the + * start of the playback. Once the preparation is done, the session will change its playback + * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called + * to start playback. + * <p> + * This calls {@link MediaPlayerBase#reset()}. */ - public void skipToPrevious() { - mProvider.skipToPrevious_impl(); + public void prepare() { + mProvider.prepare_impl(); } /** - * Rewind playback + * Move to a new location in the media stream. + * + * @param pos Position to move to, in milliseconds. */ - public void skipToNext() { - mProvider.skipToNext_impl(); + public void seekTo(long pos) { + mProvider.seekTo_impl(pos); } /** - * Request that the player prepare its playback. In other words, other sessions can continue - * to play during the preparation of this session. This method can be used to speed up the - * start of the playback. Once the preparation is done, the session will change its playback - * state to {@link PlaybackState#STATE_PAUSED}. Afterwards, {@link #play} can be called to - * start playback. + * @hide */ - public void prepare() { - mProvider.prepare_impl(); + public void skipForward() { + // To match with KEYCODE_MEDIA_SKIP_FORWARD } /** - * Start fast forwarding. If playback is already fast forwarding this may increase the rate. + * @hide */ - public void fastForward() { - mProvider.fastForward_impl(); + public void skipBackward() { + // To match with KEYCODE_MEDIA_SKIP_BACKWARD } /** - * Start rewinding. If playback is already rewinding this may increase the rate. + * Notify errors to the connected controllers + * + * @param errorCode error code + * @param extras extras */ - public void rewind() { - mProvider.rewind_impl(); + public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) { + mProvider.notifyError_impl(errorCode, extras); } /** - * Move to a new location in the media stream. + * Gets the current player state. * - * @param pos Position to move to, in milliseconds. + * @return the current player state */ - public void seekTo(long pos) { - mProvider.seekTo_impl(pos); + public @PlayerState int getPlayerState() { + return mProvider.getPlayerState_impl(); } /** - * Sets the index of current DataSourceDesc in the play list to be played. + * Gets the current position. * - * @param index the index of DataSourceDesc in the play list you want to play - * @throws IllegalArgumentException if the play list is null - * @throws NullPointerException if index is outside play list range + * @return the current playback position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME} if + * unknown. */ - public void setCurrentPlaylistItem(int index) { - mProvider.setCurrentPlaylistItem_impl(index); + public long getCurrentPosition() { + return mProvider.getCurrentPosition_impl(); } /** - * @hide + * Gets the buffered position, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown. + * + * @return the buffered position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}. */ - public void skipForward() { - // To match with KEYCODE_MEDIA_SKIP_FORWARD + public long getBufferedPosition() { + return mProvider.getBufferedPosition_impl(); } /** - * @hide + * Gets the current buffering state of the player. + * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already + * buffered. + * + * @return the buffering state. */ - public void skipBackward() { - // To match with KEYCODE_MEDIA_SKIP_BACKWARD + public @BuffState int getBufferingState() { + // TODO(jaewan): Implement this + return BUFFERING_STATE_UNKNOWN; + } + + /** + * Get the playback speed. + * + * @return speed + */ + public float getPlaybackSpeed() { + // TODO(jaewan): implement this (b/74093080) + return -1; } - public void setPlaylist(@NonNull List<MediaItem2> playlist, @NonNull PlaylistParam param) { - mProvider.setPlaylist_impl(playlist, param); + /** + * Set the playback speed. + */ + public void setPlaybackSpeed(float speed) { + // TODO(jaewan): implement this (b/74093080) + } + + /** + * Sets the data source missing helper. Helper will be used to provide default implementation of + * {@link MediaPlaylistAgent} when it isn't set by developer. + * <p> + * Default implementation of the {@link MediaPlaylistAgent} will call helper when a + * {@link MediaItem2} in the playlist doesn't have a {@link DataSourceDesc}. This may happen + * when + * <ul> + * <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't + * have {@link DataSourceDesc}</li> + * <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted + * by {@link SessionCallback#onCommandRequest( + * MediaSession2, ControllerInfo, SessionCommand2)}. + * In that case, an item would be added automatically without the data source.</li> + * </ul> + * <p> + * If it's not set, playback wouldn't happen for the item without data source descriptor. + * <p> + * The helper will be run on the executor that was specified by + * {@link Builder#setSessionCallback(Executor, SessionCallback)}. + * + * @param helper a data source missing helper. + * @throws IllegalStateException when the helper is set when the playlist agent is set + * @see #setPlaylist(List, MediaMetadata2) + * @see SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2) + * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM + * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM + */ + public void setOnDataSourceMissingHelper(@NonNull OnDataSourceMissingHelper helper) { + mProvider.setOnDataSourceMissingHelper_impl(helper); + } + + /** + * Clears the data source missing helper. + * + * @see #setOnDataSourceMissingHelper(OnDataSourceMissingHelper) + */ + public void clearOnDataSourceMissingHelper() { + mProvider.clearOnDataSourceMissingHelper_impl(); + } + + /** + * Returns the playlist from the {@link MediaPlaylistAgent}. + * <p> + * This list may differ with the list that was specified with + * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent} + * implementation. Use media items returned here for other playlist agent APIs such as + * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}. + * + * @return playlist + * @see MediaPlaylistAgent#getPlaylist() + * @see SessionCallback#onPlaylistChanged( + * MediaSession2, MediaPlaylistAgent, List, MediaMetadata2) + */ + public List<MediaItem2> getPlaylist() { + return mProvider.getPlaylist_impl(); + } + + /** + * Sets a list of {@link MediaItem2} to the {@link MediaPlaylistAgent}. Ensure uniqueness of + * each {@link MediaItem2} in the playlist so the session can uniquely identity individual + * items. + * <p> + * This may be an asynchronous call, and {@link MediaPlaylistAgent} may keep the copy of the + * list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent, + * List, MediaMetadata2)} to know the operation finishes. + * <p> + * You may specify a {@link MediaItem2} without {@link DataSourceDesc}. In that case, + * {@link MediaPlaylistAgent} has responsibility to dynamically query {@link DataSourceDesc} + * when such media item is ready for preparation or play. Default implementation needs + * {@link OnDataSourceMissingHelper} for such case. + * + * @param list A list of {@link MediaItem2} objects to set as a play list. + * @throws IllegalArgumentException if given list is {@code null}, or has duplicated media + * items. + * @see MediaPlaylistAgent#setPlaylist(List, MediaMetadata2) + * @see SessionCallback#onPlaylistChanged( + * MediaSession2, MediaPlaylistAgent, List, MediaMetadata2) + * @see #setOnDataSourceMissingHelper + */ + public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { + mProvider.setPlaylist_impl(list, metadata); + } + + /** + * Skips to the item in the playlist. + * <p> + * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)} and the behavior depends + * on the playlist agent implementation, especially with the shuffle/repeat mode. + * + * @param item The item in the playlist you want to play + * @see #getShuffleMode() + * @see #getRepeatMode() + */ + public void skipToPlaylistItem(@NonNull MediaItem2 item) { + mProvider.skipToPlaylistItem_impl(item); + } + + /** + * Skips to the previous item. + * <p> + * This calls {@link MediaPlaylistAgent#skipToPreviousItem()} and the behavior depends on the + * playlist agent implementation, especially with the shuffle/repeat mode. + * + * @see #getShuffleMode() + * @see #getRepeatMode() + **/ + public void skipToPreviousItem() { + mProvider.skipToPreviousItem_impl(); + } + + /** + * Skips to the next item. + * <p> + * This calls {@link MediaPlaylistAgent#skipToNextItem()} and the behavior depends on the + * playlist agent implementation, especially with the shuffle/repeat mode. + * + * @see #getShuffleMode() + * @see #getRepeatMode() + */ + public void skipToNextItem() { + mProvider.skipToNextItem_impl(); + } + + /** + * Gets the playlist metadata from the {@link MediaPlaylistAgent}. + * + * @return the playlist metadata + */ + public MediaMetadata2 getPlaylistMetadata() { + return mProvider.getPlaylistMetadata_impl(); + } + + /** + * Adds the media item to the playlist at position index. Index equals or greater than + * the current playlist size will add the item at the end of the playlist. + * <p> + * This will not change the currently playing media item. + * If index is less than or equal to the current index of the play list, + * the current index of the play list will be incremented correspondingly. + * + * @param index the index you want to add + * @param item the media item you want to add + */ + public void addPlaylistItem(int index, @NonNull MediaItem2 item) { + mProvider.addPlaylistItem_impl(index, item); + } + + /** + * Removes the media item in the playlist. + * <p> + * If the item is the currently playing item of the playlist, current playback + * will be stopped and playback moves to next source in the list. + * + * @param item the media item you want to add + */ + public void removePlaylistItem(@NonNull MediaItem2 item) { + mProvider.removePlaylistItem_impl(item); + } + + /** + * Replaces the media item at index in the playlist. This can be also used to update metadata of + * an item. + * + * @param index the index of the item to replace + * @param item the new item + */ + public void replacePlaylistItem(int index, @NonNull MediaItem2 item) { + mProvider.replacePlaylistItem_impl(index, item); + } + + /** + * Return currently playing media item. + * + * @return currently playing media item + */ + public MediaItem2 getCurrentMediaItem() { + // TODO(jaewan): Rename provider, and implement (b/74316764) + return mProvider.getCurrentPlaylistItem_impl(); + } + + /** + * Updates the playlist metadata to the {@link MediaPlaylistAgent}. + * + * @param metadata metadata of the playlist + */ + public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) { + mProvider.updatePlaylistMetadata_impl(metadata); + } + + /** + * Gets the repeat mode from the {@link MediaPlaylistAgent}. + * + * @return repeat mode + * @see MediaPlaylistAgent#REPEAT_MODE_NONE + * @see MediaPlaylistAgent#REPEAT_MODE_ONE + * @see MediaPlaylistAgent#REPEAT_MODE_ALL + * @see MediaPlaylistAgent#REPEAT_MODE_GROUP + */ + public @RepeatMode int getRepeatMode() { + return mProvider.getRepeatMode_impl(); + } + + /** + * Sets the repeat mode to the {@link MediaPlaylistAgent}. + * + * @param repeatMode repeat mode + * @see MediaPlaylistAgent#REPEAT_MODE_NONE + * @see MediaPlaylistAgent#REPEAT_MODE_ONE + * @see MediaPlaylistAgent#REPEAT_MODE_ALL + * @see MediaPlaylistAgent#REPEAT_MODE_GROUP + */ + public void setRepeatMode(@RepeatMode int repeatMode) { + mProvider.setRepeatMode_impl(repeatMode); + } + + /** + * Gets the shuffle mode from the {@link MediaPlaylistAgent}. + * + * @return The shuffle mode + * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE + * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL + * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP + */ + public @ShuffleMode int getShuffleMode() { + return mProvider.getShuffleMode_impl(); + } + + /** + * Sets the shuffle mode to the {@link MediaPlaylistAgent}. + * + * @param shuffleMode The shuffle mode + * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE + * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL + * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP + */ + public void setShuffleMode(@ShuffleMode int shuffleMode) { + mProvider.setShuffleMode_impl(shuffleMode); } } |