androidx.media.MediaSession2.java Source code

Java tutorial

Introduction

Here is the source code for androidx.media.MediaSession2.java

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.media;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;

import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.AudioFocusRequest;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ResultReceiver;
import android.support.v4.media.session.IMediaControllerCallback;
import android.support.v4.media.session.PlaybackStateCompat;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.media.MediaController2.PlaybackInfo;
import androidx.media.MediaPlayerBase.BuffState;
import androidx.media.MediaPlayerBase.PlayerState;
import androidx.media.MediaPlaylistAgent.PlaylistEventCallback;
import androidx.media.MediaPlaylistAgent.RepeatMode;
import androidx.media.MediaPlaylistAgent.ShuffleMode;

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 MediaSession2 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>
 * 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 MediaController2} 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>
 * {@link MediaSession2} objects should be used on the thread on the looper.
 */
@TargetApi(Build.VERSION_CODES.KITKAT)
public class MediaSession2 extends MediaInterface2.SessionPlayer implements AutoCloseable {
    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    @IntDef({ ERROR_CODE_UNKNOWN_ERROR, ERROR_CODE_APP_ERROR, ERROR_CODE_NOT_SUPPORTED,
            ERROR_CODE_AUTHENTICATION_EXPIRED, ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED,
            ERROR_CODE_CONCURRENT_STREAM_LIMIT, ERROR_CODE_PARENTAL_CONTROL_RESTRICTED,
            ERROR_CODE_NOT_AVAILABLE_IN_REGION, ERROR_CODE_CONTENT_ALREADY_PLAYING, ERROR_CODE_SKIP_LIMIT_REACHED,
            ERROR_CODE_ACTION_ABORTED, ERROR_CODE_END_OF_QUEUE, ERROR_CODE_SETUP_REQUIRED })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ErrorCode {
    }

    /**
     * This is the default error code and indicates that none of the other error codes applies.
     */
    public static final int ERROR_CODE_UNKNOWN_ERROR = 0;

    /**
     * Error code when the application state is invalid to fulfill the request.
     */
    public static final int ERROR_CODE_APP_ERROR = 1;

    /**
     * Error code when the request is not supported by the application.
     */
    public static final int ERROR_CODE_NOT_SUPPORTED = 2;

    /**
     * Error code when the request cannot be performed because authentication has expired.
     */
    public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = 3;

    /**
     * Error code when a premium account is required for the request to succeed.
     */
    public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = 4;

    /**
     * Error code when too many concurrent streams are detected.
     */
    public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = 5;

    /**
     * Error code when the content is blocked due to parental controls.
     */
    public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = 6;

    /**
     * Error code when the content is blocked due to being regionally unavailable.
     */
    public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = 7;

    /**
     * Error code when the requested content is already playing.
     */
    public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = 8;

    /**
     * Error code when the application cannot skip any more songs because skip limit is reached.
     */
    public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9;

    /**
     * Error code when the action is interrupted due to some external event.
     */
    public static final int ERROR_CODE_ACTION_ABORTED = 10;

    /**
     * Error code when the playback navigation (previous, next) is not possible because the queue
     * was exhausted.
     */
    public static final int ERROR_CODE_END_OF_QUEUE = 11;

    /**
     * Error code when the session needs user's manual intervention.
     */
    public static final int ERROR_CODE_SETUP_REQUIRED = 12;

    /**
     * Interface definition of a callback to be invoked when a {@link MediaItem2} in the playlist
     * didn't have a {@link DataSourceDesc} but it's needed now for preparing or playing it.
     *
     * #see #setOnDataSourceMissingHelper
     */
    public interface OnDataSourceMissingHelper {
        /**
         * Called when a {@link MediaItem2} in the playlist didn't have a {@link DataSourceDesc}
         * but it's needed now for preparing or playing it. Returned data source descriptor will be
         * sent to the player directly to prepare or play the contents.
         * <p>
         * An exception may be thrown if the returned {@link DataSourceDesc} is duplicated in the
         * playlist, so items cannot be differentiated.
         *
         * @param session the session for this event
         * @param item media item from the controller
         * @return a data source descriptor if the media item. Can be {@code null} if the content
         *        isn't available.
         */
        @Nullable
        DataSourceDesc onDataSourceMissing(@NonNull MediaSession2 session, @NonNull MediaItem2 item);
    }

    /**
     * Callback to be called for all incoming commands from {@link MediaController2}s.
     * <p>
     * If it's not set, the session will accept all controllers and all incoming commands by
     * default.
     */
    public abstract static class SessionCallback {
        /**
         * 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 MediaController2.ControllerCallback#onDisconnected(MediaController2)} 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 SessionCommandGroup2 onConnect(@NonNull MediaSession2 session,
                @NonNull ControllerInfo controller) {
            SessionCommandGroup2 commands = new SessionCommandGroup2();
            commands.addAllPredefinedCommands();
            return commands;
        }

        /**
         * Called when a controller is disconnected
         *
         * @param session the session for this event
         * @param controller controller information
         */
        public void onDisconnected(@NonNull MediaSession2 session, @NonNull ControllerInfo controller) {
        }

        /**
         * Called when a controller sent a command which will be sent directly to one of the
         * following:
         * <ul>
         *  <li> {@link MediaPlayerBase} </li>
         *  <li> {@link MediaPlaylistAgent} </li>
         *  <li> {@link android.media.AudioManager} or {@link VolumeProviderCompat} </li>
         * </ul>
         * Return {@code false} here to reject the request and stop sending command.
         *
         * @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 true} if you want to accept incoming command. {@code false} otherwise.
         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PLAY
         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PAUSE
         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_RESET
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM
         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_PREPARE
         * @see SessionCommand2#COMMAND_CODE_PLAYBACK_SEEK_TO
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REMOVE_ITEM
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_LIST
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST_METADATA
         * @see SessionCommand2#COMMAND_CODE_PLAYLIST_SET_LIST_METADATA
         * @see SessionCommand2#COMMAND_CODE_VOLUME_SET_VOLUME
         * @see SessionCommand2#COMMAND_CODE_VOLUME_ADJUST_VOLUME
         */
        public boolean onCommandRequest(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
                @NonNull SessionCommand2 command) {
            return true;
        }

        /**
         * Called when a controller set rating of a media item through
         * {@link MediaController2#setRating(String, Rating2)}.
         * <p>
         * To allow setting user rating for a {@link MediaItem2}, the media item's metadata
         * should have {@link Rating2} with the key {@link MediaMetadata2#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 media id from the controller
         * @param rating new rating from the controller
         * @see SessionCommand2#COMMAND_CODE_SESSION_SET_RATING
         */
        public void onSetRating(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
                @NonNull String mediaId, @NonNull Rating2 rating) {
        }

        /**
         * Called when a controller sent a custom command through
         * {@link MediaController2#sendCustomCommand(SessionCommand2, Bundle, ResultReceiver)}.
         *
         * @param session the session for this event
         * @param controller controller information
         * @param customCommand custom command.
         * @param args optional arguments
         * @param cb optional result receiver
         * @see SessionCommand2#COMMAND_CODE_CUSTOM
         */
        public void onCustomCommand(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
                @NonNull SessionCommand2 customCommand, @Nullable Bundle args, @Nullable ResultReceiver cb) {
        }

        /**
         * Called when a controller requested to play a specific mediaId through
         * {@link MediaController2#playFromMediaId(String, Bundle)}.
         *
         * @param session the session for this event
         * @param controller controller information
         * @param mediaId media id
         * @param extras optional extra bundle
         * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID
         */
        public void onPlayFromMediaId(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
                @NonNull String mediaId, @Nullable Bundle extras) {
        }

        /**
         * Called when a controller requested to begin playback from a search query through
         * {@link MediaController2#playFromSearch(String, Bundle)}
         * <p>
         * An empty query indicates that the app may play any music. The implementation should
         * attempt to make a smart choice about what to play.
         *
         * @param session the session for this event
         * @param controller controller information
         * @param query query string. Can be empty to indicate any suggested media
         * @param extras optional extra bundle
         * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_SEARCH
         */
        public void onPlayFromSearch(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
                @NonNull String query, @Nullable Bundle extras) {
        }

        /**
         * Called when a controller requested to play a specific media item represented by a URI
         * through {@link MediaController2#playFromUri(Uri, Bundle)}
         *
         * @param session the session for this event
         * @param controller controller information
         * @param uri uri
         * @param extras optional extra bundle
         * @see SessionCommand2#COMMAND_CODE_SESSION_PLAY_FROM_URI
         */
        public void onPlayFromUri(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
                @NonNull Uri uri, @Nullable Bundle extras) {
        }

        /**
         * Called when a controller requested to prepare for playing a specific mediaId through
         * {@link MediaController2#prepareFromMediaId(String, Bundle)}.
         * <p>
         * During the preparation, a session should not hold audio focus in order to allow other
         * sessions play seamlessly. The state of playback should be updated to
         * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
         * <p>
         * The playback of the prepared content should start in the later calls of
         * {@link MediaSession2#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 media id to prepare
         * @param extras optional extra bundle
         * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID
         */
        public void onPrepareFromMediaId(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
                @NonNull String mediaId, @Nullable Bundle extras) {
        }

        /**
         * Called when a controller requested to prepare playback from a search query through
         * {@link MediaController2#prepareFromSearch(String, Bundle)}.
         * <p>
         * An empty query indicates that the app may prepare any music. The implementation should
         * attempt to make a smart choice about what to play.
         * <p>
         * The state of playback should be updated to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}
         * after the preparation is done. The playback of the prepared content should start in the
         * later calls of {@link MediaSession2#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 query string. Can be empty to indicate any suggested media
         * @param extras optional extra bundle
         * @see SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH
         */
        public void onPrepareFromSearch(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
                @NonNull String query, @Nullable Bundle extras) {
        }

        /**
         * Called when a controller requested to prepare a specific media item represented by a URI
         * through {@link MediaController2#prepareFromUri(Uri, Bundle)}.
         * <p>
         * During the preparation, a session should not hold audio focus in order to allow
         * other sessions play seamlessly. The state of playback should be updated to
         * {@link MediaPlayerBase#PLAYER_STATE_PAUSED} after the preparation is done.
         * <p>
         * The playback of the prepared content should start in the later calls of
         * {@link MediaSession2#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 SessionCommand2#COMMAND_CODE_SESSION_PREPARE_FROM_URI
         */
        public void onPrepareFromUri(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
                @NonNull Uri uri, @Nullable Bundle extras) {
        }

        /**
         * Called when a controller called {@link MediaController2#fastForward()}
         *
         * @param session the session for this event
         * @param controller controller information
         * @see SessionCommand2#COMMAND_CODE_SESSION_FAST_FORWARD
         */
        public void onFastForward(@NonNull MediaSession2 session, ControllerInfo controller) {
        }

        /**
         * Called when a controller called {@link MediaController2#rewind()}
         *
         * @param session the session for this event
         * @param controller controller information
         * @see SessionCommand2#COMMAND_CODE_SESSION_REWIND
         */
        public void onRewind(@NonNull MediaSession2 session, ControllerInfo controller) {
        }

        /**
         * Called when a controller called {@link MediaController2#subscribeRoutesInfo()}
         * Session app should notify the routes information by calling
         * {@link MediaSession2#notifyRoutesInfoChanged(ControllerInfo, List)}.
         *
         * @param session the session for this event
         * @param controller controller information
         * @see SessionCommand2#COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO
         */
        public void onSubscribeRoutesInfo(@NonNull MediaSession2 session, @NonNull ControllerInfo controller) {
        }

        /**
         * Called when a controller called {@link MediaController2#unsubscribeRoutesInfo()}
         *
         * @param session the session for this event
         * @param controller controller information
         * @see SessionCommand2#COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO
         */
        public void onUnsubscribeRoutesInfo(@NonNull MediaSession2 session, @NonNull ControllerInfo controller) {
        }

        /**
         * Called when a controller called {@link MediaController2#selectRoute(Bundle)}.
         * @param session the session for this event
         * @param controller controller information
         * @param route The route bundle which may be from MediaRouteDescritor.asBundle().
         * @see SessionCommand2#COMMAND_CODE_SESSION_SELECT_ROUTE
         */
        public void onSelectRoute(@NonNull MediaSession2 session, @NonNull ControllerInfo controller,
                @NonNull Bundle route) {
        }

        /**
         * Called when the player's current playing item is changed
         * <p>
         * When it's called, you should invalidate previous playback information and wait for later
         * callbacks.
         *
         * @param session the controller for this event
         * @param player the player for this event
         * @param item new item
         */
        public void onCurrentMediaItemChanged(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player,
                @NonNull MediaItem2 item) {
        }

        /**
         * Called when the player is <i>prepared</i>, i.e. it is ready to play the content
         * referenced by the given data source.
         * @param session the session for this event
         * @param player the player for this event
         * @param item the media item for which buffering is happening
         */
        public void onMediaPrepared(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player,
                @NonNull MediaItem2 item) {
        }

        /**
         * Called to indicate that the state of the player has changed.
         * See {@link MediaPlayerBase#getPlayerState()} for polling the player state.
         * @param session the session for this event
         * @param player the player for this event
         * @param state the new state of the player.
         */
        public void onPlayerStateChanged(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player,
                @PlayerState int state) {
        }

        /**
         * Called to report buffering events for a data source.
         *
         * @param session the session for this event
         * @param player the player for this event
         * @param item the media item for which buffering is happening.
         * @param state the new buffering state.
         */
        public void onBufferingStateChanged(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player,
                @NonNull MediaItem2 item, @BuffState int state) {
        }

        /**
         * Called to indicate that the playback speed has changed.
         * @param session the session for this event
         * @param player the player for this event
         * @param speed the new playback speed.
         */
        public void onPlaybackSpeedChanged(@NonNull MediaSession2 session, @NonNull MediaPlayerBase player,
                float speed) {
        }

        /**
         * Called to indicate that {@link #seekTo(long)} is completed.
         *
         * @param session the session for this event.
         * @param mpb the player that has completed seeking.
         * @param position the previous seeking request.
         * @see #seekTo(long)
         */
        public void onSeekCompleted(@NonNull MediaSession2 session, @NonNull MediaPlayerBase mpb, long position) {
        }

        /**
         * Called when a playlist is changed from the {@link MediaPlaylistAgent}.
         * <p>
         * This is called when the underlying agent has called
         * {@link PlaylistEventCallback#onPlaylistChanged(MediaPlaylistAgent,
         * List, MediaMetadata2)}.
         *
         * @param session the session for this event
         * @param playlistAgent playlist agent for this event
         * @param list new playlist
         * @param metadata new metadata
         */
        public void onPlaylistChanged(@NonNull MediaSession2 session, @NonNull MediaPlaylistAgent playlistAgent,
                @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
        }

        /**
         * Called when a playlist metadata is changed.
         *
         * @param session the session for this event
         * @param playlistAgent playlist agent for this event
         * @param metadata new metadata
         */
        public void onPlaylistMetadataChanged(@NonNull MediaSession2 session,
                @NonNull MediaPlaylistAgent playlistAgent, @Nullable MediaMetadata2 metadata) {
        }

        /**
         * Called when the shuffle mode is changed.
         *
         * @param session the session for this event
         * @param playlistAgent playlist agent for this event
         * @param shuffleMode repeat mode
         * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
         * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
         * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
         */
        public void onShuffleModeChanged(@NonNull MediaSession2 session, @NonNull MediaPlaylistAgent playlistAgent,
                @MediaPlaylistAgent.ShuffleMode int shuffleMode) {
        }

        /**
         * Called when the repeat mode is changed.
         *
         * @param session the session for this event
         * @param playlistAgent playlist agent for this event
         * @param repeatMode repeat mode
         * @see MediaPlaylistAgent#REPEAT_MODE_NONE
         * @see MediaPlaylistAgent#REPEAT_MODE_ONE
         * @see MediaPlaylistAgent#REPEAT_MODE_ALL
         * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
         */
        public void onRepeatModeChanged(@NonNull MediaSession2 session, @NonNull MediaPlaylistAgent playlistAgent,
                @MediaPlaylistAgent.RepeatMode int repeatMode) {
        }
    }

    /**
     * Base builder class for MediaSession2 and its subclass. Any change in this class should be
     * also applied to the subclasses {@link MediaSession2.Builder} and
     * {@link MediaLibraryService2.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 android.media.MediaSession2,
     *      U extends android.media.MediaSession2.BuilderBase<
     *              T, U, C extends android.media.MediaSession2.SessionCallback>, C></pre>
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    abstract static class BuilderBase<T extends MediaSession2, U extends BuilderBase<T, U, C>, C extends SessionCallback> {
        final Context mContext;
        MediaSession2ImplBase.BuilderBase<T, C> mBaseImpl;
        MediaPlayerBase mPlayer;
        String mId;
        Executor mCallbackExecutor;
        C mCallback;
        MediaPlaylistAgent mPlaylistAgent;
        VolumeProviderCompat mVolumeProvider;
        PendingIntent mSessionActivity;

        BuilderBase(Context context) {
            if (context == null) {
                throw new IllegalArgumentException("context shouldn't be null");
            }
            mContext = context;
            // Ensure non-null
            mId = "";
        }

        /**
         * Sets the underlying {@link MediaPlayerBase} for this session to dispatch incoming event
         * to.
         *
         * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
         */
        @NonNull
        U setPlayer(@NonNull MediaPlayerBase player) {
            if (player == null) {
                throw new IllegalArgumentException("player shouldn't be null");
            }
            mBaseImpl.setPlayer(player);
            return (U) this;
        }

        /**
         * Sets the {@link MediaPlaylistAgent} for this session to manages playlist of the
         * underlying {@link MediaPlayerBase}. The playlist agent should manage
         * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
         * <p>
         * If the {@link MediaPlaylistAgent} isn't set, session will create the default playlist
         * agent.
         *
         * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the
         *                      {@code player}
         */
        U setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
            if (playlistAgent == null) {
                throw new IllegalArgumentException("playlistAgent shouldn't be null");
            }
            mBaseImpl.setPlaylistAgent(playlistAgent);
            return (U) this;
        }

        /**
         * Sets the {@link VolumeProviderCompat} for this session to handle volume events. If not
         * set, system will adjust the appropriate stream volume for this session's player.
         *
         * @param volumeProvider The provider that will receive volume button events.
         */
        @NonNull
        U setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
            mBaseImpl.setVolumeProvider(volumeProvider);
            return (U) this;
        }

        /**
         * 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) {
            mBaseImpl.setSessionActivity(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");
            }
            mBaseImpl.setId(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");
            }
            mBaseImpl.setSessionCallback(executor, callback);
            return (U) this;
        }

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

        void setImpl(MediaSession2ImplBase.BuilderBase<T, C> impl) {
            mBaseImpl = impl;
        }
    }

    /**
     * Builder for {@link MediaSession2}.
     * <p>
     * Any incoming event from the {@link MediaController2} will be handled on the thread
     * that created session with the {@link Builder#build()}.
     */
    public static final class Builder extends BuilderBase<MediaSession2, Builder, SessionCallback> {
        private MediaSession2ImplBase.Builder mImpl;

        public Builder(Context context) {
            super(context);
            mImpl = new MediaSession2ImplBase.Builder(context);
            setImpl(mImpl);
        }

        @Override
        public @NonNull Builder setPlayer(@NonNull MediaPlayerBase player) {
            return super.setPlayer(player);
        }

        @Override
        public @NonNull Builder setPlaylistAgent(@NonNull MediaPlaylistAgent playlistAgent) {
            return super.setPlaylistAgent(playlistAgent);
        }

        @Override
        public @NonNull Builder setVolumeProvider(@Nullable VolumeProviderCompat volumeProvider) {
            return super.setVolumeProvider(volumeProvider);
        }

        @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
        public @NonNull Builder setSessionCallback(@NonNull Executor executor, @NonNull SessionCallback callback) {
            return super.setSessionCallback(executor, callback);
        }

        @Override
        public @NonNull MediaSession2 build() {
            return super.build();
        }
    }

    /**
     * Information of a controller.
     */
    public static final class ControllerInfo {
        private final int mUid;
        private final String mPackageName;
        // Note: IMediaControllerCallback should be used only for MediaSession2ImplBase
        private final IMediaControllerCallback mIControllerCallback;
        private final boolean mIsTrusted;

        /**
         * @hide
         */
        @RestrictTo(LIBRARY_GROUP)
        public ControllerInfo(@NonNull Context context, int uid, int pid, @NonNull String packageName,
                @NonNull IMediaControllerCallback callback) {
            mUid = uid;
            mPackageName = packageName;
            mIControllerCallback = callback;
            mIsTrusted = false;
        }

        /**
         * @return package name of the controller
         */
        public @NonNull String getPackageName() {
            return mPackageName;
        }

        /**
         * @return uid of the controller
         */
        public int getUid() {
            return mUid;
        }

        /**
         * 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;
        }

        IBinder getId() {
            return mIControllerCallback.asBinder();
        }

        @Override
        public int hashCode() {
            return mIControllerCallback.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof ControllerInfo)) {
                return false;
            }
            ControllerInfo other = (ControllerInfo) obj;
            return mIControllerCallback.asBinder().equals(other.mIControllerCallback.asBinder());
        }

        @Override
        public String toString() {
            return "ControllerInfo {pkg=" + mPackageName + ", uid=" + mUid + "})";
        }

        /**
         * @hide
         * @return Bundle
         */
        @RestrictTo(LIBRARY_GROUP)
        public @NonNull Bundle toBundle() {
            return new Bundle();
        }

        /**
         * @hide
         * @return Bundle
         */
        @RestrictTo(LIBRARY_GROUP)
        public static @NonNull ControllerInfo fromBundle(@NonNull Context context, Bundle bundle) {
            return new ControllerInfo(context, -1, -1, "TODO", null);
        }

        IMediaControllerCallback getControllerBinder() {
            return mIControllerCallback;
        }
    }

    /**
     * Button for a {@link SessionCommand2} that will be shown by the controller.
     * <p>
     * It's up to the controller's decision to respect or ignore this customization request.
     */
    public static final class CommandButton {
        private static final String KEY_COMMAND = "android.media.media_session2.command_button.command";
        private static final String KEY_ICON_RES_ID = "android.media.media_session2.command_button.icon_res_id";
        private static final String KEY_DISPLAY_NAME = "android.media.media_session2.command_button.display_name";
        private static final String KEY_EXTRAS = "android.media.media_session2.command_button.extras";
        private static final String KEY_ENABLED = "android.media.media_session2.command_button.enabled";

        private SessionCommand2 mCommand;
        private int mIconResId;
        private String mDisplayName;
        private Bundle mExtras;
        private boolean mEnabled;

        private CommandButton(@Nullable SessionCommand2 command, int iconResId, @Nullable String 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 SessionCommand2 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 String 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;
        }

        /**
         * @hide
         * @return Bundle
         */
        @RestrictTo(LIBRARY_GROUP)
        public @NonNull Bundle toBundle() {
            Bundle bundle = new Bundle();
            bundle.putBundle(KEY_COMMAND, mCommand.toBundle());
            bundle.putInt(KEY_ICON_RES_ID, mIconResId);
            bundle.putString(KEY_DISPLAY_NAME, mDisplayName);
            bundle.putBundle(KEY_EXTRAS, mExtras);
            bundle.putBoolean(KEY_ENABLED, mEnabled);
            return bundle;
        }

        /**
         * @hide
         * @return CommandButton
         */
        @RestrictTo(LIBRARY_GROUP)
        public static @Nullable CommandButton fromBundle(Bundle bundle) {
            if (bundle == null) {
                return null;
            }
            CommandButton.Builder builder = new CommandButton.Builder();
            builder.setCommand(SessionCommand2.fromBundle(bundle.getBundle(KEY_COMMAND)));
            builder.setIconResId(bundle.getInt(KEY_ICON_RES_ID, 0));
            builder.setDisplayName(bundle.getString(KEY_DISPLAY_NAME));
            builder.setExtras(bundle.getBundle(KEY_EXTRAS));
            builder.setEnabled(bundle.getBoolean(KEY_ENABLED));
            try {
                return builder.build();
            } catch (IllegalStateException e) {
                // Malformed or version mismatch. Return null for now.
                return null;
            }
        }

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

            /**
             * Sets the {@link SessionCommand2} that would be sent to the session when the button
             * is clicked.
             *
             * @param command session command
             */
            public @NonNull Builder setCommand(@Nullable SessionCommand2 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 MediaController2} 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 String 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);
            }
        }
    }

    abstract static class SupportLibraryImpl extends MediaInterface2.SessionPlayer implements AutoCloseable {
        abstract void updatePlayer(@NonNull MediaPlayerBase player, @Nullable MediaPlaylistAgent playlistAgent,
                @Nullable VolumeProviderCompat volumeProvider);

        abstract @NonNull MediaPlayerBase getPlayer();

        abstract @NonNull MediaPlaylistAgent getPlaylistAgent();

        abstract @Nullable VolumeProviderCompat getVolumeProvider();

        abstract @NonNull SessionToken2 getToken();

        abstract @NonNull List<ControllerInfo> getConnectedControllers();

        abstract void setAudioFocusRequest(@Nullable AudioFocusRequest afr);

        abstract void setCustomLayout(@NonNull ControllerInfo controller, @NonNull List<CommandButton> layout);

        abstract void setAllowedCommands(@NonNull ControllerInfo controller,
                @NonNull SessionCommandGroup2 commands);

        abstract void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args);

        abstract void sendCustomCommand(@NonNull ControllerInfo controller, @NonNull SessionCommand2 command,
                @Nullable Bundle args, @Nullable ResultReceiver receiver);

        abstract void notifyRoutesInfoChanged(@NonNull ControllerInfo controller, @Nullable List<Bundle> routes);

        // Internally used methods
        abstract void setInstance(MediaSession2 session);

        abstract MediaSession2 getInstance();

        abstract Context getContext();

        abstract Executor getCallbackExecutor();

        abstract SessionCallback getCallback();

        abstract boolean isClosed();

        abstract PlaybackStateCompat getPlaybackStateCompat();

        abstract PlaybackInfo getPlaybackInfo();
    }

    static final String TAG = "MediaSession2";

    private final SupportLibraryImpl mImpl;

    MediaSession2(SupportLibraryImpl impl) {
        mImpl = impl;
        mImpl.setInstance(this);
    }

    /**
     * Sets the underlying {@link MediaPlayerBase} and {@link MediaPlaylistAgent} for this session
     * to dispatch incoming event to.
     * <p>
     * When a {@link MediaPlaylistAgent} is specified here, the playlist agent should manage
     * {@link MediaPlayerBase} for calling {@link MediaPlayerBase#setNextDataSources(List)}.
     * <p>
     * If the {@link MediaPlaylistAgent} isn't set, session will recreate the default playlist
     * agent.
     *
     * @param player a {@link MediaPlayerBase} that handles actual media playback in your app
     * @param playlistAgent a {@link MediaPlaylistAgent} that manages playlist of the {@code player}
     * @param volumeProvider a {@link VolumeProviderCompat}. If {@code null}, system will adjust the
     *                       appropriate stream volume for this session's player.
     */
    public void updatePlayer(@NonNull MediaPlayerBase player, @Nullable MediaPlaylistAgent playlistAgent,
            @Nullable VolumeProviderCompat volumeProvider) {
        mImpl.updatePlayer(player, playlistAgent, volumeProvider);
    }

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

    /**
     * @return player
     */
    public @NonNull MediaPlayerBase getPlayer() {
        return mImpl.getPlayer();
    }

    /**
     * @return playlist agent
     */
    public @NonNull MediaPlaylistAgent getPlaylistAgent() {
        return mImpl.getPlaylistAgent();
    }

    /**
     * @return volume provider
     */
    public @Nullable VolumeProviderCompat getVolumeProvider() {
        return mImpl.getVolumeProvider();
    }

    /**
     * Returns the {@link SessionToken2} for creating {@link MediaController2}.
     */
    public @NonNull SessionToken2 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();
    }

    /**
     * Set the {@link AudioFocusRequest} to obtain the audio focus
     *
     * @param afr the full request parameters
     */
    public void setAudioFocusRequest(@Nullable AudioFocusRequest afr) {
        mImpl.setAudioFocusRequest(afr);
    }

    /**
     * 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's the same way
     * (layout[i] means a CommandButton at index i in the given list)
     * For 5 icons row
     *      layout[3] layout[1] layout[0] layout[2] layout[4]
     * For 3 icons row
     *      layout[1] layout[0] layout[2]
     * For 5 icons row with overflow icon (can show +5 extra buttons with overflow button)
     *      expanded row:   layout[5] layout[6] layout[7] layout[8] layout[9]
     *      main row:       layout[3] layout[1] layout[0] layout[2] layout[4]
     * <p>
     * This API can be called in the
     * {@link SessionCallback#onConnect(MediaSession2, ControllerInfo)}.
     *
     * @param controller controller to specify layout.
     * @param layout ordered list of layout.
     */
    public void setCustomLayout(@NonNull ControllerInfo controller, @NonNull List<CommandButton> layout) {
        mImpl.setCustomLayout(controller, layout);
    }

    /**
     * Set the new allowed command group for the controller
     *
     * @param controller controller to change allowed commands
     * @param commands new allowed commands
     */
    public void setAllowedCommands(@NonNull ControllerInfo controller, @NonNull SessionCommandGroup2 commands) {
        mImpl.setAllowedCommands(controller, commands);
    }

    /**
     * Send custom command to all connected controllers.
     *
     * @param command a command
     * @param args optional argument
     */
    public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args) {
        mImpl.sendCustomCommand(command, args);
    }

    /**
     * Send custom command to a specific controller.
     *
     * @param command a command
     * @param args optional argument
     * @param receiver result receiver for the session
     */
    public void sendCustomCommand(@NonNull ControllerInfo controller, @NonNull SessionCommand2 command,
            @Nullable Bundle args, @Nullable ResultReceiver receiver) {
        mImpl.sendCustomCommand(controller, command, args, receiver);
    }

    /**
     * Play playback.
     * <p>
     * This calls {@link MediaPlayerBase#play()}.
     */
    @Override
    public void play() {
        mImpl.play();
    }

    /**
     * Pause playback.
     * <p>
     * This calls {@link MediaPlayerBase#pause()}.
     */
    @Override
    public void pause() {
        mImpl.pause();
    }

    /**
     * Stop playback, and reset the player to the initial state.
     * <p>
     * This calls {@link MediaPlayerBase#reset()}.
     */
    @Override
    public void reset() {
        mImpl.reset();
    }

    /**
     * Request that the player prepare its playback. In other words, other sessions can continue
     * to play during the preparation of this session. This method can be used to speed up the
     * start of the playback. Once the preparation is done, the session will change its playback
     * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called
     * to start playback.
     * <p>
     * This calls {@link MediaPlayerBase#reset()}.
     */
    @Override
    public void prepare() {
        mImpl.prepare();
    }

    /**
     * Move to a new location in the media stream.
     *
     * @param pos Position to move to, in milliseconds.
     */
    @Override
    public void seekTo(long pos) {
        mImpl.seekTo(pos);
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    @Override
    public void skipForward() {
        mImpl.skipForward();
    }

    /**
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    @Override
    public void skipBackward() {
        mImpl.skipBackward();
    }

    /**
     * Notify errors to the connected controllers
     *
     * @param errorCode error code
     * @param extras extras
     */
    @Override
    public void notifyError(@ErrorCode int errorCode, @Nullable Bundle extras) {
        mImpl.notifyError(errorCode, extras);
    }

    /**
     * Notify routes information to a connected controller
     *
     * @param controller controller information
     * @param routes The routes information. Each bundle should be from
     *              MediaRouteDescritor.asBundle().
     */
    public void notifyRoutesInfoChanged(@NonNull ControllerInfo controller, @Nullable List<Bundle> routes) {
        mImpl.notifyRoutesInfoChanged(controller, routes);
    }

    /**
     * Gets the current player state.
     *
     * @return the current player state
     */
    @Override
    public @PlayerState int getPlayerState() {
        return mImpl.getPlayerState();
    }

    /**
     * Gets the current position.
     *
     * @return the current playback position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME} if
     *         unknown.
     */
    @Override
    public long getCurrentPosition() {
        return mImpl.getCurrentPosition();
    }

    @Override
    public long getDuration() {
        return mImpl.getDuration();
    }

    /**
     * Gets the buffered position, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown.
     *
     * @return the buffered position in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}.
     */
    @Override
    public long getBufferedPosition() {
        return mImpl.getBufferedPosition();
    }

    /**
     * Gets the current buffering state of the player.
     * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
     * buffered.
     *
     * @return the buffering state.
     */
    @Override
    public @BuffState int getBufferingState() {
        return mImpl.getBufferingState();
    }

    /**
     * Get the playback speed.
     *
     * @return speed
     */
    @Override
    public float getPlaybackSpeed() {
        return mImpl.getPlaybackSpeed();
    }

    /**
     * Set the playback speed.
     */
    @Override
    public void setPlaybackSpeed(float speed) {
        mImpl.setPlaybackSpeed(speed);
    }

    /**
     * Sets the data source missing helper. Helper will be used to provide default implementation of
     * {@link MediaPlaylistAgent} when it isn't set by developer.
     * <p>
     * Default implementation of the {@link MediaPlaylistAgent} will call helper when a
     * {@link MediaItem2} in the playlist doesn't have a {@link DataSourceDesc}. This may happen
     * when
     * <ul>
     *      <li>{@link MediaItem2} specified by {@link #setPlaylist(List, MediaMetadata2)} doesn't
     *          have {@link DataSourceDesc}</li>
     *      <li>{@link MediaController2#addPlaylistItem(int, MediaItem2)} is called and accepted
     *          by {@link SessionCallback#onCommandRequest(
     *          MediaSession2, ControllerInfo, SessionCommand2)}.
     *          In that case, an item would be added automatically without the data source.</li>
     * </ul>
     * <p>
     * If it's not set, playback wouldn't happen for the item without data source descriptor.
     * <p>
     * The helper will be run on the executor that was specified by
     * {@link Builder#setSessionCallback(Executor, SessionCallback)}.
     *
     * @param helper a data source missing helper.
     * @throws IllegalStateException when the helper is set when the playlist agent is set
     * @see #setPlaylist(List, MediaMetadata2)
     * @see SessionCallback#onCommandRequest(MediaSession2, ControllerInfo, SessionCommand2)
     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_ADD_ITEM
     * @see SessionCommand2#COMMAND_CODE_PLAYLIST_REPLACE_ITEM
     */
    @Override
    public void setOnDataSourceMissingHelper(@NonNull OnDataSourceMissingHelper helper) {
        mImpl.setOnDataSourceMissingHelper(helper);
    }

    /**
     * Clears the data source missing helper.
     *
     * @see #setOnDataSourceMissingHelper(OnDataSourceMissingHelper)
     */
    @Override
    public void clearOnDataSourceMissingHelper() {
        mImpl.clearOnDataSourceMissingHelper();
    }

    /**
     * Returns the playlist from the {@link MediaPlaylistAgent}.
     * <p>
     * This list may differ with the list that was specified with
     * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent}
     * implementation. Use media items returned here for other playlist agent APIs such as
     * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}.
     *
     * @return playlist
     * @see MediaPlaylistAgent#getPlaylist()
     * @see SessionCallback#onPlaylistChanged(
     *          MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
     */
    @Override
    public List<MediaItem2> getPlaylist() {
        return mImpl.getPlaylist();
    }

    /**
     * Sets a list of {@link MediaItem2} to the {@link MediaPlaylistAgent}. Ensure uniqueness of
     * each {@link MediaItem2} in the playlist so the session can uniquely identity individual
     * items.
     * <p>
     * This may be an asynchronous call, and {@link MediaPlaylistAgent} may keep the copy of the
     * list. Wait for {@link SessionCallback#onPlaylistChanged(MediaSession2, MediaPlaylistAgent,
     * List, MediaMetadata2)} to know the operation finishes.
     * <p>
     * You may specify a {@link MediaItem2} without {@link DataSourceDesc}. In that case,
     * {@link MediaPlaylistAgent} has responsibility to dynamically query {link DataSourceDesc}
     * when such media item is ready for preparation or play. Default implementation needs
     * {@link OnDataSourceMissingHelper} for such case.
     *
     * @param list A list of {@link MediaItem2} objects to set as a play list.
     * @throws IllegalArgumentException if given list is {@code null}, or has duplicated media
     * items.
     * @see MediaPlaylistAgent#setPlaylist(List, MediaMetadata2)
     * @see SessionCallback#onPlaylistChanged(
     *          MediaSession2, MediaPlaylistAgent, List, MediaMetadata2)
     * @see #setOnDataSourceMissingHelper
     */
    @Override
    public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) {
        mImpl.setPlaylist(list, metadata);
    }

    /**
     * Skips to the item in the playlist.
     * <p>
     * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)} and the behavior depends
     * on the playlist agent implementation, especially with the shuffle/repeat mode.
     *
     * @param item The item in the playlist you want to play
     * @see #getShuffleMode()
     * @see #getRepeatMode()
     */
    @Override
    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
        mImpl.skipToPlaylistItem(item);
    }

    /**
     * Skips to the previous item.
     * <p>
     * This calls {@link MediaPlaylistAgent#skipToPreviousItem()} and the behavior depends on the
     * playlist agent implementation, especially with the shuffle/repeat mode.
     *
     * @see #getShuffleMode()
     * @see #getRepeatMode()
     **/
    @Override
    public void skipToPreviousItem() {
        mImpl.skipToPreviousItem();
    }

    /**
     * Skips to the next item.
     * <p>
     * This calls {@link MediaPlaylistAgent#skipToNextItem()} and the behavior depends on the
     * playlist agent implementation, especially with the shuffle/repeat mode.
     *
     * @see #getShuffleMode()
     * @see #getRepeatMode()
     */
    @Override
    public void skipToNextItem() {
        mImpl.skipToNextItem();
    }

    /**
     * Gets the playlist metadata from the {@link MediaPlaylistAgent}.
     *
     * @return the playlist metadata
     */
    @Override
    public MediaMetadata2 getPlaylistMetadata() {
        return mImpl.getPlaylistMetadata();
    }

    /**
     * Adds the media item to the playlist at position index. Index equals or greater than
     * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
     * the playlist.
     * <p>
     * This will not change the currently playing media item.
     * If index is less than or equal to the current index of the play list,
     * the current index of the play list will be incremented correspondingly.
     *
     * @param index the index you want to add
     * @param item the media item you want to add
     */
    @Override
    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
        mImpl.addPlaylistItem(index, item);
    }

    /**
     * Removes the media item in the playlist.
     * <p>
     * If the item is the currently playing item of the playlist, current playback
     * will be stopped and playback moves to next source in the list.
     *
     * @param item the media item you want to add
     */
    @Override
    public void removePlaylistItem(@NonNull MediaItem2 item) {
        mImpl.removePlaylistItem(item);
    }

    /**
     * Replaces the media item at index in the playlist. This can be also used to update metadata of
     * an item.
     *
     * @param index the index of the item to replace
     * @param item the new item
     */
    @Override
    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
        mImpl.replacePlaylistItem(index, item);
    }

    /**
     * Return currently playing media item.
     *
     * @return currently playing media item
     */
    @Override
    public MediaItem2 getCurrentMediaItem() {
        return mImpl.getCurrentMediaItem();
    }

    /**
     * Updates the playlist metadata to the {@link MediaPlaylistAgent}.
     *
     * @param metadata metadata of the playlist
     */
    @Override
    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
        mImpl.updatePlaylistMetadata(metadata);
    }

    /**
     * Gets the repeat mode from the {@link MediaPlaylistAgent}.
     *
     * @return repeat mode
     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
     */
    @Override
    public @RepeatMode int getRepeatMode() {
        return mImpl.getRepeatMode();
    }

    /**
     * Sets the repeat mode to the {@link MediaPlaylistAgent}.
     *
     * @param repeatMode repeat mode
     * @see MediaPlaylistAgent#REPEAT_MODE_NONE
     * @see MediaPlaylistAgent#REPEAT_MODE_ONE
     * @see MediaPlaylistAgent#REPEAT_MODE_ALL
     * @see MediaPlaylistAgent#REPEAT_MODE_GROUP
     */
    @Override
    public void setRepeatMode(@RepeatMode int repeatMode) {
        mImpl.setRepeatMode(repeatMode);
    }

    /**
     * Gets the shuffle mode from the {@link MediaPlaylistAgent}.
     *
     * @return The shuffle mode
     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
     */
    @Override
    public @ShuffleMode int getShuffleMode() {
        return mImpl.getShuffleMode();
    }

    /**
     * Sets the shuffle mode to the {@link MediaPlaylistAgent}.
     *
     * @param shuffleMode The shuffle mode
     * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE
     * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL
     * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP
     */
    @Override
    public void setShuffleMode(@ShuffleMode int shuffleMode) {
        mImpl.setShuffleMode(shuffleMode);
    }
}