/* * Copyright 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.media2.session; import static androidx.annotation.RestrictTo.Scope.LIBRARY; import static androidx.media2.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED; import static androidx.media2.session.SessionResult.RESULT_SUCCESS; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.view.KeyEvent; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.core.content.ContextCompat; import androidx.core.util.ObjectsCompat; import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media2.common.CallbackMediaItem; import androidx.media2.common.MediaItem; import androidx.media2.common.MediaMetadata; import androidx.media2.common.Rating; import androidx.media2.common.SessionPlayer; import androidx.media2.common.SessionPlayer.BuffState; import androidx.media2.common.SessionPlayer.PlayerResult; import androidx.media2.common.SessionPlayer.PlayerState; import androidx.media2.common.SessionPlayer.TrackInfo; import androidx.media2.common.SubtitleData; import androidx.media2.common.UriMediaItem; import androidx.media2.common.VideoSize; import androidx.media2.session.MediaController.PlaybackInfo; import androidx.media2.session.MediaLibraryService.LibraryParams; import androidx.media2.session.SessionResult.ResultCode; import androidx.versionedparcelable.ParcelField; import androidx.versionedparcelable.VersionedParcelable; import androidx.versionedparcelable.VersionedParcelize; import com.google.common.util.concurrent.ListenableFuture; import java.io.Closeable; import java.util.HashMap; import java.util.List; import java.util.concurrent.Executor; /** * Allows a media app to expose its transport controls and playback information in a process to * other processes including the Android framework and other apps. Common use cases are as follows. *
* A MediaSession should be created when an app wants to publish media playback information or * handle media keys. In general an app only needs one session for all playback, though multiple * sessions can be created to provide finer grain controls of media. See * Supporting Multiple Sessions for detail. *
* If you want to support background playback, {@link MediaSessionService} is preferred * instead. With it, your playback can be revived even after playback is finished. See * {@link MediaSessionService} for details. *
* Topics covered here: *
* A session can be obtained by {@link Builder}. The owner of the session may pass its session token * to other processes to allow them to create a {@link MediaController} to interact with the * session. *
* When a session receive transport control commands, the session sends the commands directly to * the underlying media player set by {@link Builder} or {@link #updatePlayer}. *
* When an app is finished performing playback it must call {@link #close()} to clean up the session * and notify any controllers. The app is responsible for closing the underlying player after * closing the session. * is closed. *
* {@link MediaSession} objects are thread safe, but should be used on the thread on the looper. *
* Here's the table of per key event. *
Key code | {@link MediaSession} API |
---|---|
{@link KeyEvent#KEYCODE_MEDIA_PLAY} | *{@link SessionPlayer#play()} |
{@link KeyEvent#KEYCODE_MEDIA_PAUSE} | *{@link SessionPlayer#pause()} |
{@link KeyEvent#KEYCODE_MEDIA_NEXT} | *{@link SessionPlayer#skipToNextPlaylistItem()} |
{@link KeyEvent#KEYCODE_MEDIA_PREVIOUS} | *{@link SessionPlayer#skipToPreviousPlaylistItem()} |
{@link KeyEvent#KEYCODE_MEDIA_STOP} | *{@link SessionPlayer#pause()} and then * {@link SessionPlayer#seekTo(long)} with 0 |
{@link KeyEvent#KEYCODE_MEDIA_FAST_FORWARD} | *{@link SessionCallback#onFastForward} |
{@link KeyEvent#KEYCODE_MEDIA_REWIND} | *{@link SessionCallback#onRewind} |
|
*
|
*
* Unknown package name nor UID doesn't mean that you should disallow connection nor commands. For
* SDK levels where such issue happen, session tokens could only be obtained by trusted apps (e.g.
* Bluetooth, Auto, ...), so it may be better for you to allow them as you did with legacy session.
*
* @see MediaSessionService
*/
public class MediaSession implements Closeable {
static final String TAG = "MediaSession";
// It's better to have private static lock instead of using MediaSession.class because the
// private lock object isn't exposed.
private static final Object STATIC_LOCK = new Object();
// Note: This checks the uniqueness of a session ID only in single process.
// When the framework becomes able to check the uniqueness, this logic should be removed.
@GuardedBy("STATIC_LOCK")
private static final HashMap
* When the session is closed, it returns the lastly set player.
*
* @return player.
*/
@NonNull
public SessionPlayer getPlayer() {
return mImpl.getPlayer();
}
/**
* Gets the session ID
*
* @return
*/
@NonNull
public String getId() {
return mImpl.getId();
}
/**
* Returns the {@link SessionToken} for creating {@link MediaController}.
*/
@NonNull
public SessionToken getToken() {
return mImpl.getToken();
}
@NonNull
Context getContext() {
return mImpl.getContext();
}
@NonNull
Executor getCallbackExecutor() {
return mImpl.getCallbackExecutor();
}
@NonNull
SessionCallback getCallback() {
return mImpl.getCallback();
}
/**
* Returns the list of connected controller.
*
* @return list of {@link ControllerInfo}
*/
@NonNull
public List
* It's up to controller's decision how to represent the layout in its own UI.
* Here are some examples.
*
* Note:
* This API can be called in the
* {@link SessionCallback#onConnect(MediaSession, ControllerInfo)}.
*
* @param controller controller to specify layout.
* @param layout ordered list of layout.
*/
@NonNull
public ListenableFuture
* This is synchronous call. Changes in the allowed commands take effect immediately regardless
* of the controller notified about the change through
* {@link MediaController.ControllerCallback
* #onAllowedCommandsChanged(MediaController, SessionCommandGroup)}
*
* @param controller controller to change allowed commands
* @param commands new allowed commands
*/
public void setAllowedCommands(@NonNull ControllerInfo controller,
@NonNull SessionCommandGroup commands) {
if (controller == null) {
throw new NullPointerException("controller shouldn't be null");
}
if (commands == null) {
throw new NullPointerException("commands shouldn't be null");
}
mImpl.setAllowedCommands(controller, commands);
}
/**
* Broadcasts a custom command to all connected controllers.
*
* This is synchronous call and doesn't wait for result from the controller. Use
* {@link #sendCustomCommand(ControllerInfo, SessionCommand, Bundle)} for getting the result.
*
* A command is not accepted if it is not a custom command.
*
* @param command a command
* @param args optional argument
* @see #sendCustomCommand(ControllerInfo, SessionCommand, Bundle)
*/
public void broadcastCustomCommand(@NonNull SessionCommand command, @Nullable Bundle args) {
if (command == null) {
throw new NullPointerException("command shouldn't be null");
}
if (command.getCommandCode() != SessionCommand.COMMAND_CODE_CUSTOM) {
throw new IllegalArgumentException("command should be a custom command");
}
mImpl.broadcastCustomCommand(command, args);
}
/**
* Sends a custom command to a specific controller.
*
* A command is not accepted if it is not a custom command.
*
* @param command a command
* @param args optional argument
* @see #broadcastCustomCommand(SessionCommand, Bundle)
*/
@NonNull
public ListenableFuture
* If it's not set, the session will accept all controllers and all incoming commands by
* default.
*/
public abstract static class SessionCallback {
ForegroundServiceEventCallback mForegroundServiceEventCallback;
/**
* Called when a controller is created for this session. Return allowed commands for
* controller. By default it allows all connection requests and commands.
*
* You can reject the connection by return {@code null}. In that case, the controller
* receives {@link MediaController.ControllerCallback#onDisconnected(MediaController)} and
* cannot be used.
*
* The controller hasn't connected yet in this method, so calls to the controller
* (e.g. {@link #sendCustomCommand}, {@link #setCustomLayout}) would be ignored. Override
* {@link #onPostConnect} for the custom initialization for the controller instead.
*
* @param session the session for this event
* @param controller controller information.
* @return allowed commands. Can be {@code null} to reject connection.
* @see #onPostConnect(MediaSession, ControllerInfo)
*/
@Nullable
public SessionCommandGroup onConnect(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
SessionCommandGroup commands = new SessionCommandGroup.Builder()
.addAllPredefinedCommands(SessionCommand.COMMAND_VERSION_CURRENT)
.build();
return commands;
}
/**
* Called immediately after a controller is connected. This is a convenient method to add
* custom initialization between the session and a controller.
*
* Note that calls to the controller (e.g. {@link #sendCustomCommand},
* {@link #setCustomLayout}) work here but don't work in {@link #onConnect} because the
* controller hasn't connected yet in {@link #onConnect}.
*
* @param session the session for this event
* @param controller controller information.
*/
public void onPostConnect(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
}
/**
* Called when a controller is disconnected.
*
* Interoperability: For legacy controller, this is called when the controller doesn't send
* any command for a while. It's because there were no explicit disconnect API in legacy
* controller API.
*
* @param session the session for this event
* @param controller controller information
*/
public void onDisconnected(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {}
/**
* Called when a controller sent a command which will be sent directly to one of the
* following:
*
* Return {@link SessionResult#RESULT_SUCCESS} to proceed the command. If something
* else is returned, command wouldn't be sent and the controller would receive the code with
* it.
*
* @param session the session for this event
* @param controller controller information.
* @param command a command. This method will be called for every single command.
* @return {@code RESULT_SUCCESS} if you want to proceed with incoming command.
* Another code for ignore.
* @see SessionCommand#COMMAND_CODE_PLAYER_PLAY
* @see SessionCommand#COMMAND_CODE_PLAYER_PAUSE
* @see SessionCommand#COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_PREPARE
* @see SessionCommand#COMMAND_CODE_PLAYER_SEEK_TO
* @see SessionCommand#COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_SET_SHUFFLE_MODE
* @see SessionCommand#COMMAND_CODE_PLAYER_SET_REPEAT_MODE
* @see SessionCommand#COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_GET_PLAYLIST
* @see SessionCommand#COMMAND_CODE_PLAYER_SET_PLAYLIST
* @see SessionCommand#COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA
* @see SessionCommand#COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA
* @see SessionCommand#COMMAND_CODE_VOLUME_SET_VOLUME
* @see SessionCommand#COMMAND_CODE_VOLUME_ADJUST_VOLUME
*/
@ResultCode
public int onCommandRequest(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull SessionCommand command) {
return RESULT_SUCCESS;
}
/**
* Called when a controller has sent a command with a {@link MediaItem} to add a new media
* item to this session. Being specific, this will be called for following APIs.
*
* If the given media ID is valid, you should return the media item with the given media ID.
* If the ID doesn't match, an {@link RuntimeException} will be thrown.
* You may return {@code null} if the given item is invalid. Here's the behavior when it
* happens.
*
* This will be called on the same thread where {@link #onCommandRequest} and commands with
* the media controller will be executed.
*
* Default implementation returns the {@code null}.
*
* @param session the session for this event
* @param controller controller information
* @param mediaId non-empty media id for creating item with
* @return translated media item for player with the mediaId. Can be {@code null} to ignore.
* @see MediaMetadata#METADATA_KEY_MEDIA_ID
*/
@Nullable
public MediaItem onCreateMediaItem(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String mediaId) {
return null;
}
/**
* Called when a controller set rating of a media item through
* {@link MediaController#setRating(String, Rating)}.
*
* To allow setting user rating for a {@link MediaItem}, the media item's metadata
* should have {@link Rating} with the key {@link MediaMetadata#METADATA_KEY_USER_RATING},
* in order to provide possible rating style for controller. Controller will follow the
* rating style.
*
* @param session the session for this event
* @param controller controller information
* @param mediaId non-empty media id
* @param rating new rating from the controller
* @see SessionCommand#COMMAND_CODE_SESSION_SET_RATING
*/
@ResultCode
public int onSetRating(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String mediaId,
@NonNull Rating rating) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller requested to set the specific media item(s) represented by a URI
* through {@link MediaController#setMediaUri(Uri, Bundle)}.
*
* The implementation should create proper {@link MediaItem media item(s)} for the given
* {@code uri} and call {@link SessionPlayer#setMediaItem} or
* {@link SessionPlayer#setPlaylist}.
*
* When {@link MediaControllerCompat} is connected and sends commands with following
* methods, the {@code uri} would have the following patterns:
*
* {@link SessionPlayer#prepare()} or {@link SessionPlayer#play()} would be followed if
* this is called by above methods.
*
* @param session the session for this event
* @param controller controller information
* @param uri uri
* @param extras optional extra bundle
*/
@ResultCode
public int onSetMediaUri(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull Uri uri, @Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller sent a custom command through
* {@link MediaController#sendCustomCommand(SessionCommand, Bundle)}.
*
* Interoperability: This would be also called by {@link
* android.support.v4.media.MediaBrowserCompat
* #sendCustomAction(String, Bundle, CustomActionCallback)}. If so, extra from
* sendCustomAction will be considered as args and customCommand would have null extra.
*
* @param session the session for this event
* @param controller controller information
* @param customCommand custom command.
* @param args optional arguments
* @return result of handling custom command. A runtime exception will be thrown if
* {@code null} is returned.
* @see SessionCommand#COMMAND_CODE_CUSTOM
*/
@NonNull
public SessionResult onCustomCommand(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull SessionCommand customCommand,
@Nullable Bundle args) {
return new SessionResult(RESULT_ERROR_NOT_SUPPORTED, null);
}
/**
* Called when a controller called {@link MediaController#fastForward()}.
*
* It can be implemented in many ways. For example, it can be implemented by seeking forward
* once, series of seeking forward, or increasing playback speed.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_FAST_FORWARD
*/
@ResultCode
public int onFastForward(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller called {@link MediaController#rewind()}.
*
* It can be implemented in many ways. For example, it can be implemented by seeking
* backward once, series of seeking backward, or decreasing playback speed.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_REWIND
*/
@ResultCode
public int onRewind(@NonNull MediaSession session, @NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller called {@link MediaController#skipForward()}.
*
* It's recommended to seek forward within the current media item, but its detail may vary.
* For example, it can be implemented by seeking forward for the fixed amount of seconds, or
* seeking forward to the nearest bookmark.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_SKIP_FORWARD
*/
@ResultCode
public int onSkipForward(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller called {@link MediaController#skipBackward()}.
*
* It's recommended to seek backward within the current media item, but its detail may vary.
* For example, it can be implemented by seeking backward for the fixed amount of seconds,
* or seeking backward to the nearest bookmark.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_SKIP_BACKWARD
*/
@ResultCode
public int onSkipBackward(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when the player state is changed. Used internally for setting the
* {@link MediaSessionService} as foreground/background.
*/
final void onPlayerStateChanged(MediaSession session, @PlayerState int state) {
if (mForegroundServiceEventCallback != null) {
mForegroundServiceEventCallback.onPlayerStateChanged(session, state);
}
}
final void onCurrentMediaItemChanged(MediaSession session) {
if (mForegroundServiceEventCallback != null) {
mForegroundServiceEventCallback.onNotificationUpdateNeeded(session);
}
}
final void onSessionClosed(MediaSession session) {
if (mForegroundServiceEventCallback != null) {
mForegroundServiceEventCallback.onSessionClosed(session);
}
}
void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
mForegroundServiceEventCallback = callback;
}
abstract static class ForegroundServiceEventCallback {
public void onPlayerStateChanged(MediaSession session, @PlayerState int state) {}
public void onNotificationUpdateNeeded(MediaSession session) {}
public void onSessionClosed(MediaSession session) {}
}
}
/**
* Builder for {@link MediaSession}.
*
* Any incoming event from the {@link MediaController} will be handled on the callback executor.
* If it's not set, {@link ContextCompat#getMainExecutor(Context)} will be used by default.
*/
public static final class Builder extends BuilderBase
* Interoperability: Package name may not be precisely obtained for legacy controller API on
* older device. Here are details.
*
* Interoperability: If {@code 21} ≤ {@code SDK_VERSION} < {@code 28}, then UID would
* be a negative value because it cannot be obtained.
*
* @return uid of the controller. Can be a negative value if the uid cannot be obtained.
*/
public int getUid() {
return mRemoteUserInfo.getUid();
}
/**
* Gets the connection hints sent from controller, or {@link Bundle#EMPTY} if none.
*/
@NonNull
public Bundle getConnectionHints() {
return mConnectionHints == null ? Bundle.EMPTY : new Bundle(mConnectionHints);
}
/**
* Returns if the controller has been 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.
* @hide
*/
@RestrictTo(LIBRARY)
public boolean isTrusted() {
return mIsTrusted;
}
@Override
public int hashCode() {
return ObjectsCompat.hash(mControllerCb, mRemoteUserInfo);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ControllerInfo)) {
return false;
}
if (this == obj) {
return true;
}
ControllerInfo other = (ControllerInfo) obj;
if (mControllerCb != null || other.mControllerCb != null) {
return ObjectsCompat.equals(mControllerCb, other.mControllerCb);
}
return mRemoteUserInfo.equals(other.mRemoteUserInfo);
}
@Override
public String toString() {
return "ControllerInfo {pkg=" + mRemoteUserInfo.getPackageName() + ", uid="
+ mRemoteUserInfo.getUid() + "})";
}
@Nullable ControllerCb getControllerCb() {
return mControllerCb;
}
@NonNull
static ControllerInfo createLegacyControllerInfo() {
RemoteUserInfo legacyRemoteUserInfo =
new RemoteUserInfo(
RemoteUserInfo.LEGACY_CONTROLLER,
/* pid= */ RemoteUserInfo.UNKNOWN_PID,
/* uid= */ RemoteUserInfo.UNKNOWN_UID);
return new ControllerInfo(
legacyRemoteUserInfo,
MediaUtils.VERSION_UNKNOWN,
/* trusted= */ false,
/* cb= */ null,
/* connectionHints= */ null);
}
}
/**
* Button for a {@link SessionCommand} that will be shown by the controller.
*
* It's up to the controller's decision to respect or ignore this customization request.
*/
@VersionedParcelize
public static final class CommandButton implements VersionedParcelable {
@ParcelField(1)
SessionCommand mCommand;
@ParcelField(2)
int mIconResId;
@ParcelField(3)
CharSequence mDisplayName;
@ParcelField(4)
Bundle mExtras;
@ParcelField(5)
boolean mEnabled;
// WARNING: Adding a new ParcelField may break old library users (b/152830728)
/**
* Used for VersionedParcelable
*/
CommandButton() {
}
CommandButton(@Nullable SessionCommand command, int iconResId,
@Nullable CharSequence displayName, Bundle extras, boolean enabled) {
mCommand = command;
mIconResId = iconResId;
mDisplayName = displayName;
mExtras = extras;
mEnabled = enabled;
}
/**
* Gets the command associated with this button. Can be {@code null} if the button isn't
* enabled and only providing placeholder.
*
* @return command or {@code null}
*/
@Nullable
public SessionCommand getCommand() {
return mCommand;
}
/**
* Gets the 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;
}
/**
* Gets the 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.
*/
@Nullable
public CharSequence getDisplayName() {
return mDisplayName;
}
/**
* Gets extra information of the button. It's private information between session and
* controller.
*
* @return
*/
@Nullable
public Bundle getExtras() {
return mExtras;
}
/**
* Returns whether it's enabled.
*
* @return {@code true} if enabled. {@code false} otherwise.
*/
public boolean isEnabled() {
return mEnabled;
}
/**
* Builder for {@link CommandButton}.
*/
public static final class Builder {
private SessionCommand mCommand;
private int mIconResId;
private CharSequence mDisplayName;
private Bundle mExtras;
private boolean mEnabled;
/**
* Sets the {@link SessionCommand} that would be sent to the session when the button
* is clicked.
*
* @param command session command
*/
@NonNull
public Builder setCommand(@Nullable SessionCommand command) {
mCommand = command;
return this;
}
/**
* Sets the bitmap-type (e.g. PNG) icon resource id of the button.
*
* None bitmap type (e.g. VectorDrawabale) may cause unexpected behavior when it's sent
* to {@link MediaController} app, so please avoid using it especially for the older
* platform (API < 21).
*
* @param resId resource id of the button
*/
@NonNull
public Builder setIconResId(int resId) {
mIconResId = resId;
return this;
}
/**
* Sets the display name of the button.
*
* @param displayName display name of the button
*/
@NonNull
public Builder setDisplayName(@Nullable CharSequence displayName) {
mDisplayName = displayName;
return this;
}
/**
* Sets whether the button is enabled. Can be {@code false} to indicate that the button
* should be shown but isn't clickable.
*
* @param enabled {@code true} if the button is enabled and ready.
* {@code false} otherwise.
*/
@NonNull
public Builder setEnabled(boolean enabled) {
mEnabled = enabled;
return this;
}
/**
* Sets the extras of the button.
*
* @param extras extras information of the button
*/
@NonNull
public Builder setExtras(@Nullable Bundle extras) {
mExtras = extras;
return this;
}
/**
* Builds the {@link CommandButton}.
*
* @return a new {@link CommandButton}
*/
@NonNull
public CommandButton build() {
return new CommandButton(mCommand, mIconResId, mDisplayName, mExtras, mEnabled);
}
}
}
// TODO: Drop 'Cb' from the name.
abstract static class ControllerCb {
abstract void onPlayerResult(int seq, PlayerResult result) throws RemoteException;
abstract void onSessionResult(int seq, SessionResult result) throws RemoteException;
abstract void onLibraryResult(int seq, LibraryResult result) throws RemoteException;
// Mostly matched with the methods in MediaController.ControllerCallback
abstract void setCustomLayout(int seq, @NonNull List
* APIs here should be package private, but should have documentations for developers.
* Otherwise, javadoc will generate documentation with the generic types such as follows.
*
* This class is hidden to prevent from generating test stub, which fails with
* 'unexpected bound' because it tries to auto generate stub class as follows.
*
* Use this if and only if your app supports multiple playback at the same time and also
* wants to provide external apps to have finer controls of them.
*
* @param id id of the session. Must be unique per package.
* @return
*/
// Note: This ID is not visible to the controllers. ID is introduced in order to prevent
// apps from creating multiple sessions without any clear reasons. If they create two
// sessions with the same ID in a process, then an IllegalStateException will be thrown.
@SuppressWarnings("unchecked")
@NonNull
U setId(@NonNull String id) {
if (id == null) {
throw new NullPointerException("id shouldn't be null");
}
mId = id;
return (U) this;
}
/**
* Sets callback for the session.
*
* @param executor callback executor
* @param callback session callback
* @return
*/
@SuppressWarnings("unchecked")
@NonNull
U setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
if (executor == null) {
throw new NullPointerException("executor shouldn't be null");
}
if (callback == null) {
throw new NullPointerException("callback shouldn't be null");
}
mCallbackExecutor = executor;
mCallback = callback;
return (U) this;
}
/**
* Sets extras for the session token. If not set, {@link SessionToken#getExtras()}
* will return an empty {@link Bundle}.
*
* @return the Builder to allow chaining
* @throws IllegalArgumentException if the bundle contains any non-framework Parcelable
* objects.
* @see SessionToken#getExtras()
*/
@NonNull
@SuppressWarnings("unchecked")
U setExtras(@NonNull Bundle extras) {
if (extras == null) {
throw new NullPointerException("extras shouldn't be null");
}
if (MediaUtils.doesBundleHaveCustomParcelable(extras)) {
throw new IllegalArgumentException(
"extras shouldn't contain any custom parcelables");
}
mExtras = new Bundle(extras);
return (U) this;
}
/**
* Builds a {@link MediaSession}.
*
* @return a new session
* @throws IllegalStateException if the session with the same id already exists for the
* package.
*/
@NonNull abstract T build();
}
}
layout[i]
means a CommandButton at index i in the given list
*
*
*
* Controller UX layout Layout example
* Row with 3 icons
* layout[1]
layout[0]
layout[2]
* Row with 5 icons
* layout[3]
layout[1]
layout[0]
* layout[2]
layout[4]
* Row with 5 icons and an overflow icon, and another expandable row with 5
* extra icons
* layout[3]
layout[1]
layout[0]
* layout[2]
layout[4]
* layout[3]
layout[1]
layout[0]
* layout[2]
layout[4]
*
*
*
* Override this to translate incoming {@code mediaId} to a {@link MediaItem} to be
* understood by your player. For example, a player may only understand
* {@link androidx.media2.common.FileMediaItem}, {@link UriMediaItem},
* and {@link CallbackMediaItem}. Check the documentation of the player that you're using.
*
*
*
* Controller command Behavior when {@code null} is returned
* addPlaylistItem Ignore
* replacePlaylistItem Ignore
* setPlaylist
* Ignore {@code null} items, and build a list with non-{@code null} items. Call
* {@link SessionPlayer#setPlaylist(List, MediaMetadata)} with the list
* setMediaItem Ignore
*
*
* Method Uri pattern
*
* {@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
* The {@code uri} passed as argument
*
* {@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId}
* {@code androidx://media2-session/prepareFromMediaId?id=[mediaId]}
*
* {@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch}
* {@code androidx://media2-session/prepareFromSearch?query=[query]}
*
* {@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
* The {@code uri} passed as argument
*
* {@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId}
* {@code androidx://media2-session/playFromMediaId?id=[mediaId]}
*
* {@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch}
* {@code androidx://media2-session/playFromSearch?query=[query]}
*
*
*
* @return package name of the controller. Can be
* {@link RemoteUserInfo#LEGACY_CONTROLLER LEGACY_CONTROLLER} if the package name
* cannot be obtained.
*/
@NonNull
public String getPackageName() {
return mRemoteUserInfo.getPackageName();
}
/**
* Gets the UID of the controller. Can be a negative value for interoperability.
* SDK version when package name isn't precise
* {@code ControllerInfo#getPackageName()} for legacy controller
* {@code SDK_VERSION} < {@code 21}
* Actual package name via {@link PackageManager#getNameForUid} with UID.
*
* It's sufficient for most cases, but doesn't precisely distinguish caller if it
* uses shared user ID.{@code 21} ≤ {@code SDK_VERSION} < {@code 24}
* {@link RemoteUserInfo#LEGACY_CONTROLLER LEGACY_CONTROLLER}
* U extends BuilderBase
* abstract static class BuilderBase<
* T extends MediaSession,
* U extends MediaSession.BuilderBase<
* T, U, C extends MediaSession.SessionCallback>, C>
* @hide
*/
@RestrictTo(LIBRARY)
abstract static class BuilderBase