public class

MediaSession

extends java.lang.Object

implements java.lang.AutoCloseable

 java.lang.Object

↳androidx.media2.MediaSession

Subclasses:

MediaLibraryService.MediaLibrarySession

Gradle dependencies

compile group: 'androidx.media2', name: 'media2', version: '1.0.0-alpha04'

  • groupId: androidx.media2
  • artifactId: media2
  • version: 1.0.0-alpha04

Artifact androidx.media2:media2:1.0.0-alpha04 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.media2:media2 com.android.support:media2

Overview

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.

  • Bluetooth/wired headset key events support
  • Android Auto/Wearable support
  • Separating UI process and playback process

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.

If you want to support background playback, MediaSessionService is preferred instead. With it, your playback can be revived even after playback is finished. See MediaSessionService for details.

Topic covered here:

  1. Session Lifecycle
  2. Audio focus and noisy intent
  3. Thread
  4. Media key events mapping

Session Lifecycle

A session can be obtained by MediaSession.Builder. The owner of the session may pass its session token to other processes to allow them to create a MediaController to interact with the session.

When a session receive transport control commands, the session sends the commands directly to the the underlying media player set by MediaSession.Builder or MediaSession.updatePlayer(SessionPlayer).

When an app is finished performing playback it must call MediaSession.close() to clean up the session and notify any controllers.

Thread

MediaSession objects are thread safe, but should be used on the thread on the looper.

Media key events mapping

Here's the table of per key event.

Key codeMediaSession API
SessionPlayer.play()
SessionPlayer.pause()
SessionPlayer.skipToNextPlaylistItem()
SessionPlayer.skipToPreviousPlaylistItem()
SessionPlayer.pause() and then SessionPlayer.seekTo(long) with 0
MediaSession.SessionCallback.onFastForward(MediaSession, MediaSession.ControllerInfo)
MediaSession.SessionCallback.onRewind(MediaSession, MediaSession.ControllerInfo)

Summary

Methods
public voidbroadcastCustomCommand(SessionCommand command, Bundle args)

Broadcasts custom command to all connected controllers.

public voidclose()

public java.util.List<MediaSession.ControllerInfo>getConnectedControllers()

Returns the list of connected controller.

public java.lang.StringgetId()

Gets the session ID

public SessionPlayergetPlayer()

Gets the underlying SessionPlayer.

public android.support.v4.media.session.MediaSessionCompatgetSessionCompat()

public SessionTokengetToken()

Returns the SessionToken for creating MediaController.

public booleanisClosed()

public <any>sendCustomCommand(MediaSession.ControllerInfo controller, SessionCommand command, Bundle args)

Send custom command to a specific controller.

public voidsetAllowedCommands(MediaSession.ControllerInfo controller, SessionCommandGroup commands)

Sets the new allowed command group for the controller.

public <any>setCustomLayout(MediaSession.ControllerInfo controller, java.util.List<MediaSession.CommandButton> layout)

Sets ordered list of MediaSession.CommandButton for controllers to build UI with it.

public voidupdatePlayer(SessionPlayer player)

Updates the underlying SessionPlayer for this session to dispatch incoming event to.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Methods

public void updatePlayer(SessionPlayer player)

Updates the underlying SessionPlayer for this session to dispatch incoming event to.

Parameters:

player: a player that handles actual media playback in your app

public void close()

public boolean isClosed()

public SessionPlayer getPlayer()

Gets the underlying SessionPlayer.

When the session is closed, it returns the lastly set player.

Returns:

player.

public java.lang.String getId()

Gets the session ID

Returns:

public SessionToken getToken()

Returns the SessionToken for creating MediaController.

public java.util.List<MediaSession.ControllerInfo> getConnectedControllers()

Returns the list of connected controller.

Returns:

list of MediaSession.ControllerInfo

public <any> setCustomLayout(MediaSession.ControllerInfo controller, java.util.List<MediaSession.CommandButton> layout)

Sets ordered list of MediaSession.CommandButton for controllers to build UI with it.

It's up to controller's decision how to represent the layout in its own UI. Here are some examples.

Note: layout[i] means a CommandButton at index i in the given list

Controller UX layoutLayout example
Row with 3 icons layout[1] layout[0] layout[2]
Row with 5 icons layout[3] layout[1] layout[0] layout[2] layout[4]
Row with 5 icons and an overflow icon, and another expandable row with 5 extra icons layout[3] layout[1] layout[0] layout[2] layout[4]
layout[3] layout[1] layout[0] layout[2] layout[4]

This API can be called in the MediaSession.SessionCallback.

Parameters:

controller: controller to specify layout.
layout: ordered list of layout.

public void setAllowedCommands(MediaSession.ControllerInfo controller, SessionCommandGroup commands)

Sets the new allowed command group for the controller.

This is synchronous call. Changes in the allowed commands take effect immediately regardless of the controller notified about the change through #onAllowedCommandsChanged(MediaController, SessionCommandGroup)

Parameters:

controller: controller to change allowed commands
commands: new allowed commands

public void broadcastCustomCommand(SessionCommand command, Bundle args)

Broadcasts custom command to all connected controllers.

This is synchronous call and doesn't wait for result from the controller. Use MediaSession.sendCustomCommand(MediaSession.ControllerInfo, SessionCommand, Bundle) for getting the result.

Parameters:

command: a command
args: optional argument

See also: MediaSession.sendCustomCommand(MediaSession.ControllerInfo, SessionCommand, Bundle)

public <any> sendCustomCommand(MediaSession.ControllerInfo controller, SessionCommand command, Bundle args)

Send custom command to a specific controller.

Parameters:

command: a command
args: optional argument

See also: MediaSession.broadcastCustomCommand(SessionCommand, Bundle)

public android.support.v4.media.session.MediaSessionCompat getSessionCompat()

Returns:

Bundle

Source

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.media2;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.media2.MediaSession.SessionResult.RESULT_CODE_NOT_SUPPORTED;
import static androidx.media2.MediaSession.SessionResult.RESULT_CODE_SUCCESS;

import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.view.KeyEvent;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.core.content.ContextCompat;
import androidx.core.util.ObjectsCompat;
import androidx.media.MediaSessionManager.RemoteUserInfo;
import androidx.media2.MediaController.ControllerResult;
import androidx.media2.MediaController.PlaybackInfo;
import androidx.media2.MediaLibraryService.LibraryParams;
import androidx.media2.MediaLibraryService.LibraryResult;
import androidx.media2.MediaSession.SessionResult.ResultCode;
import androidx.media2.SessionPlayer.BuffState;
import androidx.media2.SessionPlayer.PlayerResult;
import androidx.media2.SessionPlayer.PlayerState;
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelable;
import androidx.versionedparcelable.VersionedParcelize;

import com.google.common.util.concurrent.ListenableFuture;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
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
 */
@TargetApi(Build.VERSION_CODES.P)
public class MediaSession implements AutoCloseable {
    static final String TAG = "MediaSession";

    private final MediaSessionImpl mImpl;

    MediaSession(Context context, String id, SessionPlayer player,
            PendingIntent sessionActivity, Executor callbackExecutor, SessionCallback callback) {
        mImpl = createImpl(context, id, player, sessionActivity, callbackExecutor,
                callback);
    }

    MediaSessionImpl createImpl(Context context, String id, SessionPlayer player,
            PendingIntent sessionActivity, Executor callbackExecutor, SessionCallback callback) {
        return new MediaSessionImplBase(this, context, id, player, sessionActivity,
                callbackExecutor, callback);
    }

    /**
     * Should be only used by subclass.
     */
    MediaSessionImpl getImpl() {
        return mImpl;
    }

    /**
     * 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 IllegalArgumentException("player shouldn't be null");
        }
        mImpl.updatePlayer(player);
    }

    @Override
    public void close() {
        try {
            mImpl.close();
        } catch (Exception e) {
            // Should not be here.
        }
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    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 IllegalArgumentException("controller shouldn't be null");
        }
        if (layout == null) {
            throw new IllegalArgumentException("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 IllegalArgumentException("controller shouldn't be null");
        }
        if (commands == null) {
            throw new IllegalArgumentException("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.
     *
     * @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 IllegalArgumentException("command shouldn't be null");
        }
        mImpl.broadcastCustomCommand(command, args);
    }

    /**
     * Send custom command to a specific controller.
     *
     * @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 IllegalArgumentException("controller shouldn't be null");
        }
        if (command == null) {
            throw new IllegalArgumentException("command shouldn't be null");
        }
        return mImpl.sendCustomCommand(controller, command, args);
    }

    /**
     * @hide
     * @return Bundle
     */
    @RestrictTo(LIBRARY_GROUP)
    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
     */
    void handleControllerConnectionFromService(IMediaController controller, String packageName,
            int pid, int uid) {
        mImpl.connectFromService(controller, packageName, pid, uid);
    }

    IBinder getLegacyBrowerServiceBinder() {
        return mImpl.getLegacyBrowserServiceBinder();
    }

    /**
     * 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, controller receives
         * {@link MediaController.ControllerCallback#onDisconnected(MediaController)} and cannot
         * be usable.
         *
         * @param session the session for this event
         * @param controller controller information.
         * @return allowed commands. Can be {@code null} to reject connection.
         */
        public @Nullable SessionCommandGroup onConnect(@NonNull MediaSession session,
                @NonNull ControllerInfo controller) {
            SessionCommandGroup commands = new SessionCommandGroup.Builder()
                    .addAllPredefinedCommands(SessionCommand.COMMAND_VERSION_1)
                    .build();
            return commands;
        }

        /**
         * 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_CODE_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_CODE_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_CODE_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 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_CODE_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_CODE_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)
        @ResultCode
        public int onPlayFromMediaId(@NonNull MediaSession session,
                @NonNull ControllerInfo controller, @NonNull String mediaId,
                @Nullable Bundle extras) {
            return RESULT_CODE_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)
        @ResultCode
        public int onPlayFromSearch(@NonNull MediaSession session,
                @NonNull ControllerInfo controller, @NonNull String query,
                @Nullable Bundle extras) {
            return RESULT_CODE_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)
        @ResultCode
        public int onPlayFromUri(@NonNull MediaSession session,
                @NonNull ControllerInfo controller, @NonNull Uri uri,
                @Nullable Bundle extras) {
            return RESULT_CODE_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)
        @ResultCode
        public int onPrepareFromMediaId(@NonNull MediaSession session,
                @NonNull ControllerInfo controller, @NonNull String mediaId,
                @Nullable Bundle extras) {
            return RESULT_CODE_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)
        @ResultCode
        public int onPrepareFromSearch(@NonNull MediaSession session,
                @NonNull ControllerInfo controller, @NonNull String query,
                @Nullable Bundle extras) {
            return RESULT_CODE_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)
        @ResultCode
        public int onPrepareFromUri(@NonNull MediaSession session,
                @NonNull ControllerInfo controller, @NonNull Uri uri, @Nullable Bundle extras) {
            return RESULT_CODE_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_CODE_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_CODE_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_CODE_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_CODE_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 thread
     * that created session with the {@link Builder#build()}.
     */
    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);
        }

        @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);
        }
    }

    /**
     * Information of a controller.
     */
    public static final class ControllerInfo {
        private final RemoteUserInfo mRemoteUserInfo;
        private final boolean mIsTrusted;
        private final ControllerCb mControllerCb;

        /**
         * @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().
         * @hide
         */
        @RestrictTo(LIBRARY_GROUP)
        ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, boolean trusted,
                @Nullable ControllerCb cb) {
            mRemoteUserInfo = remoteUserInfo;
            mIsTrusted = trusted;
            mControllerCb = cb;
        }

        /**
         * @hide
         */
        @RestrictTo(LIBRARY_GROUP)
        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 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)
        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)
                throws RemoteException;
        abstract void onRepeatModeChanged(int seq, @SessionPlayer.RepeatMode int repeatMode)
                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 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);
    }

    /**
     * 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 androidx.media2.MediaSession,
     *      U extends androidx.media2.MediaSession.BuilderBase<
     *              T, U, C extends androidx.media2.MediaSession.SessionCallback>, C></pre>
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    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;

        BuilderBase(@NonNull Context context, @NonNull SessionPlayer player) {
            if (context == null) {
                throw new IllegalArgumentException("context shouldn't be null");
            }
            if (player == null) {
                throw new IllegalArgumentException("player shouldn't be null");
            }
            mContext = context;
            mPlayer = player;
            // Ensure non-null 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 with used to create a session.
         * <p>
         * Use this if and only if your app supports multiple playback at the same time and also
         * wants to provide external apps to have finer controls of them.
         *
         * @param id id of the session. Must be unique per package.
         * @throws IllegalArgumentException if id is {@code null}
         * @return
         */
        @NonNull U setId(@NonNull String id) {
            if (id == null) {
                throw new IllegalArgumentException("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 IllegalArgumentException("executor shouldn't be null");
            }
            if (callback == null) {
                throw new IllegalArgumentException("callback shouldn't be null");
            }
            mCallbackExecutor = executor;
            mCallback = callback;
            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();
    }

    /**
     * Result class to be used with {@link ListenableFuture} for asynchronous calls.
     */
    // Specify full name to avoid build error 'cannot find symbol' for versioned parcelable.
    @androidx.versionedparcelable.VersionedParcelize
    public static class SessionResult implements RemoteResult,
            androidx.versionedparcelable.VersionedParcelable {
        /**
         * @hide
         */
        @IntDef(flag = false, /*prefix = "RESULT_CODE",*/ value = {
                RESULT_CODE_SUCCESS,
                RESULT_CODE_UNKNOWN_ERROR,
                RESULT_CODE_INVALID_STATE,
                RESULT_CODE_BAD_VALUE,
                RESULT_CODE_PERMISSION_DENIED,
                RESULT_CODE_IO_ERROR,
                RESULT_CODE_SKIPPED,
                RESULT_CODE_DISCONNECTED,
                RESULT_CODE_NOT_SUPPORTED,
                RESULT_CODE_AUTHENTICATION_EXPIRED,
                RESULT_CODE_PREMIUM_ACCOUNT_REQUIRED,
                RESULT_CODE_CONCURRENT_STREAM_LIMIT,
                RESULT_CODE_PARENTAL_CONTROL_RESTRICTED,
                RESULT_CODE_NOT_AVAILABLE_IN_REGION,
                RESULT_CODE_SKIP_LIMIT_REACHED,
                RESULT_CODE_SETUP_REQUIRED})
        @Retention(RetentionPolicy.SOURCE)
        @RestrictTo(LIBRARY_GROUP)
        public @interface ResultCode {}

        @ParcelField(1)
        int mResultCode;
        @ParcelField(2)
        long mCompletionTime;
        @ParcelField(3)
        Bundle mCustomCommandResult;
        @ParcelField(4)
        MediaItem mItem;

        /**
         * Constructor to be used by {@link SessionCallback#onCustomCommand(
         * MediaSession, ControllerInfo, SessionCommand, Bundle)}.
         *
         * @param resultCode result code
         * @param customCommandResult custom command result.
         */
        public SessionResult(@ResultCode int resultCode, @Nullable Bundle customCommandResult) {
            this(resultCode, customCommandResult, null, SystemClock.elapsedRealtime());
        }

        // For versioned-parcelable
        SessionResult() {
            // no-op
        }

        SessionResult(@ResultCode int resultCode) {
            this(resultCode, null);
        }

        SessionResult(@ResultCode int resultCode, @Nullable Bundle customCommandResult,
                @Nullable MediaItem item, long completionTime) {
            mResultCode = resultCode;
            mCustomCommandResult = customCommandResult;
            mItem = item;
            mCompletionTime = completionTime;
        }

        static @Nullable SessionResult from(@Nullable PlayerResult result) {
            if (result == null) {
                return null;
            }
            return new SessionResult(result.getResultCode(), null, result.getMediaItem(),
                    result.getCompletionTime());
        }

        static @Nullable SessionResult from(@Nullable ControllerResult result) {
            if (result == null) {
                return null;
            }
            return new SessionResult(result.getResultCode(), result.getCustomCommandResult(),
                    result.getMediaItem(), result.getCompletionTime());
        }

        static ListenableFuture<SessionResult> createFutureWithResult(@ResultCode int resultCode) {
            ResolvableFuture<SessionResult> result = ResolvableFuture.create();
            result.set(new SessionResult(resultCode));
            return result;
        }

        /**
         * Gets the result code.
         *
         * @return result code
         * @see #RESULT_CODE_SUCCESS
         * @see #RESULT_CODE_UNKNOWN_ERROR
         * @see #RESULT_CODE_INVALID_STATE
         * @see #RESULT_CODE_BAD_VALUE
         * @see #RESULT_CODE_PERMISSION_DENIED
         * @see #RESULT_CODE_IO_ERROR
         * @see #RESULT_CODE_SKIPPED
         * @see #RESULT_CODE_DISCONNECTED
         * @see #RESULT_CODE_NOT_SUPPORTED
         * @see #RESULT_CODE_AUTHENTICATION_EXPIRED
         * @see #RESULT_CODE_PREMIUM_ACCOUNT_REQUIRED
         * @see #RESULT_CODE_CONCURRENT_STREAM_LIMIT
         * @see #RESULT_CODE_PARENTAL_CONTROL_RESTRICTED
         * @see #RESULT_CODE_NOT_AVAILABLE_IN_REGION
         * @see #RESULT_CODE_SKIP_LIMIT_REACHED
         * @see #RESULT_CODE_SETUP_REQUIRED
         */
        @Override
        public @ResultCode int getResultCode() {
            return mResultCode;
        }

        /**
         * Gets the result of {@link #sendCustomCommand(ControllerInfo, SessionCommand, Bundle)}.
         * This is only valid when it's returned by the
         * {@link #sendCustomCommand(ControllerInfo, SessionCommand, Bundle)} and will be
         * {@code null} otherwise.
         *
         * @see #sendCustomCommand(ControllerInfo, SessionCommand, Bundle)
         * @return result of send custom command
         */
        public @Nullable Bundle getCustomCommandResult() {
            return mCustomCommandResult;
        }

        /**
         * Gets the completion time of the command. Being more specific, it's the same as
         * {@link android.os.SystemClock#elapsedRealtime()} when the command is completed.
         *
         * @return completion time of the command
         */
        @Override
        public long getCompletionTime() {
            return mCompletionTime;
        }

        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        @Override
        public MediaItem getMediaItem() {
            return mItem;
        }
    }
}