MediaSession.java

/*
 * 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.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.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. The app is responsible for closing the underlying player after
 * closing the session.
 * is closed.
 * <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;
        abstract void onVideoSizeChanged(int seq, @NonNull MediaItem item,
                @NonNull VideoSize videoSize) throws RemoteException;
        abstract void onTrackInfoChanged(int seq, List<TrackInfo> trackInfos,
                TrackInfo selectedVideoTrack, TrackInfo selectedAudioTrack,
                TrackInfo selectedSubtitleTrack, TrackInfo selectedMetadataTrack)
                throws RemoteException;
        abstract void onTrackSelected(int seq, TrackInfo trackInfo) throws RemoteException;
        abstract void onTrackDeselected(int seq, TrackInfo trackInfo) throws RemoteException;
        abstract void onSubtitleData(int seq, @NonNull MediaItem item, @NonNull TrackInfo track,
                @NonNull SubtitleData data) 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();
    }
}