public class

MediaSession

extends java.lang.Object

implements java.io.Closeable

 java.lang.Object

↳androidx.media2.session.MediaSession

Subclasses:

MediaLibraryService.MediaLibrarySession

Gradle dependencies

compile group: 'androidx.media2', name: 'media2-session', version: '1.2.1'

  • groupId: androidx.media2
  • artifactId: media2-session
  • version: 1.2.1

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

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. See Supporting Multiple Sessions for detail.

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.

Topics covered here:

  1. Session Lifecycle
  2. Thread
  3. Media key events mapping
  4. Supporting Multiple Sessions
  5. Backward compatibility with legacy session APIs
  6. Backward compatibility with legacy controller APIs

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 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. The app is responsible for closing the underlying player after closing the session. is closed.

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)

Supporting Multiple Sessions

Generally speaking, multiple sessions aren't necessary for most media apps. One exception is if your app can play multiple media content at the same time, but only for the playback of video-only media or remote playback, since audio focus policy recommends not playing multiple audio content at the same time. Also keep in mind that multiple media sessions would make Android Auto and Bluetooth device with display to show your apps multiple times, because they list up media sessions, not media apps.

Backward compatibility with legacy session APIs

An active android.support.v4.media.session.MediaSessionCompat is internally created with the MediaSession for the backward compatibility. It's used to handle incoming connection and command from android.support.v4.media.session.MediaControllerCompat. And helps to utilize existing APIs that are built with legacy media session APIs. Use MediaSession.getSessionCompatToken() for getting the token for the underlying MediaSessionCompat.

Backward compatibility with legacy controller APIs

In addition to the media2 controller API, session also supports connection from the legacy controller API - and AndroidX controller compat. However, MediaSession.ControllerInfo may not be precise for legacy controller. See MediaSession.ControllerInfo for the details.

Unknown package name nor UID doesn't mean that you should disallow connection nor commands. For SDK levels where such issue happen, session tokens could only be obtained by trusted apps (e.g. Bluetooth, Auto, ...), so it may be better for you to allow them as you did with legacy session.

Summary

Methods
public voidbroadcastCustomCommand(SessionCommand command, Bundle args)

Broadcasts a 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 android.support.v4.media.session.MediaSessionCompat.TokengetSessionCompatToken()

Gets the for the MediaSessionCompat created internally by this session.

public SessionTokengetToken()

Returns the SessionToken for creating MediaController.

public booleanisClosed()

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

Sends a 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 voidsetLegacyControllerConnectionTimeoutMs(long timeoutMs)

Sets the timeout for disconnecting legacy controller.

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 a 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.

A command is not accepted if it is not a custom command.

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)

Sends a custom command to a specific controller.

A command is not accepted if it is not a custom command.

Parameters:

command: a command
args: optional argument

See also: MediaSession.broadcastCustomCommand(SessionCommand, Bundle)

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

public android.support.v4.media.session.MediaSessionCompat.Token getSessionCompatToken()

Gets the for the MediaSessionCompat created internally by this session.

Returns:

public void setLegacyControllerConnectionTimeoutMs(long timeoutMs)

Sets the timeout for disconnecting legacy controller.

Parameters:

timeoutMs: timeout in millis

Source

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

package androidx.media2.session;

import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.media2.session.SessionResult.RESULT_ERROR_NOT_SUPPORTED;
import static androidx.media2.session.SessionResult.RESULT_SUCCESS;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.view.KeyEvent;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.content.ContextCompat;
import androidx.core.util.ObjectsCompat;
import androidx.media.MediaSessionManager.RemoteUserInfo;
import androidx.media2.common.CallbackMediaItem;
import androidx.media2.common.MediaItem;
import androidx.media2.common.MediaMetadata;
import androidx.media2.common.Rating;
import androidx.media2.common.SessionPlayer;
import androidx.media2.common.SessionPlayer.BuffState;
import androidx.media2.common.SessionPlayer.PlayerResult;
import androidx.media2.common.SessionPlayer.PlayerState;
import androidx.media2.common.SessionPlayer.TrackInfo;
import androidx.media2.common.SubtitleData;
import androidx.media2.common.UriMediaItem;
import androidx.media2.common.VideoSize;
import androidx.media2.session.MediaController.PlaybackInfo;
import androidx.media2.session.MediaLibraryService.LibraryParams;
import androidx.media2.session.SessionResult.ResultCode;
import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelable;
import androidx.versionedparcelable.VersionedParcelize;

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

import java.io.Closeable;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Executor;

/**
 * Allows a media app to expose its transport controls and playback information in a process to
 * other processes including the Android framework and other apps. Common use cases are as follows.
 * <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. See
 * <a href="#MultipleSessions">Supporting Multiple Sessions</a> for detail.
 * <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>
 * Topics covered here:
 * <ol>
 * <li><a href="#SessionLifecycle">Session Lifecycle</a>
 * <li><a href="#Thread">Thread</a>
 * <li><a href="#KeyEvents">Media key events mapping</a>
 * <li><a href="#MultipleSessions">Supporting Multiple Sessions</a>
 * <li><a href="#CompatibilitySession">Backward compatibility with legacy session APIs</a>
 * <li><a href="#CompatibilityController">Backward compatibility with legacy controller APIs</a>
 *
 * </ol>
 * <h3 id="SessionLifecycle">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 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.
 * <h3 id="Thread">Thread</h3>
 * <p>
 * {@link MediaSession} objects are thread safe, but should be used on the thread on the looper.
 * <h3 id="KeyEvents">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>
 * <h3 id="MultipleSessions">Supporting Multiple Sessions</h3>
 * Generally speaking, multiple sessions aren't necessary for most media apps. One exception is if
 * your app can play multiple media content at the same time, but only for the playback of
 * video-only media or remote playback, since
 * <a href="{@docRoot}guide/topics/media-apps/audio-focus.html">audio focus policy</a> recommends
 * not playing multiple audio content at the same time. Also keep in mind that multiple media
 * sessions would make Android Auto and Bluetooth device with display to show your apps multiple
 * times, because they list up media sessions, not media apps.
 * <h3 id="CompatibilitySession">Backward compatibility with legacy session APIs</h3>
 * An active {@link MediaSessionCompat} is internally created with the MediaSession for the backward
 * compatibility. It's used to handle incoming connection and command from
 * {@link MediaControllerCompat}. And helps to utilize existing APIs that are built with legacy
 * media session APIs. Use {@link #getSessionCompatToken} for getting the token for the underlying
 * MediaSessionCompat.
 * <h3 id="CompatibilityController">Backward compatibility with legacy controller APIs</h3>
 * In addition to the {@link MediaController media2 controller} API, session also supports
 * connection from the legacy controller API -
 * {@link android.media.session.MediaController framework controller} and
 * {@link MediaControllerCompat AndroidX controller compat}.
 * However, {@link ControllerInfo} may not be precise for legacy controller.
 * See {@link ControllerInfo} for the details.
 * <p>
 * Unknown package name nor UID doesn't mean that you should disallow connection nor commands. For
 * SDK levels where such issue happen, session tokens could only be obtained by trusted apps (e.g.
 * Bluetooth, Auto, ...), so it may be better for you to allow them as you did with legacy session.
 *
 * @see MediaSessionService
 */
public class MediaSession implements Closeable {
    static final String TAG = "MediaSession";

    // It's better to have private static lock instead of using MediaSession.class because the
    // private lock object isn't exposed.
    private static final Object STATIC_LOCK = new Object();
    // Note: This checks the uniqueness of a session ID only in single process.
    // When the framework becomes able to check the uniqueness, this logic should be removed.
    @GuardedBy("STATIC_LOCK")
    private static final HashMap<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)
    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.
     */
    @NonNull
    public SessionPlayer getPlayer() {
        return mImpl.getPlayer();
    }

    /**
     * Gets the session ID
     *
     * @return
     */
    @NonNull
    public String getId() {
        return mImpl.getId();
    }

    /**
     * Returns the {@link SessionToken} for creating {@link MediaController}.
     */
    @NonNull
    public SessionToken getToken() {
        return mImpl.getToken();
    }

    @NonNull
    Context getContext() {
        return mImpl.getContext();
    }

    @NonNull
    Executor getCallbackExecutor() {
        return mImpl.getCallbackExecutor();
    }

    @NonNull
    SessionCallback getCallback() {
        return mImpl.getCallback();
    }

    /**
     * Returns the list of connected controller.
     *
     * @return list of {@link ControllerInfo}
     */
    @NonNull
    public List<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.
     */
    @NonNull
    public 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 a 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);
    }

    /**
     * Sends a 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)
     */
    @NonNull
    public 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
     */
    @RestrictTo(LIBRARY)
    public MediaSessionCompat getSessionCompat() {
        return mImpl.getSessionCompat();
    }

    /**
     * Gets the {@link MediaSessionCompat.Token} for the MediaSessionCompat created internally
     * by this session.
     *
     * @return {@link MediaSessionCompat.Token}
     */
    @NonNull
    public MediaSessionCompat.Token getSessionCompatToken() {
        return mImpl.getSessionCompat().getSessionToken();
    }

    /**
     * Sets the timeout for disconnecting legacy controller.
     * @param timeoutMs timeout in millis
     *
     * @hide
     */
    @RestrictTo(LIBRARY)
    public void setLegacyControllerConnectionTimeoutMs(long timeoutMs) {
        mImpl.setLegacyControllerConnectionTimeoutMs(timeoutMs);
    }

    /**
     * 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,
            int controllerVersion, String packageName, int pid, int uid,
            @Nullable Bundle connectionHints) {
        mImpl.connectFromService(controller, controllerVersion, 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)
         */
        @Nullable
        public SessionCommandGroup onConnect(@NonNull MediaSession session,
                @NonNull ControllerInfo controller) {
            SessionCommandGroup commands = new SessionCommandGroup.Builder()
                    .addAllPredefinedCommands(SessionCommand.COMMAND_VERSION_CURRENT)
                    .build();
            return commands;
        }

        /**
         * Called immediately after a controller is connected. This is a convenient method to add
         * custom initialization between the session and a controller.
         * <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.
         * <p>
         * Interoperability: For legacy controller, this is called when the controller doesn't send
         * any command for a while. It's because there were no explicit disconnect API in legacy
         * controller API.
         *
         * @param session the session for this event
         * @param controller controller information
         */
        public void onDisconnected(@NonNull MediaSession session,
                @NonNull ControllerInfo controller) {}

        /**
         * Called when a controller sent a command which will be sent directly to one of the
         * following:
         * <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
         */
        @ResultCode
        public int onCommandRequest(@NonNull MediaSession session,
                @NonNull ControllerInfo controller, @NonNull SessionCommand command) {
            return RESULT_SUCCESS;
        }

        /**
         * Called when a controller has sent a command with a {@link MediaItem} to add a new media
         * item to this session. Being specific, this will be called for following APIs.
         * <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
         */
        @Nullable
        public MediaItem onCreateMediaItem(@NonNull MediaSession session,
                @NonNull ControllerInfo controller, @NonNull String mediaId) {
            return null;
        }

        /**
         * Called when a controller set rating of a media item through
         * {@link MediaController#setRating(String, Rating)}.
         * <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
         */
        @ResultCode
        public int onSetRating(@NonNull MediaSession session,
                @NonNull ControllerInfo controller, @NonNull String mediaId,
                @NonNull Rating rating) {
            return RESULT_ERROR_NOT_SUPPORTED;
        }

        /**
         * Called when a controller requested to set the specific media item(s) represented by a URI
         * through {@link MediaController#setMediaUri(Uri, Bundle)}.
         * <p>
         * The implementation should create proper {@link MediaItem media item(s)} for the given
         * {@code uri} and call {@link SessionPlayer#setMediaItem} or
         * {@link SessionPlayer#setPlaylist}.
         * <p>
         * When {@link MediaControllerCompat} is connected and sends commands with following
         * methods, the {@code uri} would have the following patterns:
         * <table>
         * <tr>
         * <th>Method</th><th align="left">Uri pattern</th>
         * </tr><tr>
         * <td>{@link MediaControllerCompat.TransportControls#prepareFromUri prepareFromUri}
         * </td><td>The {@code uri} passed as argument</td>
         * </tr><tr>
         * <td>{@link MediaControllerCompat.TransportControls#prepareFromMediaId prepareFromMediaId}
         * </td><td>{@code androidx://media2-session/prepareFromMediaId?id=[mediaId]}</td>
         * </tr><tr>
         * <td>{@link MediaControllerCompat.TransportControls#prepareFromSearch prepareFromSearch}
         * </td><td>{@code androidx://media2-session/prepareFromSearch?query=[query]}</td>
         * </tr><tr>
         * <td>{@link MediaControllerCompat.TransportControls#playFromUri playFromUri}
         * </td><td>The {@code uri} passed as argument</td>
         * </tr><tr>
         * <td>{@link MediaControllerCompat.TransportControls#playFromMediaId playFromMediaId}
         * </td><td>{@code androidx://media2-session/playFromMediaId?id=[mediaId]}</td>
         * </tr><tr>
         * <td>{@link MediaControllerCompat.TransportControls#playFromSearch playFromSearch}
         * </td><td>{@code androidx://media2-session/playFromSearch?query=[query]}</td>
         * </tr></table>
         * <p>
         * {@link SessionPlayer#prepare()} or {@link SessionPlayer#play()} would be followed if
         * this is called by above methods.
         *
         * @param session the session for this event
         * @param controller controller information
         * @param uri uri
         * @param extras optional extra bundle
         */
        @ResultCode
        public int onSetMediaUri(@NonNull MediaSession session,
                @NonNull ControllerInfo controller, @NonNull Uri uri, @Nullable Bundle extras) {
            return RESULT_ERROR_NOT_SUPPORTED;
        }

        /**
         * Called when a controller sent a custom command through
         * {@link MediaController#sendCustomCommand(SessionCommand, Bundle)}.
         * <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
         */
        @NonNull
        public SessionResult onCustomCommand(@NonNull MediaSession session,
                @NonNull ControllerInfo controller, @NonNull SessionCommand customCommand,
                @Nullable Bundle args) {
            return new SessionResult(RESULT_ERROR_NOT_SUPPORTED, null);
        }

        /**
         * Called when a controller called {@link MediaController#fastForward()}.
         * <p>
         * It can be implemented in many ways. For example, it can be implemented by seeking forward
         * once, series of seeking forward, or increasing playback speed.
         *
         * @param session the session for this event
         * @param controller controller information
         * @see SessionCommand#COMMAND_CODE_SESSION_FAST_FORWARD
         */
        @ResultCode
        public int onFastForward(@NonNull MediaSession session,
                @NonNull ControllerInfo controller) {
            return RESULT_ERROR_NOT_SUPPORTED;
        }

        /**
         * Called when a controller called {@link MediaController#rewind()}.
         * <p>
         * It can be implemented in many ways. For example, it can be implemented by seeking
         * backward once, series of seeking backward, or decreasing playback speed.
         *
         * @param session the session for this event
         * @param controller controller information
         * @see SessionCommand#COMMAND_CODE_SESSION_REWIND
         */
        @ResultCode
        public int onRewind(@NonNull MediaSession session, @NonNull ControllerInfo controller) {
            return RESULT_ERROR_NOT_SUPPORTED;
        }

        /**
         * Called when a controller called {@link MediaController#skipForward()}.
         * <p>
         * It's recommended to seek forward within the current media item, but its detail may vary.
         * For example, it can be implemented by seeking forward for the fixed amount of seconds, or
         * seeking forward to the nearest bookmark.
         *
         * @param session the session for this event
         * @param controller controller information
         * @see SessionCommand#COMMAND_CODE_SESSION_SKIP_FORWARD
         */
        @ResultCode
        public int onSkipForward(@NonNull MediaSession session,
                @NonNull ControllerInfo controller) {
            return RESULT_ERROR_NOT_SUPPORTED;
        }

        /**
         * Called when a controller called {@link MediaController#skipBackward()}.
         * <p>
         * It's recommended to seek backward within the current media item, but its detail may vary.
         * For example, it can be implemented by seeking backward for the fixed amount of seconds,
         * or seeking backward to the nearest bookmark.
         *
         * @param session the session for this event
         * @param controller controller information
         * @see SessionCommand#COMMAND_CODE_SESSION_SKIP_BACKWARD
         */
        @ResultCode
        public int onSkipBackward(@NonNull MediaSession session,
                @NonNull ControllerInfo controller) {
            return RESULT_ERROR_NOT_SUPPORTED;
        }

        /**
         * Called when the player state is changed. Used internally for setting the
         * {@link MediaSessionService} as foreground/background.
         */
        final void onPlayerStateChanged(MediaSession session, @PlayerState int state) {
            if (mForegroundServiceEventCallback != null) {
                mForegroundServiceEventCallback.onPlayerStateChanged(session, state);
            }
        }

        final void onCurrentMediaItemChanged(MediaSession session) {
            if (mForegroundServiceEventCallback != null) {
                mForegroundServiceEventCallback.onNotificationUpdateNeeded(session);
            }
        }

        final void onSessionClosed(MediaSession session) {
            if (mForegroundServiceEventCallback != null) {
                mForegroundServiceEventCallback.onSessionClosed(session);
            }
        }

        void setForegroundServiceEventCallback(ForegroundServiceEventCallback callback) {
            mForegroundServiceEventCallback = callback;
        }

        abstract static class ForegroundServiceEventCallback {
            public void onPlayerStateChanged(MediaSession session, @PlayerState int state) {}
            public void onNotificationUpdateNeeded(MediaSession session) {}
            public void onSessionClosed(MediaSession session) {}
        }
    }

    /**
     * Builder for {@link MediaSession}.
     * <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
        @NonNull
        public Builder setSessionActivity(@Nullable PendingIntent pi) {
            return super.setSessionActivity(pi);
        }

        @Override
        @NonNull
        public 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
        @NonNull
        public Builder setExtras(@NonNull Bundle extras) {
            return super.setExtras(extras);
        }

        @Override
        @NonNull
        public 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 {
        @SuppressWarnings("UnusedVariable")
        private final int mControllerVersion;
        private final RemoteUserInfo mRemoteUserInfo;
        private final boolean mIsTrusted;
        private final ControllerCb mControllerCb;
        private final Bundle mConnectionHints;

        /**
         * @param remoteUserInfo remote user info
         * @param version connected controller version
         * @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.
         */
        ControllerInfo(@NonNull RemoteUserInfo remoteUserInfo, int version, boolean trusted,
                @Nullable ControllerCb cb, @Nullable Bundle connectionHints) {
            mRemoteUserInfo = remoteUserInfo;
            mControllerVersion = version;
            mIsTrusted = trusted;
            mControllerCb = cb;
            if (connectionHints == null
                    || MediaUtils.doesBundleHaveCustomParcelable(connectionHints)) {
                mConnectionHints = null;
            } else {
                mConnectionHints = connectionHints;
            }
        }

        RemoteUserInfo getRemoteUserInfo() {
            return mRemoteUserInfo;
        }

        /**
         * Gets the package name. Can be
         * {@link androidx.media.MediaSessionManager.RemoteUserInfo#LEGACY_CONTROLLER} for
         * interoperability.
         * <p>
         * Interoperability: Package name may not be precisely obtained for legacy controller API on
         * older device. Here are details.
         * <table>
         * <tr><th>SDK version when package name isn't precise</th>
         *     <th>{@code ControllerInfo#getPackageName()} for legacy controller</th>
         * <tr><td>{@code SDK_VERSION} &lt; {@code 21}</td>
         *     <td>Actual package name via {@link PackageManager#getNameForUid} with UID.<br>
         *         It's sufficient for most cases, but doesn't precisely distinguish caller if it
         *         uses shared user ID.</td>
         * <tr><td>{@code 21} &le; {@code SDK_VERSION} &lt; {@code 24}</td>
         *     <td>{@link RemoteUserInfo#LEGACY_CONTROLLER LEGACY_CONTROLLER}</td>
         * </table>
         *
         * @return package name of the controller. Can be
         *         {@link RemoteUserInfo#LEGACY_CONTROLLER LEGACY_CONTROLLER} if the package name
         *         cannot be obtained.
         */
        @NonNull
        public String getPackageName() {
            return mRemoteUserInfo.getPackageName();
        }

        /**
         * Gets the UID of the controller. Can be a negative value for interoperability.
         * <p>
         * Interoperability: If {@code 21} &le; {@code SDK_VERSION} &lt; {@code 28}, then UID would
         * be a negative value because it cannot be obtained.
         *
         * @return uid of the controller. Can be a negative value if the uid cannot be obtained.
         */
        public int getUid() {
            return mRemoteUserInfo.getUid();
        }

        /**
         * Gets the connection hints sent from controller, or {@link Bundle#EMPTY} if none.
         */
        @NonNull
        public Bundle getConnectionHints() {
            return mConnectionHints == null ? Bundle.EMPTY : new Bundle(mConnectionHints);
        }

        /**
         * Returns if the controller has been granted
         * {@code android.permission.MEDIA_CONTENT_CONTROL} or has a enabled notification listener
         * so can be trusted to accept connection and incoming command request.
         *
         * @return {@code true} if the controller is trusted.
         * @hide
         */
        @RestrictTo(LIBRARY)
        public boolean isTrusted() {
            return mIsTrusted;
        }

        @Override
        public int hashCode() {
            return ObjectsCompat.hash(mControllerCb, mRemoteUserInfo);
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof ControllerInfo)) {
                return false;
            }
            if (this == obj) {
                return true;
            }
            ControllerInfo other = (ControllerInfo) obj;
            if (mControllerCb != null || other.mControllerCb != null) {
                return ObjectsCompat.equals(mControllerCb, other.mControllerCb);
            }
            return mRemoteUserInfo.equals(other.mRemoteUserInfo);
        }

        @Override
        public String toString() {
            return "ControllerInfo {pkg=" + mRemoteUserInfo.getPackageName() + ", uid="
                    + mRemoteUserInfo.getUid() + "})";
        }

        @Nullable ControllerCb getControllerCb() {
            return mControllerCb;
        }

        @NonNull
        static ControllerInfo createLegacyControllerInfo() {
            RemoteUserInfo legacyRemoteUserInfo =
                    new RemoteUserInfo(
                            RemoteUserInfo.LEGACY_CONTROLLER,
                            /* pid= */ RemoteUserInfo.UNKNOWN_PID,
                            /* uid= */ RemoteUserInfo.UNKNOWN_UID);
            return new ControllerInfo(
                    legacyRemoteUserInfo,
                    MediaUtils.VERSION_UNKNOWN,
                    /* trusted= */ false,
                    /* cb= */ null,
                    /* connectionHints= */ null);
        }
    }

    /**
     * Button for a {@link SessionCommand} that will be shown by the controller.
     * <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;

        // WARNING: Adding a new ParcelField may break old library users (b/152830728)

        /**
         * Used for VersionedParcelable
         */
        CommandButton() {
        }

        CommandButton(@Nullable SessionCommand command, int iconResId,
                @Nullable CharSequence displayName, Bundle extras, boolean enabled) {
            mCommand = command;
            mIconResId = iconResId;
            mDisplayName = displayName;
            mExtras = extras;
            mEnabled = enabled;
        }

        /**
         * Gets the command associated with this button. Can be {@code null} if the button isn't
         * enabled and only providing placeholder.
         *
         * @return command or {@code null}
         */
        @Nullable
        public SessionCommand getCommand() {
            return mCommand;
        }

        /**
         * Gets the resource id of the button in this package. Can be {@code 0} if the command is
         * predefined and custom icon isn't needed.
         *
         * @return resource id of the icon. Can be {@code 0}.
         */
        public int getIconResId() {
            return mIconResId;
        }

        /**
         * Gets the display name of the button. Can be {@code null} or empty if the command is
         * predefined and custom name isn't needed.
         *
         * @return custom display name. Can be {@code null} or empty.
         */
        @Nullable
        public CharSequence getDisplayName() {
            return mDisplayName;
        }

        /**
         * Gets extra information of the button. It's private information between session and
         * controller.
         *
         * @return
         */
        @Nullable
        public Bundle getExtras() {
            return mExtras;
        }

        /**
         * Returns whether it's enabled.
         *
         * @return {@code true} if enabled. {@code false} otherwise.
         */
        public boolean isEnabled() {
            return mEnabled;
        }

        /**
         * Builder for {@link CommandButton}.
         */
        public static final class Builder {
            private SessionCommand mCommand;
            private int mIconResId;
            private CharSequence mDisplayName;
            private Bundle mExtras;
            private boolean mEnabled;

            /**
             * Sets the {@link SessionCommand} that would be sent to the session when the button
             * is clicked.
             *
             * @param command session command
             */
            @NonNull
            public Builder setCommand(@Nullable SessionCommand command) {
                mCommand = command;
                return this;
            }

            /**
             * Sets the bitmap-type (e.g. PNG) icon resource id of the button.
             * <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
             */
            @NonNull
            public Builder setIconResId(int resId) {
                mIconResId = resId;
                return this;
            }

            /**
             * Sets the display name of the button.
             *
             * @param displayName display name of the button
             */
            @NonNull
            public Builder setDisplayName(@Nullable CharSequence displayName) {
                mDisplayName = displayName;
                return this;
            }

            /**
             * Sets whether the button is enabled. Can be {@code false} to indicate that the button
             * should be shown but isn't clickable.
             *
             * @param enabled {@code true} if the button is enabled and ready.
             *          {@code false} otherwise.
             */
            @NonNull
            public Builder setEnabled(boolean enabled) {
                mEnabled = enabled;
                return this;
            }

            /**
             * Sets the extras of the button.
             *
             * @param extras extras information of the button
             */
            @NonNull
            public Builder setExtras(@Nullable Bundle extras) {
                mExtras = extras;
                return this;
            }

            /**
             * Builds the {@link CommandButton}.
             *
             * @return a new {@link CommandButton}
             */
            @NonNull
            public CommandButton build() {
                return new CommandButton(mCommand, mIconResId, mDisplayName, mExtras, mEnabled);
            }
        }
    }

    // TODO: Drop 'Cb' from the name.
    abstract static class ControllerCb {
        abstract void onPlayerResult(int seq, PlayerResult result) throws RemoteException;
        abstract void onSessionResult(int seq, SessionResult result) throws RemoteException;
        abstract void onLibraryResult(int seq, LibraryResult result) throws RemoteException;
        abstract void onPlayerChanged(int seq, @Nullable SessionPlayer oldPlayer,
                @Nullable PlaybackInfo oldPlaybackInfo, @NonNull SessionPlayer player,
                @NonNull PlaybackInfo playbackInfo) 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 VideoSize videoSize)
                throws RemoteException;
        abstract void onTracksChanged(int seq, List<TrackInfo> tracks,
                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, Closeable {
        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();
        @NonNull MediaSessionCompat getSessionCompat();
        void setLegacyControllerConnectionTimeoutMs(long timeoutMs);
        Context getContext();
        Executor getCallbackExecutor();
        SessionCallback getCallback();
        boolean isClosed();
        PlaybackStateCompat createPlaybackStateCompat();
        PlaybackInfo getPlaybackInfo();
        PendingIntent getSessionActivity();
        IBinder getLegacyBrowserServiceBinder();
        void connectFromService(IMediaController caller, int controllerVersion, 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)
    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 = "";
        }

        /**
         * Sets 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.
         */
        @SuppressWarnings("unchecked")
        @NonNull
        U setSessionActivity(@Nullable PendingIntent pi) {
            mSessionActivity = pi;
            return (U) this;
        }

        /**
         * Sets the 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.
        @SuppressWarnings("unchecked")
        @NonNull
        U setId(@NonNull String id) {
            if (id == null) {
                throw new NullPointerException("id shouldn't be null");
            }
            mId = id;
            return (U) this;
        }

        /**
         * Sets callback for the session.
         *
         * @param executor callback executor
         * @param callback session callback
         * @return
         */
        @SuppressWarnings("unchecked")
        @NonNull
        U setSessionCallback(@NonNull Executor executor, @NonNull C callback) {
            if (executor == null) {
                throw new NullPointerException("executor shouldn't be null");
            }
            if (callback == null) {
                throw new NullPointerException("callback shouldn't be null");
            }
            mCallbackExecutor = executor;
            mCallback = callback;
            return (U) this;
        }

        /**
         * Sets extras for the session token.  If not set, {@link SessionToken#getExtras()}
         * will return an empty {@link Bundle}.
         *
         * @return the Builder to allow chaining
         * @throws IllegalArgumentException if the bundle contains any non-framework Parcelable
         * objects.
         * @see SessionToken#getExtras()
         */
        @NonNull
        @SuppressWarnings("unchecked")
        U setExtras(@NonNull Bundle extras) {
            if (extras == null) {
                throw new NullPointerException("extras shouldn't be null");
            }
            if (MediaUtils.doesBundleHaveCustomParcelable(extras)) {
                throw new IllegalArgumentException(
                        "extras shouldn't contain any custom parcelables");
            }
            mExtras = new Bundle(extras);
            return (U) this;
        }

        /**
         * Builds a {@link MediaSession}.
         *
         * @return a new session
         * @throws IllegalStateException if the session with the same id already exists for the
         *      package.
         */
        @NonNull abstract T build();
    }
}