blob: 15da1853072d470e2cc7cdb7041a40b3c99875e4 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media2.session;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import static androidx.media2.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED;
import static androidx.media2.session.SessionResult.RESULT_SUCCESS;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.view.KeyEvent;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.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.UriMediaItem;
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.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.
* <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 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.
* <p>
* 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.
* <p>
* Topic covered here:
* <ol>
* <li><a href="#SessionLifecycle">Session Lifecycle</a>
* <li><a href="#AudioFocusAndNoisyIntent">Audio focus and noisy intent</a>
* <li><a href="#Thread">Thread</a>
* <li><a href="#KeyEvents">Media key events mapping</a>
* </ol>
* <a name="SessionLifecycle"></a>
* <h3>Session Lifecycle</h3>
* <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 MediaController} 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 #updatePlayer}.
* <p>
* When an app is finished performing playback it must call {@link #close()} to clean up the session
* and notify any controllers.
* <p>
* <a name="Thread"></a>
* <h3>Thread</h3>
* <p>
* {@link MediaSession} objects are thread safe, but should be used on the thread on the looper.
* <a name="KeyEvents"></a>
* <h3>Media key events mapping</h3>
* <p>
* Here's the table of per key event.
* <table>
* <tr><th>Key code</th><th>{@link MediaSession} API</th></tr>
* <tr><td>{@link KeyEvent#KEYCODE_MEDIA_PLAY}</td>
* <td>{@link SessionPlayer#play()}</td></tr>
* <tr><td>{@link KeyEvent#KEYCODE_MEDIA_PAUSE}</td>
* <td>{@link SessionPlayer#pause()}</td></tr>
* <tr><td>{@link KeyEvent#KEYCODE_MEDIA_NEXT}</td>
* <td>{@link SessionPlayer#skipToNextPlaylistItem()}</td></tr>
* <tr><td>{@link KeyEvent#KEYCODE_MEDIA_PREVIOUS}</td>
* <td>{@link SessionPlayer#skipToPreviousPlaylistItem()}</td></tr>
* <tr><td>{@link KeyEvent#KEYCODE_MEDIA_STOP}</td>
* <td>{@link SessionPlayer#pause()} and then
* {@link SessionPlayer#seekTo(long)} with 0</td></tr>
* <tr><td>{@link KeyEvent#KEYCODE_MEDIA_FAST_FORWARD}</td>
* <td>{@link SessionCallback#onFastForward}</td></tr>
* <tr><td>{@link KeyEvent#KEYCODE_MEDIA_REWIND}</td>
* <td>{@link SessionCallback#onRewind}</td></tr>
* <tr><td><ul><li>{@link KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE}</li>
* <li>{@link KeyEvent#KEYCODE_HEADSETHOOK}</li></ul></td>
* <td><ul><li>For a single tap
* <ul><li>{@link SessionPlayer#pause()} if
* {@link SessionPlayer#PLAYER_STATE_PLAYING}</li>
* <li>{@link SessionPlayer#play()} otherwise</li></ul>
* <li>For a double tap, {@link SessionPlayer#skipToNextPlaylistItem()}</li></ul></td>
* </tr>
* </table>
* @see MediaSessionService
*/
public class MediaSession implements AutoCloseable {
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<String, MediaSession> SESSION_ID_TO_SESSION_MAP = new HashMap<>();
private final MediaSessionImpl mImpl;
MediaSession(Context context, String id, SessionPlayer player,
PendingIntent sessionActivity, Executor callbackExecutor, SessionCallback callback,
Bundle tokenExtras) {
synchronized (STATIC_LOCK) {
if (SESSION_ID_TO_SESSION_MAP.containsKey(id)) {
throw new IllegalStateException("Session ID must be unique. ID=" + id);
}
SESSION_ID_TO_SESSION_MAP.put(id, this);
}
mImpl = createImpl(context, id, player, sessionActivity, callbackExecutor, callback,
tokenExtras);
}
MediaSessionImpl createImpl(Context context, String id, SessionPlayer player,
PendingIntent sessionActivity, Executor callbackExecutor, SessionCallback callback,
Bundle tokenExtras) {
return new MediaSessionImplBase(this, context, id, player, sessionActivity,
callbackExecutor, callback, tokenExtras);
}
/**
* Should be only used by subclass.
*/
MediaSessionImpl getImpl() {
return mImpl;
}
static MediaSession getSession(Uri sessionUri) {
synchronized (STATIC_LOCK) {
for (MediaSession session : SESSION_ID_TO_SESSION_MAP.values()) {
if (ObjectsCompat.equals(session.getUri(), sessionUri)) {
return session;
}
}
}
return null;
}
/**
* Updates the underlying {@link SessionPlayer} for this session to dispatch incoming event to.
*
* @param player a player that handles actual media playback in your app
*/
public void updatePlayer(@NonNull SessionPlayer player) {
if (player == null) {
throw new NullPointerException("player shouldn't be null");
}
mImpl.updatePlayer(player);
}
@Override
public void close() {
try {
synchronized (STATIC_LOCK) {
SESSION_ID_TO_SESSION_MAP.remove(mImpl.getId());
}
mImpl.close();
} catch (Exception e) {
// Should not be here.
}
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public boolean isClosed() {
return mImpl.isClosed();
}
/**
* Gets the underlying {@link SessionPlayer}.
* <p>
* When the session is closed, it returns the lastly set player.
*
* @return player.
*/
public @NonNull SessionPlayer getPlayer() {
return mImpl.getPlayer();
}
/**
* Gets the session ID
*
* @return
*/
public @NonNull String getId() {
return mImpl.getId();
}
/**
* Returns the {@link SessionToken} for creating {@link MediaController}.
*/
public @NonNull SessionToken getToken() {
return mImpl.getToken();
}
@NonNull Context getContext() {
return mImpl.getContext();
}
@NonNull Executor getCallbackExecutor() {
return mImpl.getCallbackExecutor();
}
@NonNull SessionCallback getCallback() {
return mImpl.getCallback();
}
/**
* Returns the list of connected controller.
*
* @return list of {@link ControllerInfo}
*/
public @NonNull List<ControllerInfo> getConnectedControllers() {
return mImpl.getConnectedControllers();
}
/**
* 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 are some examples.
* <p>
* Note: <code>layout[i]</code> means a CommandButton at index i in the given list
* <table>
* <tr><th>Controller UX layout</th><th>Layout example</th></tr>
* <tr><td>Row with 3 icons</td>
* <td><code>layout[1]</code> <code>layout[0]</code> <code>layout[2]</code></td></tr>
* <tr><td>Row with 5 icons</td>
* <td><code>layout[3]</code> <code>layout[1]</code> <code>layout[0]</code>
* <code>layout[2]</code> <code>layout[4]</code></td></tr>
* <tr><td rowspan=2>Row with 5 icons and an overflow icon, and another expandable row with 5
* extra icons</td>
* <td><code>layout[3]</code> <code>layout[1]</code> <code>layout[0]</code>
* <code>layout[2]</code> <code>layout[4]</code></td></tr>
* <tr><td><code>layout[3]</code> <code>layout[1]</code> <code>layout[0]</code>
* <code>layout[2]</code> <code>layout[4]</code></td></tr>
* </table>
* <p>
* This API can be called in the
* {@link SessionCallback#onConnect(MediaSession, ControllerInfo)}.
*
* @param controller controller to specify layout.
* @param layout ordered list of layout.
*/
public @NonNull ListenableFuture<SessionResult> setCustomLayout(
@NonNull ControllerInfo controller, @NonNull List<CommandButton> layout) {
if (controller == null) {
throw new NullPointerException("controller shouldn't be null");
}
if (layout == null) {
throw new NullPointerException("layout shouldn't be null");
}
return mImpl.setCustomLayout(controller, layout);
}
/**
* Sets the new allowed command group for the controller.
* <p>
* This is synchronous call. Changes in the allowed commands take effect immediately regardless
* of the controller notified about the change through
* {@link MediaController.ControllerCallback
* #onAllowedCommandsChanged(MediaController, SessionCommandGroup)}
*
* @param controller controller to change allowed commands
* @param commands new allowed commands
*/
public void setAllowedCommands(@NonNull ControllerInfo controller,
@NonNull SessionCommandGroup commands) {
if (controller == null) {
throw new NullPointerException("controller shouldn't be null");
}
if (commands == null) {
throw new NullPointerException("commands shouldn't be null");
}
mImpl.setAllowedCommands(controller, commands);
}
/**
* Broadcasts custom command to all connected controllers.
* <p>
* This is synchronous call and doesn't wait for result from the controller. Use
* {@link #sendCustomCommand(ControllerInfo, SessionCommand, Bundle)} for getting the result.
* <p>
* A command is not accepted if it is not a custom command.
*
* @param command a command
* @param args optional argument
* @see #sendCustomCommand(ControllerInfo, SessionCommand, Bundle)
*/
public void broadcastCustomCommand(@NonNull SessionCommand command, @Nullable Bundle args) {
if (command == null) {
throw new NullPointerException("command shouldn't be null");
}
if (command.getCommandCode() != SessionCommand.COMMAND_CODE_CUSTOM) {
throw new IllegalArgumentException("command should be a custom command");
}
mImpl.broadcastCustomCommand(command, args);
}
/**
* Send custom command to a specific controller.
* <p>
* A command is not accepted if it is not a custom command.
*
* @param command a command
* @param args optional argument
* @see #broadcastCustomCommand(SessionCommand, Bundle)
*/
public @NonNull ListenableFuture<SessionResult> sendCustomCommand(
@NonNull ControllerInfo controller, @NonNull SessionCommand command,
@Nullable Bundle args) {
if (controller == null) {
throw new NullPointerException("controller shouldn't be null");
}
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");
}
return mImpl.sendCustomCommand(controller, command, args);
}
/**
* @hide
* @return Bundle
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public MediaSessionCompat getSessionCompat() {
return mImpl.getSessionCompat();
}
/**
* Handles the controller's connection request from {@link MediaSessionService}.
*
* @param controller controller aidl
* @param packageName controller package name
* @param pid controller pid
* @param uid controller uid
* @param connectionHints controller connection hints
*/
void handleControllerConnectionFromService(IMediaController controller, String packageName,
int pid, int uid, @Nullable Bundle connectionHints) {
mImpl.connectFromService(controller, packageName, pid, uid, connectionHints);
}
IBinder getLegacyBrowerServiceBinder() {
return mImpl.getLegacyBrowserServiceBinder();
}
@NonNull
private Uri getUri() {
return mImpl.getUri();
}
/**
* Callback to be called for all incoming commands from {@link MediaController}s.
* <p>
* 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.
* <p>
* You can reject the connection by return {@code null}. In that case, the controller
* receives {@link MediaController.ControllerCallback#onDisconnected(MediaController)} and
* cannot be used.
* <p>
* The controller hasn't connected yet in this method, so calls to the controller
* (e.g. {@link #sendCustomCommand}, {@link #setCustomLayout}) would be ignored. Override
* {@link #onPostConnect} for the custom initialization for the controller instead.
*
* @param session the session for this event
* @param controller controller information.
* @return allowed commands. Can be {@code null} to reject connection.
* @see #onPostConnect(MediaSession, ControllerInfo)
*/
public @Nullable SessionCommandGroup onConnect(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
SessionCommandGroup commands = new SessionCommandGroup.Builder()
.addAllPredefinedCommands(SessionCommand.COMMAND_VERSION_1)
.build();
return commands;
}
/**
* Called immediately after a controller is connected. This is a convenient method to add
* custom initialization between the session and a controller.
* <p>
* Note that calls to the controller (e.g. {@link #sendCustomCommand},
* {@link #setCustomLayout}) work here but don't work in {@link #onConnect} because the
* controller hasn't connected yet in {@link #onConnect}.
*
* @param session the session for this event
* @param controller controller information.
*/
public void onPostConnect(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
}
/**
* Called when a controller is disconnected
*
* @param session the session for this event
* @param controller controller information
*/
public void onDisconnected(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {}
/**
* Called when a controller sent a command which will be sent directly to one of the
* following:
* <ul>
* <li>{@link SessionPlayer}</li>
* <li>{@link android.media.AudioManager}</li>
* </ul>
* <p>
* Return {@link SessionResult#RESULT_SUCCESS} to proceed the command. If something
* else is returned, command wouldn't be sent and the controller would receive the code with
* it.
*
* @param session the session for this event
* @param controller controller information.
* @param command a command. This method will be called for every single command.
* @return {@code RESULT_SUCCESS} if you want to proceed with incoming command.
* Another code for ignore.
* @see SessionCommand#COMMAND_CODE_PLAYER_PLAY
* @see SessionCommand#COMMAND_CODE_PLAYER_PAUSE
* @see SessionCommand#COMMAND_CODE_PLAYER_SKIP_TO_NEXT_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_SKIP_TO_PREVIOUS_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_PREPARE
* @see SessionCommand#COMMAND_CODE_PLAYER_SEEK_TO
* @see SessionCommand#COMMAND_CODE_PLAYER_SKIP_TO_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_SET_SHUFFLE_MODE
* @see SessionCommand#COMMAND_CODE_PLAYER_SET_REPEAT_MODE
* @see SessionCommand#COMMAND_CODE_PLAYER_ADD_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_REMOVE_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_REPLACE_PLAYLIST_ITEM
* @see SessionCommand#COMMAND_CODE_PLAYER_GET_PLAYLIST
* @see SessionCommand#COMMAND_CODE_PLAYER_SET_PLAYLIST
* @see SessionCommand#COMMAND_CODE_PLAYER_GET_PLAYLIST_METADATA
* @see SessionCommand#COMMAND_CODE_PLAYER_UPDATE_LIST_METADATA
* @see SessionCommand#COMMAND_CODE_VOLUME_SET_VOLUME
* @see SessionCommand#COMMAND_CODE_VOLUME_ADJUST_VOLUME
*/
public @ResultCode int onCommandRequest(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull SessionCommand command) {
return RESULT_SUCCESS;
}
/**
* Called when a controller has sent a command with a {@link MediaItem} to add a new media
* item to this session. Being specific, this will be called for following APIs.
* <ol>
* <li>{@link MediaController#addPlaylistItem(int, String)}
* <li>{@link MediaController#replacePlaylistItem(int, String)}
* <li>{@link MediaController#setPlaylist(List, MediaMetadata)}
* <li>{@link MediaController#setMediaItem(String)}
* </ol>
* 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.
* <p>
* 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.
* <table border="0" cellspacing="0" cellpadding="0">
* <tr><th>Controller command</th> <th>Behavior when {@code null} is returned</th></tr>
* <tr><td>addPlaylistItem</td> <td>Ignore</td></tr>
* <tr><td>replacePlaylistItem</td> <td>Ignore</td></tr>
* <tr><td>setPlaylist</td>
* <td>Ignore {@code null} items, and build a list with non-{@code null} items. Call
* {@link SessionPlayer#setPlaylist(List, MediaMetadata)} with the list</td></tr>
* <tr><td>setMediaItem</td> <td>Ignore</td></tr>
* </table>
* <p>
* This will be called on the same thread where {@link #onCommandRequest} and commands with
* the media controller will be executed.
* <p>
* Default implementation returns the {@code null}.
*
* @param session the session for this event
* @param controller controller information
* @param mediaId non-empty media id for creating item with
* @return translated media item for player with the mediaId. Can be {@code null} to ignore.
* @see MediaMetadata#METADATA_KEY_MEDIA_ID
*/
public @Nullable MediaItem onCreateMediaItem(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String mediaId) {
return null;
}
/**
* Called when a controller set rating of a media item through
* {@link MediaController#setRating(String, Rating)}.
* <p>
* To allow setting user rating for a {@link MediaItem}, the media item's metadata
* should have {@link Rating} with the key {@link MediaMetadata#METADATA_KEY_USER_RATING},
* in order to provide possible rating style for controller. Controller will follow the
* rating style.
*
* @param session the session for this event
* @param controller controller information
* @param mediaId non-empty media id
* @param rating new rating from the controller
* @see SessionCommand#COMMAND_CODE_SESSION_SET_RATING
*/
public @ResultCode int onSetRating(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String mediaId,
@NonNull Rating rating) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller sent a custom command through
* {@link MediaController#sendCustomCommand(SessionCommand, Bundle)}.
* <p>
* Interoperability: This would be also called by {@link
* android.support.v4.media.MediaBrowserCompat
* #sendCustomAction(String, Bundle, CustomActionCallback)}. If so, extra from
* sendCustomAction will be considered as args and customCommand would have null extra.
*
* @param session the session for this event
* @param controller controller information
* @param customCommand custom command.
* @param args optional arguments
* @return result of handling custom command. A runtime exception will be thrown if
* {@code null} is returned.
* @see SessionCommand#COMMAND_CODE_CUSTOM
*/
public @NonNull SessionResult onCustomCommand(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull SessionCommand customCommand,
@Nullable Bundle args) {
return new SessionResult(RESULT_ERROR_NOT_SUPPORTED, null);
}
/**
* Called when a controller requested to play a specific mediaId through
* {@link MediaController#playFromMediaId(String, Bundle)}.
*
* @param session the session for this event
* @param controller controller information
* @param mediaId non-empty media id
* @param extras optional extra bundle
* @see SessionCommand#COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPlayFromMediaId(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String mediaId,
@Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller requested to begin playback from a search query through
* {@link MediaController#playFromSearch(String, Bundle)}
*
* @param session the session for this event
* @param controller controller information
* @param query non-empty search query.
* @param extras optional extra bundle
* @see SessionCommand#COMMAND_CODE_SESSION_PLAY_FROM_SEARCH
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPlayFromSearch(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String query,
@Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller requested to play a specific media item represented by a URI
* through {@link MediaController#playFromUri(Uri, Bundle)}
*
* @param session the session for this event
* @param controller controller information
* @param uri uri
* @param extras optional extra bundle
* @see SessionCommand#COMMAND_CODE_SESSION_PLAY_FROM_URI
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPlayFromUri(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull Uri uri,
@Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller requested to prepare for playing a specific mediaId through
* {@link MediaController#prepareFromMediaId(String, Bundle)}.
* <p>
* During the prepare, a session should not hold audio focus in order to allow
* other sessions play seamlessly. The state of playback should be updated to
* {@link SessionPlayer#PLAYER_STATE_PAUSED} after the prepare is done.
* <p>
* The playback of the prepared content should start in the later calls of
* {@link SessionPlayer#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 non-empty media id
* @param extras optional extra bundle
* @see SessionCommand#COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPrepareFromMediaId(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String mediaId,
@Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller requested to prepare playback from a search query through
* {@link MediaController#prepareFromSearch(String, Bundle)}.
* <p>
* During the prepare, a session should not hold audio focus in order to allow
* other sessions play seamlessly. The state of playback should be updated to
* {@link SessionPlayer#PLAYER_STATE_PAUSED} after the prepare is done.
* <p>
* The playback of the prepared content should start in the later calls of
* {@link SessionPlayer#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 non-empty search query
* @param extras optional extra bundle
* @see SessionCommand#COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPrepareFromSearch(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull String query,
@Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller requested to prepare a specific media item represented by a URI
* through {@link MediaController#prepareFromUri(Uri, Bundle)}.
* <p>
* During the prepare, a session should not hold audio focus in order to allow
* other sessions play seamlessly. The state of playback should be updated to
* {@link SessionPlayer#PLAYER_STATE_PAUSED} after the prepare is done.
* <p>
* The playback of the prepared content should start in the later calls of
* {@link SessionPlayer#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 SessionCommand#COMMAND_CODE_SESSION_PREPARE_FROM_URI
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
@ResultCode
public int onPrepareFromUri(@NonNull MediaSession session,
@NonNull ControllerInfo controller, @NonNull Uri uri, @Nullable Bundle extras) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller called {@link MediaController#fastForward()}.
* <p>
* It's recommended to increase the playback speed when this method is called.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_FAST_FORWARD
*/
public @ResultCode int onFastForward(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller called {@link MediaController#rewind()}.
* <p>
* It's recommended to decrease the playback speed when this method is called.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_REWIND
*/
public @ResultCode int onRewind(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller called {@link MediaController#skipForward()}.
* <p>
* It's recommended to seek forward within the current media item when this method
* is called.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_SKIP_FORWARD
*/
public @ResultCode int onSkipForward(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when a controller called {@link MediaController#skipBackward()}.
* <p>
* It's recommended to seek backward within the current media item when this method
* is called.
*
* @param session the session for this event
* @param controller controller information
* @see SessionCommand#COMMAND_CODE_SESSION_SKIP_BACKWARD
*/
public @ResultCode int onSkipBackward(@NonNull MediaSession session,
@NonNull ControllerInfo controller) {
return RESULT_ERROR_NOT_SUPPORTED;
}
/**
* Called when the player state is changed. Used internally for setting the
* {@link MediaSessionService} as foreground/background.
*/
final void onPlayerStateChanged(MediaSession session, @PlayerState int state) {
if (mForegroundServiceEventCallback != null) {
mForegroundServiceEventCallback.onPlayerStateChanged(session, state);
}
}
final void onSessionClosed(MediaSession session) {
if (mForegroundServiceEventCallback != null) {
mForegroundServiceEventCallback.onSessionClosed(session);
}
}
void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
mForegroundServiceEventCallback = callback;
}
abstract static class ForegroundServiceEventCallback {
public void onPlayerStateChanged(MediaSession session, @PlayerState int state) {}
public void onSessionClosed(MediaSession session) {}
}
}
/**
* Builder for {@link MediaSession}.
* <p>
* 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<MediaSession, Builder, SessionCallback> {
public Builder(@NonNull Context context, @NonNull SessionPlayer player) {
super(context, player);
}
@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
@NonNull
public Builder setSessionCallback(@NonNull Executor executor,
@NonNull SessionCallback callback) {
return super.setSessionCallback(executor, callback);
}
@NonNull
@Override
public Builder setExtras(@NonNull Bundle extras) {
return super.setExtras(extras);
}
@Override
public @NonNull MediaSession build() {
if (mCallbackExecutor == null) {
mCallbackExecutor = ContextCompat.getMainExecutor(mContext);
}
if (mCallback == null) {
mCallback = new SessionCallback() {};
}
return new MediaSession(mContext, mId, mPlayer, mSessionActivity,
mCallbackExecutor, mCallback, mExtras);
}
}
/**
* Information of a controller.
*/
public static final class ControllerInfo {
private final RemoteUserInfo mRemoteUserInfo;
private final boolean mIsTrusted;
private final ControllerCb mControllerCb;
private final Bundle mConnectionHints;
/**
* @param remoteUserInfo remote user info
* @param trusted {@code true} if trusted, {@code false} otherwise
* @param cb ControllerCb. Can be {@code null} only when a MediaBrowserCompat connects to
* MediaSessionService and ControllerInfo is needed for
* SessionCallback#onConnected().
* @param connectionHints a session-specific argument sent from the controller for the
* connection. The contents of this bundle may affect the
* connection result.
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted,
@Nullable ControllerCb cb, @Nullable Bundle connectionHints) {
mRemoteUserInfo = remoteUserInfo;
mIsTrusted = trusted;
mControllerCb = cb;
mConnectionHints = connectionHints;
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public @NonNull RemoteUserInfo getRemoteUserInfo() {
return mRemoteUserInfo;
}
/**
* @return package name of the controller. Can be
* {@link androidx.media.MediaSessionManager.RemoteUserInfo#LEGACY_CONTROLLER} if
* the package name cannot be obtained.
*/
public @NonNull String getPackageName() {
return mRemoteUserInfo.getPackageName();
}
/**
* @return uid of the controller. Can be a negative value if the uid cannot be obtained.
*/
public int getUid() {
return mRemoteUserInfo.getUid();
}
/**
* @return connection hints sent from controller, or {@link Bundle#EMPTY} if none.
*/
@NonNull
public Bundle getConnectionHints() {
return mConnectionHints == null ? Bundle.EMPTY : new Bundle(mConnectionHints);
}
/**
* 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.
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
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;
}
}
/**
* Button for a {@link SessionCommand} that will be shown by the controller.
* <p>
* It's up to the controller's decision to respect or ignore this customization request.
*/
@VersionedParcelize
public static final class CommandButton implements VersionedParcelable {
@ParcelField(1)
SessionCommand mCommand;
@ParcelField(2)
int mIconResId;
@ParcelField(3)
CharSequence mDisplayName;
@ParcelField(4)
Bundle mExtras;
@ParcelField(5)
boolean mEnabled;
/**
* Used for VersionedParcelable
*/
CommandButton() {
}
CommandButton(@Nullable SessionCommand command, int iconResId,
@Nullable CharSequence displayName, Bundle extras, boolean enabled) {
mCommand = command;
mIconResId = iconResId;
mDisplayName = displayName;
mExtras = extras;
mEnabled = enabled;
}
/**
* Get command associated with this button. Can be {@code null} if the button isn't enabled
* and only providing placeholder.
*
* @return command or {@code null}
*/
public @Nullable SessionCommand getCommand() {
return mCommand;
}
/**
* Resource id of the button in this package. Can be {@code 0} if the command is predefined
* and custom icon isn't needed.
*
* @return resource id of the icon. Can be {@code 0}.
*/
public int getIconResId() {
return mIconResId;
}
/**
* Display name of the button. Can be {@code null} or empty if the command is predefined
* and custom name isn't needed.
*
* @return custom display name. Can be {@code null} or empty.
*/
public @Nullable CharSequence getDisplayName() {
return mDisplayName;
}
/**
* Extra information of the button. It's private information between session and controller.
*
* @return
*/
public @Nullable Bundle getExtras() {
return mExtras;
}
/**
* Return whether it's enabled.
*
* @return {@code true} if enabled. {@code false} otherwise.
*/
public boolean isEnabled() {
return mEnabled;
}
/**
* Builder for {@link CommandButton}.
*/
public static final class Builder {
private SessionCommand mCommand;
private int mIconResId;
private CharSequence mDisplayName;
private Bundle mExtras;
private boolean mEnabled;
/**
* Sets the {@link SessionCommand} that would be sent to the session when the button
* is clicked.
*
* @param command session command
*/
public @NonNull Builder setCommand(@Nullable SessionCommand command) {
mCommand = command;
return this;
}
/**
* Sets the bitmap-type (e.g. PNG) icon resource id of the button.
* <p>
* None bitmap type (e.g. VectorDrawabale) may cause unexpected behavior when it's sent
* to {@link MediaController} app, so please avoid using it especially for the older
* platform (API < 21).
*
* @param resId resource id of the button
*/
public @NonNull Builder setIconResId(int resId) {
mIconResId = resId;
return this;
}
/**
* Sets the display name of the button.
*
* @param displayName display name of the button
*/
public @NonNull Builder setDisplayName(@Nullable CharSequence displayName) {
mDisplayName = displayName;
return this;
}
/**
* Sets whether the button is enabled. Can be {@code false} to indicate that the button
* should be shown but isn't clickable.
*
* @param enabled {@code true} if the button is enabled and ready.
* {@code false} otherwise.
*/
public @NonNull Builder setEnabled(boolean enabled) {
mEnabled = enabled;
return this;
}
/**
* Sets the extras of the button.
*
* @param extras extras information of the button
*/
public @NonNull Builder setExtras(@Nullable Bundle extras) {
mExtras = extras;
return this;
}
/**
* Builds the {@link CommandButton}.
*
* @return a new {@link CommandButton}
*/
public @NonNull CommandButton build() {
return new CommandButton(mCommand, mIconResId, mDisplayName, mExtras, mEnabled);
}
}
}
// TODO: Drop 'Cb' from the name.
abstract static class ControllerCb {
abstract void onPlayerResult(int seq, PlayerResult result) throws RemoteException;
abstract void onSessionResult(int seq, SessionResult result) throws RemoteException;
abstract void onLibraryResult(int seq, LibraryResult result) throws RemoteException;
// Mostly matched with the methods in MediaController.ControllerCallback
abstract void setCustomLayout(int seq, @NonNull List<CommandButton> layout)
throws RemoteException;
abstract void sendCustomCommand(int seq, @NonNull SessionCommand command,
@Nullable Bundle args) throws RemoteException;
abstract void onPlaybackInfoChanged(int seq, @NonNull PlaybackInfo info)
throws RemoteException;
abstract void onAllowedCommandsChanged(int seq, @NonNull SessionCommandGroup commands)
throws RemoteException;
abstract void onPlayerStateChanged(int seq, long eventTimeMs, long positionMs,
int playerState) throws RemoteException;
abstract void onPlaybackSpeedChanged(int seq, long eventTimeMs, long positionMs,
float speed) throws RemoteException;
abstract void onBufferingStateChanged(int seq, @NonNull MediaItem item,
@BuffState int bufferingState, long bufferedPositionMs, long eventTimeMs,
long positionMs) throws RemoteException;
abstract void onSeekCompleted(int seq, long eventTimeMs, long positionMs, long position)
throws RemoteException;
abstract void onCurrentMediaItemChanged(int seq, @Nullable MediaItem item, int currentIdx,
int previousIdx, int nextIdx) throws RemoteException;
abstract void onPlaylistChanged(int seq, @NonNull List<MediaItem> playlist,
@Nullable MediaMetadata metadata, int currentIdx, int previousIdx,
int nextIdx) throws RemoteException;
abstract void onPlaylistMetadataChanged(int seq, @Nullable MediaMetadata metadata)
throws RemoteException;
abstract void onShuffleModeChanged(int seq, @SessionPlayer.ShuffleMode int shuffleMode,
int currentIdx, int previousIdx, int nextIdx) throws RemoteException;
abstract void onRepeatModeChanged(int seq, @SessionPlayer.RepeatMode int repeatMode,
int currentIdx, int previousIdx, int nextIdx) throws RemoteException;
abstract void onPlaybackCompleted(int seq) throws RemoteException;
abstract void onDisconnected(int seq) throws RemoteException;
// Mostly matched with the methods in MediaBrowser.BrowserCallback.
abstract void onChildrenChanged(int seq, @NonNull String parentId, int itemCount,
@Nullable LibraryParams params) throws RemoteException;
abstract void onSearchResultChanged(int seq, @NonNull String query, int itemCount,
@Nullable LibraryParams params) throws RemoteException;
}
interface MediaSessionImpl extends MediaInterface.SessionPlayer, AutoCloseable {
void updatePlayer(@NonNull SessionPlayer player,
@Nullable SessionPlayer playlistAgent);
void updatePlayer(@NonNull SessionPlayer player);
@NonNull SessionPlayer getPlayer();
@NonNull String getId();
@NonNull Uri getUri();
@NonNull SessionToken getToken();
@NonNull List<ControllerInfo> getConnectedControllers();
boolean isConnected(@NonNull ControllerInfo controller);
ListenableFuture<SessionResult> setCustomLayout(@NonNull ControllerInfo controller,
@NonNull List<CommandButton> layout);
void setAllowedCommands(@NonNull ControllerInfo controller,
@NonNull SessionCommandGroup commands);
void broadcastCustomCommand(@NonNull SessionCommand command, @Nullable Bundle args);
ListenableFuture<SessionResult> sendCustomCommand(@NonNull ControllerInfo controller,
@NonNull SessionCommand command, @Nullable Bundle args);
// Internally used methods
MediaSession getInstance();
MediaSessionCompat getSessionCompat();
Context getContext();
Executor getCallbackExecutor();
SessionCallback getCallback();
boolean isClosed();
PlaybackStateCompat createPlaybackStateCompat();
PlaybackInfo getPlaybackInfo();
PendingIntent getSessionActivity();
IBinder getLegacyBrowserServiceBinder();
void connectFromService(IMediaController caller, String packageName, int pid, int uid,
@Nullable Bundle connectionHints);
}
/**
* Base builder class for MediaSession and its subclass. Any change in this class should be
* also applied to the subclasses {@link MediaSession.Builder} and
* {@link MediaLibraryService.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 MediaSession,
* U extends MediaSession.BuilderBase<
* T, U, C extends MediaSession.SessionCallback>, C></pre>
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
abstract static class BuilderBase
<T extends MediaSession, U extends BuilderBase<T, U, C>, C extends SessionCallback> {
final Context mContext;
SessionPlayer mPlayer;
String mId;
Executor mCallbackExecutor;
C mCallback;
PendingIntent mSessionActivity;
Bundle mExtras;
BuilderBase(@NonNull Context context, @NonNull SessionPlayer player) {
if (context == null) {
throw new NullPointerException("context shouldn't be null");
}
if (player == null) {
throw new NullPointerException("player shouldn't be null");
}
mContext = context;
mPlayer = player;
// Ensure non-null id.
mId = "";
}
/**
* 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.
*/
@NonNull U setSessionActivity(@Nullable PendingIntent pi) {
mSessionActivity = pi;
return (U) this;
}
/**
* Set ID of the session. If it's not set, an empty string will be 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.
* @return
*/
// Note: This ID is not visible to the controllers. ID is introduced in order to prevent
// apps from creating multiple sessions without any clear reasons. If they create two
// sessions with the same ID in a process, then an IllegalStateException will be thrown.
@NonNull U setId(@NonNull String id) {
if (id == null) {
throw new NullPointerException("id shouldn't be null");
}
mId = id;
return (U) this;
}
/**
* Set callback for the session.
*
* @param executor callback executor
* @param callback session callback.
* @return
*/
@NonNull U setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
if (executor == null) {
throw new NullPointerException("executor shouldn't be null");
}
if (callback == null) {
throw new NullPointerException("callback shouldn't be null");
}
mCallbackExecutor = executor;
mCallback = callback;
return (U) this;
}
/**
* Set extras for the session token. If not set, {@link SessionToken#getExtras()}
* will return {@link Bundle#EMPTY}.
*
* @return The Builder to allow chaining
* @see SessionToken#getExtras()
*/
@NonNull
public U setExtras(@NonNull Bundle extras) {
if (extras == null) {
throw new NullPointerException("extras shouldn't be null");
}
mExtras = extras;
return (U) this;
}
/**
* Build {@link MediaSession}.
*
* @return a new session
* @throws IllegalStateException if the session with the same id is already exists for the
* package.
*/
@NonNull abstract T build();
}
}