Files
frameworks_base/media/java/android/media/MediaSession2.java
Jaewan Kim c599ba36e3 MediaSession2: Move binder interfaces into the updatable
Test: Run all MediaComponents tests once
Change-Id: I29a7aa9d649ea212ad4728ebabff40ec0d47ecb1
2018-01-29 22:43:06 +09:00

1326 lines
48 KiB
Java

/*
* Copyright 2018 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 android.media;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayerInterface.PlaybackListener;
import android.media.session.MediaSession;
import android.media.session.MediaSession.Callback;
import android.media.session.PlaybackState;
import android.media.update.ApiLoader;
import android.media.update.MediaSession2Provider;
import android.media.update.MediaSession2Provider.ControllerInfoProvider;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IInterface;
import android.os.Parcelable;
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;
/**
* 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>
* <li>Bluetooth/wired headset key events support</li>
* <li>Android Auto/Wearable support</li>
* <li>Separating UI process and playback process</li>
* </ul>
* <p>
* A MediaSession2 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.
* <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
* {@link MediaSessionService2} for details.
* <p>
* 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 MediaController2} to interact with the
* 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(MediaPlayerInterface)}.
* <p>
* When an app is finished performing playback it must call {@link #close()} to clean up the session
* and notify any controllers.
* <p>
* {@link MediaSession2} objects should be used on the thread on the looper.
*
* @see MediaSessionService2
* @hide
*/
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?
// TODO(jaewan): Should we also protect getPlaybackState()?
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;
public static final int COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS = 20;
/**
* 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}.
*/
// 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;
}
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;
}
/**
* @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));
}
}
@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);
}
@Override
public int hashCode() {
final int prime = 31;
return ((mCustomCommand != null)
? mCustomCommand.hashCode() : 0) * prime + mCommandCode;
}
}
/**
* Represent set of {@link Command}.
*/
// 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 CommandGroup() {
}
public CommandGroup(CommandGroup others) {
mCommands.addAll(others.mCommands);
}
public void addCommand(Command command) {
mCommands.add(command);
}
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));
mCommands.add(new Command(COMMAND_CODE_PLAYBACK_PREPARE));
mCommands.add(new Command(COMMAND_CODE_PLAYBACK_FAST_FORWARD));
mCommands.add(new Command(COMMAND_CODE_PLAYBACK_REWIND));
mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SEEK_TO));
mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM));
mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SET_PLAYLIST_PARAMS));
}
public void removeCommand(Command command) {
mCommands.remove(command);
}
public boolean hasCommand(Command command) {
return mCommands.contains(command);
}
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;
}
/**
* @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;
}
/**
* @return new instance of CommandGroup from the bundle
* @hide
*/
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;
}
}
/**
* Callback to be called for all incoming commands from {@link MediaController2}s.
* <p>
* 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 {
/**
* 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.
*
* @param controller controller information.
* @return allowed commands. Can be {@code null} to reject coonnection.
*/
// TODO(jaewan): Change return type. Once we do, null is for reject.
public @Nullable CommandGroup onConnect(@NonNull ControllerInfo controller) {
CommandGroup commands = new CommandGroup();
commands.addAllPredefinedCommands();
return commands;
}
/**
* Called when a controller is disconnected
*
* @param controller controller information
*/
public void onDisconnected(@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}.
*
* @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.
*/
// 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) {
return true;
}
/**
* Called when a controller set rating on the currently playing contents.
*
* @param controller controller information
* @param rating new rating from the controller
*/
public void onSetRating(@NonNull ControllerInfo controller, @NonNull Rating2 rating) { }
/**
* Called when a controller sent a custom command.
*
* @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) { }
/**
* Override to handle requests to prepare for playing a specific mediaId.
* 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.
* <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.
*/
public void onPlayFromMediaId(@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.
* <p>
* 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.
*/
public void onPlayFromSearch(@NonNull ControllerInfo controller,
@NonNull String query, @Nullable Bundle extras) { }
/**
* Override to handle requests to prepare a specific media item represented by a URI.
* 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.
* <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.
*/
public void onPlayFromUri(@NonNull ControllerInfo controller,
@NonNull String uri, @Nullable Bundle extras) { }
/**
* Override to handle requests to play a specific mediaId.
*/
public void onPrepareFromMediaId(@NonNull ControllerInfo controller,
@NonNull String mediaId, @Nullable Bundle extras) { }
/**
* 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.
*/
public void onPrepareFromSearch(@NonNull ControllerInfo controller,
@NonNull String query, @Nullable Bundle extras) { }
/**
* Override to handle requests to play a specific media item represented by a URI.
*/
public void onPrepareFromUri(@NonNull ControllerInfo controller,
@NonNull Uri uri, @Nullable Bundle extras) { }
/**
* Called when a controller wants to add a {@link MediaItem2} at the specified position
* in the play queue.
* <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.
*
* @param item The media item to be inserted.
* @param index The index at which the item is to be inserted.
*/
public void onAddPlaylistItem(@NonNull ControllerInfo controller,
@NonNull MediaItem2 item, int index) { }
/**
* Called when a controller wants to remove the {@link MediaItem2}
*
* @param item
*/
// Can we do this automatically?
public void onRemovePlaylistItem(@NonNull MediaItem2 item) { }
};
/**
* Base builder class for MediaSession2 and its subclass.
*
* @hide
*/
static abstract class BuilderBase
<T extends MediaSession2.BuilderBase<T, C>, C extends SessionCallback> {
final Context mContext;
final MediaPlayerInterface mPlayer;
String mId;
Executor mCallbackExecutor;
C mCallback;
VolumeProvider mVolumeProvider;
int mRatingType;
PendingIntent mSessionActivity;
/**
* Constructor.
*
* @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}.
*/
// TODO(jaewan): Also need executor
public BuilderBase(@NonNull Context context, @NonNull MediaPlayerInterface 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 = "";
}
/**
* 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.
* <p>
* Set {@code null} to reset.
*
* @param volumeProvider The provider that will handle volume changes. Can be {@code null}
*/
public T setVolumeProvider(@Nullable VolumeProvider volumeProvider) {
mVolumeProvider = volumeProvider;
return (T) 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>
*/
public T setRatingType(@Rating2.Style int type) {
mRatingType = type;
return (T) 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 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;
}
/**
* Set ID of the session. If it's not set, an empty string with used to create a session.
* <p>
* 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.
* @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;
}
/**
* Set callback for the session.
*
* @param executor callback executor
* @param callback session callback.
* @return
*/
public T 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;
}
/**
* Build {@link MediaSession2}.
*
* @return a new session
* @throws IllegalStateException if the session with the same id is already exists for the
* package.
*/
public abstract MediaSession2 build();
}
/**
* Builder for {@link MediaSession2}.
* <p>
* 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 MediaPlayerInterface player) {
super(context, player);
}
@Override
public MediaSession2 build() {
if (mCallbackExecutor == null) {
mCallbackExecutor = mContext.getMainExecutor();
}
if (mCallback == null) {
mCallback = new SessionCallback();
}
return new MediaSession2(mContext, mPlayer, mId, mVolumeProvider, mRatingType,
mSessionActivity, mCallbackExecutor, mCallback);
}
}
/**
* 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,
IInterface callback) {
mProvider = ApiLoader.getProvider(context)
.createMediaSession2ControllerInfoProvider(
context, this, uid, pid, packageName, callback);
}
/**
* @return package name of the controller
*/
public String getPackageName() {
return mProvider.getPackageName_impl();
}
/**
* @return uid of the controller
*/
public int getUid() {
return mProvider.getUid_impl();
}
/**
* Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or
* has a enabled notification listener so can be trusted to accept connection and incoming
* command request.
*
* @return {@code true} if the controller is trusted.
*/
public boolean isTrusted() {
return mProvider.isTrusted_impl();
}
@SystemApi
public ControllerInfoProvider getProvider() {
return mProvider;
}
@Override
public int hashCode() {
return mProvider.hashCode_impl();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ControllerInfo)) {
return false;
}
ControllerInfo other = (ControllerInfo) obj;
return mProvider.equals_impl(other.mProvider);
}
@Override
public String toString() {
// TODO(jaewan): Move this to updatable.
return "ControllerInfo {pkg=" + getPackageName() + ", uid=" + getUid() + ", trusted="
+ isTrusted() + "}";
}
}
/**
* Button for a {@link Command} 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;
}
/**
* 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 Command 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 String getDisplayName() {
return mDisplayName;
}
/**
* Extra information of the button. It's private information between session and controller.
*
* @return
*/
public @Nullable Bundle getExtra() {
return mExtra;
}
/**
* Return whether it's enabled
*
* @return {@code true} if enabled. {@code false} otherwise.
*/
public boolean isEnabled() {
return mEnabled;
}
/**
* @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;
}
}
/**
* Builder for {@link CommandButton}.
*/
public static class Builder {
private Command mCommand;
private int mIconResId;
private String mDisplayName;
private Bundle mExtra;
private boolean mEnabled;
public Builder() {
mEnabled = true;
}
public Builder setCommand(Command command) {
mCommand = command;
return this;
}
public Builder setIconResId(int resId) {
mIconResId = resId;
return this;
}
public Builder setDisplayName(String displayName) {
mDisplayName = displayName;
return this;
}
public Builder setEnabled(boolean enabled) {
mEnabled = enabled;
return this;
}
public Builder setExtra(Bundle extra) {
mExtra = extra;
return this;
}
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);
}
}
}
/**
* Parameter for the playlist.
*/
public static class PlaylistParams {
/**
* @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;
/**
* Keys used for converting a PlaylistParams object to a bundle object and vice versa.
*/
private static final String KEY_REPEAT_MODE =
"android.media.session2.playlistparams2.repeat_mode";
private static final String KEY_SHUFFLE_MODE =
"android.media.session2.playlistparams2.shuffle_mode";
private static final String KEY_MEDIA_METADATA2_BUNDLE =
"android.media.session2.playlistparams2.metadata2_bundle";
private @RepeatMode int mRepeatMode;
private @ShuffleMode int mShuffleMode;
private MediaMetadata2 mPlaylistMetadata;
public PlaylistParams(@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;
}
/**
* Returns this object as a bundle to share between processes.
*
* @hide
*/
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(KEY_REPEAT_MODE, mRepeatMode);
bundle.putInt(KEY_SHUFFLE_MODE, mShuffleMode);
if (mPlaylistMetadata != null) {
bundle.putBundle(KEY_MEDIA_METADATA2_BUNDLE, mPlaylistMetadata.getBundle());
}
return bundle;
}
/**
* Creates an instance from a bundle which is previously created by {@link #toBundle()}.
*
* @param bundle A bundle created by {@link #toBundle()}.
* @return A new {@link PlaylistParams} instance. Returns {@code null} if the given
* {@param bundle} is null, or if the {@param bundle} has no playlist parameters.
* @hide
*/
public static PlaylistParams fromBundle(Bundle bundle) {
if (bundle == null) {
return null;
}
if (!bundle.containsKey(KEY_REPEAT_MODE) || !bundle.containsKey(KEY_SHUFFLE_MODE)) {
return null;
}
Bundle metadataBundle = bundle.getBundle(KEY_MEDIA_METADATA2_BUNDLE);
MediaMetadata2 metadata =
metadataBundle == null ? null : new MediaMetadata2(metadataBundle);
return new PlaylistParams(
bundle.getInt(KEY_REPEAT_MODE),
bundle.getInt(KEY_SHUFFLE_MODE),
metadata);
}
}
/**
* Constructor is hidden and apps can only instantiate indirectly through {@link Builder}.
* <p>
* This intended behavior and here's the reasons.
* 1. Prevent multiple sessions with the same tag in a media app.
* 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
* @hide
*/
MediaSession2(Context context, MediaPlayerInterface player, String id,
VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
Executor callbackExecutor, SessionCallback callback) {
super();
mProvider = createProvider(context, player, id, volumeProvider, ratingType, sessionActivity,
callbackExecutor, callback);
mProvider.initialize();
}
MediaSession2Provider createProvider(Context context, MediaPlayerInterface player, String id,
VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity,
Executor callbackExecutor, SessionCallback callback) {
return ApiLoader.getProvider(context)
.createMediaSession2(context, this, player, id, volumeProvider, ratingType,
sessionActivity, callbackExecutor, callback);
}
@SystemApi
public MediaSession2Provider getProvider() {
return mProvider;
}
/**
* Set the underlying {@link MediaPlayerInterface} 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.
* <p>
* If the new player is successfully set, {@link PlaybackListener}
* will be called to tell the current playback state of the new player.
* <p>
* You can also specify a volume provider. If so, playback in the player is considered as
* remote playback.
*
* @param player a {@link MediaPlayerInterface} that handles actual media playback in your app.
* @throws IllegalArgumentException if the player is {@code null}.
*/
public void setPlayer(@NonNull MediaPlayerInterface player) {
mProvider.setPlayer_impl(player);
}
/**
* Set the underlying {@link MediaPlayerInterface} with the volume provider for remote playback.
*
* @param player a {@link MediaPlayerInterface} that handles actual media playback in your app.
* @param volumeProvider a volume provider
* @see #setPlayer(MediaPlayerInterface)
* @see Builder#setVolumeProvider(VolumeProvider)
*/
public void setPlayer(@NonNull MediaPlayerInterface player,
@NonNull VolumeProvider volumeProvider) {
mProvider.setPlayer_impl(player, volumeProvider);
}
@Override
public void close() {
mProvider.close_impl();
}
/**
* @return player
*/
public @Nullable
MediaPlayerInterface getPlayer() {
return mProvider.getPlayer_impl();
}
/**
* Returns the {@link SessionToken2} for creating {@link MediaController2}.
*/
public @NonNull
SessionToken2 getToken() {
return mProvider.getToken_impl();
}
public @NonNull List<ControllerInfo> getConnectedControllers() {
return mProvider.getConnectedControllers_impl();
}
/**
* 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.
*
* @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.
*/
public void setAudioFocusRequest(int focusGain) {
mProvider.setAudioFocusRequest_impl(focusGain);
}
/**
* Sets ordered list of {@link CommandButton} for controllers to build UI with it.
* <p>
* It's up to controller's decision how to represent the layout in its own UI.
* Here's the same way
* (layout[i] means a CommandButton at index i in the given list)
* For 5 icons row
* layout[3] layout[1] layout[0] layout[2] layout[4]
* For 3 icons row
* layout[1] layout[0] layout[2]
* For 5 icons row with overflow icon (can show +5 extra buttons with overflow button)
* 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)}.
*
* @param controller controller to specify layout.
* @param layout oredered list of layout.
*/
public void setCustomLayout(@NonNull ControllerInfo controller,
@NonNull List<CommandButton> layout) {
mProvider.setCustomLayout_impl(controller, layout);
}
/**
* Set the new allowed command group for the controller
*
* @param controller controller to change allowed commands
* @param commands new allowed commands
*/
public void setAllowedCommands(@NonNull ControllerInfo controller,
@NonNull CommandGroup 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) {
mProvider.sendCustomCommand_impl(command, args);
}
/**
* Send custom command to a specific controller.
*
* @param command a command
* @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) {
// Equivalent to the MediaController.sendCustomCommand(Action action, ResultReceiver r);
mProvider.sendCustomCommand_impl(controller, command, args, receiver);
}
/**
* Play playback
*/
public void play() {
mProvider.play_impl();
}
/**
* Pause playback
*/
public void pause() {
mProvider.pause_impl();
}
/**
* Stop playback
*/
public void stop() {
mProvider.stop_impl();
}
/**
* Rewind playback
*/
public void skipToPrevious() {
mProvider.skipToPrevious_impl();
}
/**
* Rewind playback
*/
public void skipToNext() {
mProvider.skipToNext_impl();
}
/**
* 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.
*/
public void prepare() {
mProvider.prepare_impl();
}
/**
* Start fast forwarding. If playback is already fast forwarding this may increase the rate.
*/
public void fastForward() {
mProvider.fastForward_impl();
}
/**
* Start rewinding. If playback is already rewinding this may increase the rate.
*/
public void rewind() {
mProvider.rewind_impl();
}
/**
* Move to a new location in the media stream.
*
* @param pos Position to move to, in milliseconds.
*/
public void seekTo(long pos) {
mProvider.seekTo_impl(pos);
}
/**
* Sets the index of current DataSourceDesc in the play list to be played.
*
* @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
*/
public void setCurrentPlaylistItem(int index) {
mProvider.setCurrentPlaylistItem_impl(index);
}
/**
* @hide
*/
public void skipForward() {
// To match with KEYCODE_MEDIA_SKIP_FORWARD
}
/**
* @hide
*/
public void skipBackward() {
// To match with KEYCODE_MEDIA_SKIP_BACKWARD
}
public void setPlaylist(@NonNull List<MediaItem2> playlist, @NonNull PlaylistParams param) {
mProvider.setPlaylist_impl(playlist, param);
}
public List<MediaItem2> getPlaylist() {
return mProvider.getPlaylist_impl();
}
/**
* Sets the {@link PlaylistParams} for the current play list. Repeat/shuffle mode and metadata
* for the list can be set by calling this method.
*
* @param params A {@link PlaylistParams} object to set.
* @throws IllegalArgumentException if given {@param param} is null.
*/
public void setPlaylistParams(PlaylistParams params) {
mProvider.setPlaylistParams_impl(params);
}
/**
* Returns the {@link PlaylistParams} for the current play list.
* Returns {@code null} if not set.
*/
public PlaylistParams getPlaylistParams() {
return mProvider.getPlaylistParams_impl();
}
/*
* Add a {@link PlaybackListener} to listen changes in the underlying
* {@link MediaPlayerInterface}. Listener will be called immediately to tell the current value.
* <p>
* Added listeners will be also called when the underlying player is changed.
*
* @param executor the call listener
* @param listener the listener that will be run
* @throws IllegalArgumentException when either the listener or handler is {@code null}.
*/
public void addPlaybackListener(@NonNull @CallbackExecutor Executor executor,
@NonNull PlaybackListener listener) {
mProvider.addPlaybackListener_impl(executor, listener);
}
/**
* Remove previously added {@link PlaybackListener}.
*
* @param listener the listener to be removed
* @throws IllegalArgumentException if the listener is {@code null}.
*/
public void removePlaybackListener(@NonNull PlaybackListener listener) {
mProvider.removePlaybackListener_impl(listener);
}
/**
* Return the {@link PlaybackState2} from the player.
*
* @return playback state
*/
public PlaybackState2 getPlaybackState() {
return mProvider.getPlaybackState_impl();
}
}