/*
 * 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 android.support.v4.media.session;

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

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.media.AudioManager;
import android.media.session.MediaController;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
import android.support.v4.media.session.MediaSessionCompat.QueueItem;
import android.support.v4.media.session.PlaybackStateCompat.CustomAction;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.app.BundleCompat;
import androidx.core.app.ComponentActivity;
import androidx.media.VolumeProviderCompat;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

/**
 * Allows an app to interact with an ongoing media session. Media buttons and
 * other commands can be sent to the session. A callback may be registered to
 * receive updates from the session, such as metadata and play state changes.
 * <p>
 * A MediaController can be created if you have a {@link MediaSessionCompat.Token}
 * from the session owner.
 * <p>
 * MediaController objects are thread-safe.
 * <p>
 * This is a helper for accessing features in {@link android.media.session.MediaSession}
 * introduced after API level 4 in a backwards compatible fashion.
 * <p class="note">
 * If MediaControllerCompat is created with a {@link MediaSessionCompat.Token session token}
 * from another process, following methods will not work directly after the creation if the
 * {@link MediaSessionCompat.Token session token} is not passed through a
 * {@link MediaBrowserCompat}:
 * <ul>
 * <li>{@link #getPlaybackState()}.{@link PlaybackStateCompat#getExtras() getExtras()}</li>
 * <li>{@link #isCaptioningEnabled()}</li>
 * <li>{@link #getRepeatMode()}</li>
 * <li>{@link #getShuffleMode()}</li>
 * </ul></p>
 *
 * <div class="special reference">
 * <h3>Developer Guides</h3>
 * <p>For information about building your media application, read the
 * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p>
 * </div>
 */
public final class MediaControllerCompat {
    static final String TAG = "MediaControllerCompat";

    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public static final String COMMAND_GET_EXTRA_BINDER =
            "android.support.v4.media.session.command.GET_EXTRA_BINDER";
    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public static final String COMMAND_ADD_QUEUE_ITEM =
            "android.support.v4.media.session.command.ADD_QUEUE_ITEM";
    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public static final String COMMAND_ADD_QUEUE_ITEM_AT =
            "android.support.v4.media.session.command.ADD_QUEUE_ITEM_AT";
    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public static final String COMMAND_REMOVE_QUEUE_ITEM =
            "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM";
    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public static final String COMMAND_REMOVE_QUEUE_ITEM_AT =
            "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM_AT";

    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public static final String COMMAND_ARGUMENT_MEDIA_DESCRIPTION =
            "android.support.v4.media.session.command.ARGUMENT_MEDIA_DESCRIPTION";
    /**
     * @hide
     */
    @RestrictTo(LIBRARY)
    public static final String COMMAND_ARGUMENT_INDEX =
            "android.support.v4.media.session.command.ARGUMENT_INDEX";

    private static class MediaControllerExtraData extends ComponentActivity.ExtraData {
        private final MediaControllerCompat mMediaController;

        MediaControllerExtraData(MediaControllerCompat mediaController) {
            mMediaController = mediaController;
        }

        MediaControllerCompat getMediaController() {
            return mMediaController;
        }
    }

    /**
     * Sets a {@link MediaControllerCompat} in the {@code activity} for later retrieval via
     * {@link #getMediaController(Activity)}.
     *
     * <p>This is compatible with {@link Activity#setMediaController(MediaController)}.
     * If {@code activity} inherits {@link androidx.fragment.app.FragmentActivity}, the
     * {@code mediaController} will be saved in the {@code activity}. In addition to that,
     * on API 21 and later, {@link Activity#setMediaController(MediaController)} will be
     * called.</p>
     *
     * @param activity The activity to set the {@code mediaController} in, must not be null.
     * @param mediaController The controller for the session which should receive
     *     media keys and volume changes on API 21 and later.
     * @see #getMediaController(Activity)
     * @see Activity#setMediaController(android.media.session.MediaController)
     */
    public static void setMediaController(@NonNull Activity activity,
            MediaControllerCompat mediaController) {
        if (activity instanceof ComponentActivity) {
            ((ComponentActivity) activity).putExtraData(
                    new MediaControllerExtraData(mediaController));
        }
        if (android.os.Build.VERSION.SDK_INT >= 21) {
            Object controllerObj = null;
            if (mediaController != null) {
                Object sessionTokenObj = mediaController.getSessionToken().getToken();
                controllerObj = MediaControllerCompatApi21.fromToken(activity, sessionTokenObj);
            }
            MediaControllerCompatApi21.setMediaController(activity, controllerObj);
        }
    }

    /**
     * Retrieves the {@link MediaControllerCompat} set in the activity by
     * {@link #setMediaController(Activity, MediaControllerCompat)} for sending media key and volume
     * events.
     *
     * <p>This is compatible with {@link Activity#getMediaController()}.</p>
     *
     * @param activity The activity to get the media controller from, must not be null.
     * @return The controller which should receive events.
     * @see #setMediaController(Activity, MediaControllerCompat)
     */
    public static MediaControllerCompat getMediaController(@NonNull Activity activity) {
        if (activity instanceof ComponentActivity) {
            MediaControllerExtraData extraData =
                    ((ComponentActivity) activity).getExtraData(MediaControllerExtraData.class);
            return extraData != null ? extraData.getMediaController() : null;
        } else if (android.os.Build.VERSION.SDK_INT >= 21) {
            Object controllerObj = MediaControllerCompatApi21.getMediaController(activity);
            if (controllerObj == null) {
                return null;
            }
            Object sessionTokenObj = MediaControllerCompatApi21.getSessionToken(controllerObj);
            try {
                return new MediaControllerCompat(activity,
                        MediaSessionCompat.Token.fromToken(sessionTokenObj));
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getMediaController.", e);
            }
        }
        return null;
    }

    private static void validateCustomAction(String action, Bundle args) {
        if (action == null) {
            return;
        }
        switch(action) {
            case MediaSessionCompat.ACTION_FOLLOW:
            case MediaSessionCompat.ACTION_UNFOLLOW:
                if (args == null
                        || !args.containsKey(MediaSessionCompat.ARGUMENT_MEDIA_ATTRIBUTE)) {
                    throw new IllegalArgumentException("An extra field "
                            + MediaSessionCompat.ARGUMENT_MEDIA_ATTRIBUTE + " is required "
                            + "for this action " + action + ".");
                }
                break;
        }
    }

    private final MediaControllerImpl mImpl;
    private final MediaSessionCompat.Token mToken;
    // This set is used to keep references to registered callbacks to prevent them being GCed,
    // since we only keep weak references for callbacks in this class and its inner classes.
    private final HashSet<Callback> mRegisteredCallbacks = new HashSet<>();

    /**
     * Creates a media controller from a session.
     *
     * @param session The session to be controlled.
     */
    public MediaControllerCompat(Context context, @NonNull MediaSessionCompat session) {
        if (session == null) {
            throw new IllegalArgumentException("session must not be null");
        }
        mToken = session.getSessionToken();

        if (android.os.Build.VERSION.SDK_INT >= 24) {
            mImpl = new MediaControllerImplApi24(context, session);
        } else if (android.os.Build.VERSION.SDK_INT >= 23) {
            mImpl = new MediaControllerImplApi23(context, session);
        } else if (android.os.Build.VERSION.SDK_INT >= 21) {
            mImpl = new MediaControllerImplApi21(context, session);
        } else {
            mImpl = new MediaControllerImplBase(mToken);
        }
    }

    /**
     * Creates a media controller from a session token which may have
     * been obtained from another process.
     *
     * @param sessionToken The token of the session to be controlled.
     * @throws RemoteException if the session is not accessible.
     */
    public MediaControllerCompat(Context context, @NonNull MediaSessionCompat.Token sessionToken)
            throws RemoteException {
        if (sessionToken == null) {
            throw new IllegalArgumentException("sessionToken must not be null");
        }
        mToken = sessionToken;

        if (android.os.Build.VERSION.SDK_INT >= 24) {
            mImpl = new MediaControllerImplApi24(context, sessionToken);
        } else if (android.os.Build.VERSION.SDK_INT >= 23) {
            mImpl = new MediaControllerImplApi23(context, sessionToken);
        } else if (android.os.Build.VERSION.SDK_INT >= 21) {
            mImpl = new MediaControllerImplApi21(context, sessionToken);
        } else {
            mImpl = new MediaControllerImplBase(mToken);
        }
    }

    /**
     * Gets a {@link TransportControls} instance for this session.
     *
     * @return A controls instance
     */
    public TransportControls getTransportControls() {
        return mImpl.getTransportControls();
    }

    /**
     * Sends the specified media button event to the session. Only media keys can
     * be sent by this method, other keys will be ignored.
     *
     * @param keyEvent The media button event to dispatch.
     * @return true if the event was sent to the session, false otherwise.
     */
    public boolean dispatchMediaButtonEvent(KeyEvent keyEvent) {
        if (keyEvent == null) {
            throw new IllegalArgumentException("KeyEvent may not be null");
        }
        return mImpl.dispatchMediaButtonEvent(keyEvent);
    }

    /**
     * Gets the current playback state for this session.
     *
     * <p>If the session is not ready, {@link PlaybackStateCompat#getExtras()} on the result of
     * this method may return null. </p>
     *
     * @return The current PlaybackState or null
     * @see #isSessionReady
     * @see Callback#onSessionReady
     */
    public PlaybackStateCompat getPlaybackState() {
        return mImpl.getPlaybackState();
    }

    /**
     * Gets the current metadata for this session.
     *
     * @return The current MediaMetadata or null.
     */
    public MediaMetadataCompat getMetadata() {
        return mImpl.getMetadata();
    }

    /**
     * Gets the current play queue for this session if one is set. If you only
     * care about the current item {@link #getMetadata()} should be used.
     *
     * @return The current play queue or null.
     */
    public List<QueueItem> getQueue() {
        return mImpl.getQueue();
    }

    /**
     * Adds a queue item from the given {@code description} at the end of the play queue
     * of this session. Not all sessions may support this. To know whether the session supports
     * this, get the session's flags with {@link #getFlags()} and check that the flag
     * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
     *
     * @param description The {@link MediaDescriptionCompat} for creating the
     *            {@link MediaSessionCompat.QueueItem} to be inserted.
     * @throws UnsupportedOperationException If this session doesn't support this.
     * @see #getFlags()
     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
     */
    public void addQueueItem(MediaDescriptionCompat description) {
        mImpl.addQueueItem(description);
    }

    /**
     * Adds a queue item from the given {@code description} at the specified position
     * in the play queue of this session. Shifts the queue item currently at that position
     * (if any) and any subsequent queue items to the right (adds one to their indices).
     * Not all sessions may support this. To know whether the session supports this,
     * get the session's flags with {@link #getFlags()} and check that the flag
     * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
     *
     * @param description The {@link MediaDescriptionCompat} for creating the
     *            {@link MediaSessionCompat.QueueItem} to be inserted.
     * @param index The index at which the created {@link MediaSessionCompat.QueueItem}
     *            is to be inserted.
     * @throws UnsupportedOperationException If this session doesn't support this.
     * @see #getFlags()
     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
     */
    public void addQueueItem(MediaDescriptionCompat description, int index) {
        mImpl.addQueueItem(description, index);
    }

    /**
     * Removes the first occurrence of the specified {@link MediaSessionCompat.QueueItem}
     * with the given {@link MediaDescriptionCompat description} in the play queue of the
     * associated session. Not all sessions may support this. To know whether the session supports
     * this, get the session's flags with {@link #getFlags()} and check that the flag
     * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
     *
     * @param description The {@link MediaDescriptionCompat} for denoting the
     *            {@link MediaSessionCompat.QueueItem} to be removed.
     * @throws UnsupportedOperationException If this session doesn't support this.
     * @see #getFlags()
     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
     */
    public void removeQueueItem(MediaDescriptionCompat description) {
        mImpl.removeQueueItem(description);
    }

    /**
     * Removes an queue item at the specified position in the play queue
     * of this session. Not all sessions may support this. To know whether the session supports
     * this, get the session's flags with {@link #getFlags()} and check that the flag
     * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
     *
     * @param index The index of the element to be removed.
     * @throws UnsupportedOperationException If this session doesn't support this.
     * @see #getFlags()
     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
     * @deprecated Use {@link #removeQueueItem(MediaDescriptionCompat)} instead.
     */
    @Deprecated
    public void removeQueueItemAt(int index) {
        List<QueueItem> queue = getQueue();
        if (queue != null && index >= 0 && index < queue.size()) {
            QueueItem item = queue.get(index);
            if (item != null) {
                removeQueueItem(item.getDescription());
            }
        }
    }

    /**
     * Gets the queue title for this session.
     */
    public CharSequence getQueueTitle() {
        return mImpl.getQueueTitle();
    }

    /**
     * Gets the extras for this session.
     */
    public Bundle getExtras() {
        return mImpl.getExtras();
    }

    /**
     * Gets the rating type supported by the session. One of:
     * <ul>
     * <li>{@link RatingCompat#RATING_NONE}</li>
     * <li>{@link RatingCompat#RATING_HEART}</li>
     * <li>{@link RatingCompat#RATING_THUMB_UP_DOWN}</li>
     * <li>{@link RatingCompat#RATING_3_STARS}</li>
     * <li>{@link RatingCompat#RATING_4_STARS}</li>
     * <li>{@link RatingCompat#RATING_5_STARS}</li>
     * <li>{@link RatingCompat#RATING_PERCENTAGE}</li>
     * </ul>
     * <p>If the session is not ready, it will return {@link RatingCompat#RATING_NONE}.</p>
     *
     * @return The supported rating type, or {@link RatingCompat#RATING_NONE} if the value is not
     *         set or the session is not ready.
     * @see #isSessionReady
     * @see Callback#onSessionReady
     */
    public int getRatingType() {
        return mImpl.getRatingType();
    }

    /**
     * Returns whether captioning is enabled for this session.
     *
     * <p>If the session is not ready, it will return a {@code false}.</p>
     *
     * @return {@code true} if captioning is enabled, {@code false} if disabled or not set.
     * @see #isSessionReady
     * @see Callback#onSessionReady
     */
    public boolean isCaptioningEnabled() {
        return mImpl.isCaptioningEnabled();
    }

    /**
     * Gets the repeat mode for this session.
     *
     * @return The latest repeat mode set to the session,
     *         {@link PlaybackStateCompat#REPEAT_MODE_NONE} if not set, or
     *         {@link PlaybackStateCompat#REPEAT_MODE_INVALID} if the session is not ready yet.
     * @see #isSessionReady
     * @see Callback#onSessionReady
     */
    public int getRepeatMode() {
        return mImpl.getRepeatMode();
    }

    /**
     * Gets the shuffle mode for this session.
     *
     * @return The latest shuffle mode set to the session, or
     *         {@link PlaybackStateCompat#SHUFFLE_MODE_NONE} if disabled or not set, or
     *         {@link PlaybackStateCompat#SHUFFLE_MODE_INVALID} if the session is not ready yet.
     * @see #isSessionReady
     * @see Callback#onSessionReady
     */
    public int getShuffleMode() {
        return mImpl.getShuffleMode();
    }

    /**
     * Gets the flags for this session. Flags are defined in
     * {@link MediaSessionCompat}.
     *
     * @return The current set of flags for the session.
     */
    public long getFlags() {
        return mImpl.getFlags();
    }

    /**
     * Gets the current playback info for this session.
     *
     * @return The current playback info or null.
     */
    public PlaybackInfo getPlaybackInfo() {
        return mImpl.getPlaybackInfo();
    }

    /**
     * Gets an intent for launching UI associated with this session if one
     * exists.
     *
     * @return A {@link PendingIntent} to launch UI or null.
     */
    public PendingIntent getSessionActivity() {
        return mImpl.getSessionActivity();
    }

    /**
     * Gets the token for the session this controller is connected to.
     *
     * @return The session's token.
     */
    public MediaSessionCompat.Token getSessionToken() {
        return mToken;
    }

    /**
     * Sets the volume of the output this session is playing on. The command will
     * be ignored if it does not support
     * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in
     * {@link AudioManager} may be used to affect the handling.
     *
     * @see #getPlaybackInfo()
     * @param value The value to set it to, between 0 and the reported max.
     * @param flags Flags from {@link AudioManager} to include with the volume
     *            request.
     */
    public void setVolumeTo(int value, int flags) {
        mImpl.setVolumeTo(value, flags);
    }

    /**
     * Adjusts the volume of the output this session is playing on. The direction
     * must be one of {@link AudioManager#ADJUST_LOWER},
     * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
     * The command will be ignored if the session does not support
     * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
     * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in
     * {@link AudioManager} may be used to affect the handling.
     *
     * @see #getPlaybackInfo()
     * @param direction The direction to adjust the volume in.
     * @param flags Any flags to pass with the command.
     */
    public void adjustVolume(int direction, int flags) {
        mImpl.adjustVolume(direction, flags);
    }

    /**
     * Adds a callback to receive updates from the Session. Updates will be
     * posted on the caller's thread.
     *
     * @param callback The callback object, must not be null.
     */
    public void registerCallback(@NonNull Callback callback) {
        registerCallback(callback, null);
    }

    /**
     * Adds a callback to receive updates from the session. Updates will be
     * posted on the specified handler's thread.
     *
     * @param callback The callback object, must not be null.
     * @param handler The handler to post updates on. If null the callers thread
     *            will be used.
     */
    public void registerCallback(@NonNull Callback callback, Handler handler) {
        if (callback == null) {
            throw new IllegalArgumentException("callback must not be null");
        }
        if (handler == null) {
            handler = new Handler();
        }
        callback.setHandler(handler);
        mImpl.registerCallback(callback, handler);
        mRegisteredCallbacks.add(callback);
    }

    /**
     * Stops receiving updates on the specified callback. If an update has
     * already been posted you may still receive it after calling this method.
     *
     * @param callback The callback to remove
     */
    public void unregisterCallback(@NonNull Callback callback) {
        if (callback == null) {
            throw new IllegalArgumentException("callback must not be null");
        }
        try {
            mRegisteredCallbacks.remove(callback);
            mImpl.unregisterCallback(callback);
        } finally {
            callback.setHandler(null);
        }
    }

    /**
     * Sends a generic command to the session. It is up to the session creator
     * to decide what commands and parameters they will support. As such,
     * commands should only be sent to sessions that the controller owns.
     *
     * @param command The command to send
     * @param params Any parameters to include with the command
     * @param cb The callback to receive the result on
     */
    public void sendCommand(@NonNull String command, Bundle params, ResultReceiver cb) {
        if (TextUtils.isEmpty(command)) {
            throw new IllegalArgumentException("command must neither be null nor empty");
        }
        mImpl.sendCommand(command, params, cb);
    }

    /**
     * Returns whether the session is ready or not.
     *
     * <p>If the session is not ready, following methods can work incorrectly.</p>
     * <ul>
     * <li>{@link #getPlaybackState()}</li>
     * <li>{@link #getRatingType()}</li>
     * <li>{@link #getRepeatMode()}</li>
     * <li>{@link #getShuffleMode()}</li>
     * <li>{@link #isCaptioningEnabled()}</li>
     * </ul>
     *
     * @return true if the session is ready, false otherwise.
     * @see Callback#onSessionReady()
     */
    public boolean isSessionReady() {
        return mImpl.isSessionReady();
    }

    /**
     * Gets the session owner's package name.
     *
     * @return The package name of of the session owner.
     */
    public String getPackageName() {
        return mImpl.getPackageName();
    }

    /**
     * Gets the underlying framework
     * {@link android.media.session.MediaController} object.
     * <p>
     * This method is only supported on API 21+.
     * </p>
     *
     * @return The underlying {@link android.media.session.MediaController}
     *         object, or null if none.
     */
    public Object getMediaController() {
        return mImpl.getMediaController();
    }

    /**
     * Callback for receiving updates on from the session. A Callback can be
     * registered using {@link #registerCallback}
     */
    public static abstract class Callback implements IBinder.DeathRecipient {
        private final Object mCallbackObj;
        MessageHandler mHandler;
        IMediaControllerCallback mIControllerCallback;

        public Callback() {
            if (android.os.Build.VERSION.SDK_INT >= 21) {
                mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21(this));
            } else {
                mCallbackObj = mIControllerCallback = new StubCompat(this);
            }
        }

        /**
         * Override to handle the session being ready.
         *
         * @see MediaControllerCompat#isSessionReady
         */
        public void onSessionReady() {
        }

        /**
         * Override to handle the session being destroyed. The session is no
         * longer valid after this call and calls to it will be ignored.
         */
        public void onSessionDestroyed() {
        }

        /**
         * Override to handle custom events sent by the session owner without a
         * specified interface. Controllers should only handle these for
         * sessions they own.
         *
         * @param event The event from the session.
         * @param extras Optional parameters for the event.
         */
        public void onSessionEvent(String event, Bundle extras) {
        }

        /**
         * Override to handle changes in playback state.
         *
         * @param state The new playback state of the session
         */
        public void onPlaybackStateChanged(PlaybackStateCompat state) {
        }

        /**
         * Override to handle changes to the current metadata.
         *
         * @param metadata The current metadata for the session or null if none.
         * @see MediaMetadataCompat
         */
        public void onMetadataChanged(MediaMetadataCompat metadata) {
        }

        /**
         * Override to handle changes to items in the queue.
         *
         * @see MediaSessionCompat.QueueItem
         * @param queue A list of items in the current play queue. It should
         *            include the currently playing item as well as previous and
         *            upcoming items if applicable.
         */
        public void onQueueChanged(List<QueueItem> queue) {
        }

        /**
         * Override to handle changes to the queue title.
         *
         * @param title The title that should be displayed along with the play
         *            queue such as "Now Playing". May be null if there is no
         *            such title.
         */
        public void onQueueTitleChanged(CharSequence title) {
        }

        /**
         * Override to handle changes to the {@link MediaSessionCompat} extras.
         *
         * @param extras The extras that can include other information
         *            associated with the {@link MediaSessionCompat}.
         */
        public void onExtrasChanged(Bundle extras) {
        }

        /**
         * Override to handle changes to the audio info.
         *
         * @param info The current audio info for this session.
         */
        public void onAudioInfoChanged(PlaybackInfo info) {
        }

        /**
         * Override to handle changes to the captioning enabled status.
         *
         * @param enabled {@code true} if captioning is enabled, {@code false} otherwise.
         */
        public void onCaptioningEnabledChanged(boolean enabled) {
        }

        /**
         * Override to handle changes to the repeat mode.
         *
         * @param repeatMode The repeat mode. It should be one of followings:
         *                   {@link PlaybackStateCompat#REPEAT_MODE_NONE},
         *                   {@link PlaybackStateCompat#REPEAT_MODE_ONE},
         *                   {@link PlaybackStateCompat#REPEAT_MODE_ALL},
         *                   {@link PlaybackStateCompat#REPEAT_MODE_GROUP}
         */
        public void onRepeatModeChanged(@PlaybackStateCompat.RepeatMode int repeatMode) {
        }

        /**
         * Override to handle changes to the shuffle mode.
         *
         * @param shuffleMode The shuffle mode. Must be one of the followings:
         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_NONE},
         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_ALL},
         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP}
         */
        public void onShuffleModeChanged(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
        }

        /**
         * @hide
         */
        @RestrictTo(LIBRARY)
        public IMediaControllerCallback getIControllerCallback() {
            return mIControllerCallback;
        }

        @Override
        public void binderDied() {
            onSessionDestroyed();
        }

        /**
         * Set the handler to use for callbacks.
         */
        void setHandler(Handler handler) {
            if (handler == null) {
                if (mHandler != null) {
                    mHandler.mRegistered = false;
                    mHandler.removeCallbacksAndMessages(null);
                    mHandler = null;
                }
            } else {
                mHandler = new MessageHandler(handler.getLooper());
                mHandler.mRegistered = true;
            }
        }

        void postToHandler(int what, Object obj, Bundle data) {
            if (mHandler != null) {
                Message msg = mHandler.obtainMessage(what, obj);
                msg.setData(data);
                msg.sendToTarget();
            }
        }

        private static class StubApi21 implements MediaControllerCompatApi21.Callback {
            private final WeakReference<MediaControllerCompat.Callback> mCallback;

            StubApi21(MediaControllerCompat.Callback callback) {
                mCallback = new WeakReference<>(callback);
            }

            @Override
            public void onSessionDestroyed() {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.onSessionDestroyed();
                }
            }

            @Override
            public void onSessionEvent(String event, Bundle extras) {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    if (callback.mIControllerCallback != null
                            && android.os.Build.VERSION.SDK_INT < 23) {
                        // Ignore. ExtraCallback will handle this.
                    } else {
                        callback.onSessionEvent(event, extras);
                    }
                }
            }

            @Override
            public void onPlaybackStateChanged(Object stateObj) {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    if (callback.mIControllerCallback != null) {
                        // Ignore. ExtraCallback will handle this.
                    } else {
                        callback.onPlaybackStateChanged(
                                PlaybackStateCompat.fromPlaybackState(stateObj));
                    }
                }
            }

            @Override
            public void onMetadataChanged(Object metadataObj) {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.onMetadataChanged(MediaMetadataCompat.fromMediaMetadata(metadataObj));
                }
            }

            @Override
            public void onQueueChanged(List<?> queue) {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.onQueueChanged(QueueItem.fromQueueItemList(queue));
                }
            }

            @Override
            public void onQueueTitleChanged(CharSequence title) {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.onQueueTitleChanged(title);
                }
            }

            @Override
            public void onExtrasChanged(Bundle extras) {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.onExtrasChanged(extras);
                }
            }

            @Override
            public void onAudioInfoChanged(
                    int type, int stream, int control, int max, int current) {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.onAudioInfoChanged(
                            new PlaybackInfo(type, stream, control, max, current));
                }
            }
        }

        private static class StubCompat extends IMediaControllerCallback.Stub {
            private final WeakReference<MediaControllerCompat.Callback> mCallback;

            StubCompat(MediaControllerCompat.Callback callback) {
                mCallback = new WeakReference<>(callback);
            }

            @Override
            public void onEvent(String event, Bundle extras) throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.postToHandler(MessageHandler.MSG_EVENT, event, extras);
                }
            }

            @Override
            public void onSessionDestroyed() throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.postToHandler(MessageHandler.MSG_DESTROYED, null, null);
                }
            }

            @Override
            public void onPlaybackStateChanged(PlaybackStateCompat state) throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.postToHandler(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state, null);
                }
            }

            @Override
            public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.postToHandler(MessageHandler.MSG_UPDATE_METADATA, metadata, null);
                }
            }

            @Override
            public void onQueueChanged(List<QueueItem> queue) throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.postToHandler(MessageHandler.MSG_UPDATE_QUEUE, queue, null);
                }
            }

            @Override
            public void onQueueTitleChanged(CharSequence title) throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.postToHandler(MessageHandler.MSG_UPDATE_QUEUE_TITLE, title, null);
                }
            }

            @Override
            public void onCaptioningEnabledChanged(boolean enabled) throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.postToHandler(
                            MessageHandler.MSG_UPDATE_CAPTIONING_ENABLED, enabled, null);
                }
            }

            @Override
            public void onRepeatModeChanged(int repeatMode) throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.postToHandler(MessageHandler.MSG_UPDATE_REPEAT_MODE, repeatMode, null);
                }
            }

            @Override
            public void onShuffleModeChangedRemoved(boolean enabled) throws RemoteException {
                // Do nothing.
            }

            @Override
            public void onShuffleModeChanged(int shuffleMode) throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.postToHandler(
                            MessageHandler.MSG_UPDATE_SHUFFLE_MODE, shuffleMode, null);
                }
            }

            @Override
            public void onExtrasChanged(Bundle extras) throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.postToHandler(MessageHandler.MSG_UPDATE_EXTRAS, extras, null);
                }
            }

            @Override
            public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    PlaybackInfo pi = null;
                    if (info != null) {
                        pi = new PlaybackInfo(info.volumeType, info.audioStream, info.controlType,
                                info.maxVolume, info.currentVolume);
                    }
                    callback.postToHandler(MessageHandler.MSG_UPDATE_VOLUME, pi, null);
                }
            }

            @Override
            public void onSessionReady() throws RemoteException {
                MediaControllerCompat.Callback callback = mCallback.get();
                if (callback != null) {
                    callback.postToHandler(MessageHandler.MSG_SESSION_READY, null, null);
                }
            }
        }

        private class MessageHandler extends Handler {
            private static final int MSG_EVENT = 1;
            private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
            private static final int MSG_UPDATE_METADATA = 3;
            private static final int MSG_UPDATE_VOLUME = 4;
            private static final int MSG_UPDATE_QUEUE = 5;
            private static final int MSG_UPDATE_QUEUE_TITLE = 6;
            private static final int MSG_UPDATE_EXTRAS = 7;
            private static final int MSG_DESTROYED = 8;
            private static final int MSG_UPDATE_REPEAT_MODE = 9;
            private static final int MSG_UPDATE_CAPTIONING_ENABLED = 11;
            private static final int MSG_UPDATE_SHUFFLE_MODE = 12;
            private static final int MSG_SESSION_READY = 13;

            boolean mRegistered = false;

            MessageHandler(Looper looper) {
                super(looper);
            }

            @Override
            public void handleMessage(Message msg) {
                if (!mRegistered) {
                    return;
                }
                switch (msg.what) {
                    case MSG_EVENT:
                        onSessionEvent((String) msg.obj, msg.getData());
                        break;
                    case MSG_UPDATE_PLAYBACK_STATE:
                        onPlaybackStateChanged((PlaybackStateCompat) msg.obj);
                        break;
                    case MSG_UPDATE_METADATA:
                        onMetadataChanged((MediaMetadataCompat) msg.obj);
                        break;
                    case MSG_UPDATE_QUEUE:
                        onQueueChanged((List<QueueItem>) msg.obj);
                        break;
                    case MSG_UPDATE_QUEUE_TITLE:
                        onQueueTitleChanged((CharSequence) msg.obj);
                        break;
                    case MSG_UPDATE_CAPTIONING_ENABLED:
                        onCaptioningEnabledChanged((boolean) msg.obj);
                        break;
                    case MSG_UPDATE_REPEAT_MODE:
                        onRepeatModeChanged((int) msg.obj);
                        break;
                    case MSG_UPDATE_SHUFFLE_MODE:
                        onShuffleModeChanged((int) msg.obj);
                        break;
                    case MSG_UPDATE_EXTRAS:
                        onExtrasChanged((Bundle) msg.obj);
                        break;
                    case MSG_UPDATE_VOLUME:
                        onAudioInfoChanged((PlaybackInfo) msg.obj);
                        break;
                    case MSG_DESTROYED:
                        onSessionDestroyed();
                        break;
                    case MSG_SESSION_READY:
                        onSessionReady();
                        break;
                }
            }
        }
    }

    /**
     * Interface for controlling media playback on a session. This allows an app
     * to send media transport commands to the session.
     */
    public static abstract class TransportControls {
        /**
         * Used as an integer extra field in {@link #playFromMediaId(String, Bundle)} or
         * {@link #prepareFromMediaId(String, Bundle)} to indicate the stream type to be used by the
         * media player when playing or preparing the specified media id. See {@link AudioManager}
         * for a list of stream types.
         */
        public static final String EXTRA_LEGACY_STREAM_TYPE =
                "android.media.session.extra.LEGACY_STREAM_TYPE";

        TransportControls() {
        }

        /**
         * Request that the player prepare for playback. This can decrease the time it takes to
         * start playback when a play command is received. Preparation is not required. You can
         * call {@link #play} without calling this method beforehand.
         */
        public abstract void prepare();

        /**
         * Request that the player prepare playback for a specific media id. This can decrease the
         * time it takes to start playback when a play command is received. Preparation is not
         * required. You can call {@link #playFromMediaId} without calling this method beforehand.
         *
         * @param mediaId The id of the requested media.
         * @param extras Optional extras that can include extra information about the media item
         *               to be prepared.
         */
        public abstract void prepareFromMediaId(String mediaId, Bundle extras);

        /**
         * Request that the player prepare playback for a specific search query. This can decrease
         * the time it takes to start playback when a play command is received. An empty or null
         * query should be treated as a request to prepare any music. Preparation is not required.
         * You can call {@link #playFromSearch} without calling this method beforehand.
         *
         * @param query The search query.
         * @param extras Optional extras that can include extra information
         *               about the query.
         */
        public abstract void prepareFromSearch(String query, Bundle extras);

        /**
         * Request that the player prepare playback for a specific {@link Uri}. This can decrease
         * the time it takes to start playback when a play command is received. Preparation is not
         * required. You can call {@link #playFromUri} without calling this method beforehand.
         *
         * @param uri The URI of the requested media.
         * @param extras Optional extras that can include extra information about the media item
         *               to be prepared.
         */
        public abstract void prepareFromUri(Uri uri, Bundle extras);

        /**
         * Request that the player start its playback at its current position.
         */
        public abstract void play();

        /**
         * Request that the player start playback for a specific {@link Uri}.
         *
         * @param mediaId The uri of the requested media.
         * @param extras Optional extras that can include extra information
         *            about the media item to be played.
         */
        public abstract void playFromMediaId(String mediaId, Bundle extras);

        /**
         * Request that the player start playback for a specific search query.
         * An empty or null query should be treated as a request to play any
         * music.
         *
         * @param query The search query.
         * @param extras Optional extras that can include extra information
         *            about the query.
         */
        public abstract void playFromSearch(String query, Bundle extras);

        /**
         * Request that the player start playback for a specific {@link Uri}.
         *
         * @param uri  The URI of the requested media.
         * @param extras Optional extras that can include extra information about the media item
         *               to be played.
         */
        public abstract void playFromUri(Uri uri, Bundle extras);

        /**
         * Plays an item with a specific id in the play queue. If you specify an
         * id that is not in the play queue, the behavior is undefined.
         */
        public abstract void skipToQueueItem(long id);

        /**
         * Request that the player pause its playback and stay at its current
         * position.
         */
        public abstract void pause();

        /**
         * Request that the player stop its playback; it may clear its state in
         * whatever way is appropriate.
         */
        public abstract void stop();

        /**
         * Moves to a new location in the media stream.
         *
         * @param pos Position to move to, in milliseconds.
         */
        public abstract void seekTo(long pos);

        /**
         * Starts fast forwarding. If playback is already fast forwarding this
         * may increase the rate.
         */
        public abstract void fastForward();

        /**
         * Skips to the next item.
         */
        public abstract void skipToNext();

        /**
         * Starts rewinding. If playback is already rewinding this may increase
         * the rate.
         */
        public abstract void rewind();

        /**
         * Skips to the previous item.
         */
        public abstract void skipToPrevious();

        /**
         * Rates the current content. This will cause the rating to be set for
         * the current user. The rating type of the given {@link RatingCompat} must match the type
         * returned by {@link #getRatingType()}.
         *
         * @param rating The rating to set for the current content
         */
        public abstract void setRating(RatingCompat rating);

        /**
         * Rates a media item. This will cause the rating to be set for
         * the specific media item. The rating type of the given {@link RatingCompat} must match
         * the type returned by {@link #getRatingType()}.
         *
         * @param rating The rating to set for the media item.
         * @param extras Optional arguments that can include information about the media item
         *               to be rated.
         *
         * @see MediaSessionCompat#ARGUMENT_MEDIA_ATTRIBUTE
         * @see MediaSessionCompat#ARGUMENT_MEDIA_ATTRIBUTE_VALUE
         */
        public abstract void setRating(RatingCompat rating, Bundle extras);

        /**
         * Enables/disables captioning for this session.
         *
         * @param enabled {@code true} to enable captioning, {@code false} to disable.
         */
        public abstract void setCaptioningEnabled(boolean enabled);

        /**
         * Sets the repeat mode for this session.
         *
         * @param repeatMode The repeat mode. Must be one of the followings:
         *                   {@link PlaybackStateCompat#REPEAT_MODE_NONE},
         *                   {@link PlaybackStateCompat#REPEAT_MODE_ONE},
         *                   {@link PlaybackStateCompat#REPEAT_MODE_ALL},
         *                   {@link PlaybackStateCompat#REPEAT_MODE_GROUP}
         */
        public abstract void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);

        /**
         * Sets the shuffle mode for this session.
         *
         * @param shuffleMode The shuffle mode. Must be one of the followings:
         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_NONE},
         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_ALL},
         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP}
         */
        public abstract void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode);

        /**
         * Sends a custom action for the {@link MediaSessionCompat} to perform.
         *
         * @param customAction The action to perform.
         * @param args Optional arguments to supply to the
         *            {@link MediaSessionCompat} for this custom action.
         */
        public abstract void sendCustomAction(PlaybackStateCompat.CustomAction customAction,
                Bundle args);

        /**
         * Sends the id and args from a custom action for the
         * {@link MediaSessionCompat} to perform.
         *
         * @see #sendCustomAction(PlaybackStateCompat.CustomAction action,
         *      Bundle args)
         * @see MediaSessionCompat#ACTION_FLAG_AS_INAPPROPRIATE
         * @see MediaSessionCompat#ACTION_SKIP_AD
         * @see MediaSessionCompat#ACTION_FOLLOW
         * @see MediaSessionCompat#ACTION_UNFOLLOW
         * @param action The action identifier of the
         *            {@link PlaybackStateCompat.CustomAction} as specified by
         *            the {@link MediaSessionCompat}.
         * @param args Optional arguments to supply to the
         *            {@link MediaSessionCompat} for this custom action.
         */
        public abstract void sendCustomAction(String action, Bundle args);
    }

    /**
     * Holds information about the way volume is handled for this session.
     */
    public static final class PlaybackInfo {
        /**
         * The session uses local playback.
         */
        public static final int PLAYBACK_TYPE_LOCAL = 1;
        /**
         * The session uses remote playback.
         */
        public static final int PLAYBACK_TYPE_REMOTE = 2;

        private final int mPlaybackType;
        // TODO update audio stream with AudioAttributes support version
        private final int mAudioStream;
        private final int mVolumeControl;
        private final int mMaxVolume;
        private final int mCurrentVolume;

        PlaybackInfo(int type, int stream, int control, int max, int current) {
            mPlaybackType = type;
            mAudioStream = stream;
            mVolumeControl = control;
            mMaxVolume = max;
            mCurrentVolume = current;
        }

        /**
         * Gets the type of volume handling, either local or remote. One of:
         * <ul>
         * <li>{@link PlaybackInfo#PLAYBACK_TYPE_LOCAL}</li>
         * <li>{@link PlaybackInfo#PLAYBACK_TYPE_REMOTE}</li>
         * </ul>
         *
         * @return The type of volume handling this session is using.
         */
        public int getPlaybackType() {
            return mPlaybackType;
        }

        /**
         * Gets the stream this is currently controlling volume on. When the volume
         * type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} this value does not
         * have meaning and should be ignored.
         *
         * @return The stream this session is playing on.
         */
        public int getAudioStream() {
            // TODO switch to AudioAttributesCompat when it is added.
            return mAudioStream;
        }

        /**
         * Gets the type of volume control that can be used. One of:
         * <ul>
         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li>
         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li>
         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}</li>
         * </ul>
         *
         * @return The type of volume control that may be used with this
         *         session.
         */
        public int getVolumeControl() {
            return mVolumeControl;
        }

        /**
         * Gets the maximum volume that may be set for this session.
         *
         * @return The maximum allowed volume where this session is playing.
         */
        public int getMaxVolume() {
            return mMaxVolume;
        }

        /**
         * Gets the current volume for this session.
         *
         * @return The current volume where this session is playing.
         */
        public int getCurrentVolume() {
            return mCurrentVolume;
        }
    }

    interface MediaControllerImpl {
        void registerCallback(Callback callback, Handler handler);

        void unregisterCallback(Callback callback);
        boolean dispatchMediaButtonEvent(KeyEvent keyEvent);
        TransportControls getTransportControls();
        PlaybackStateCompat getPlaybackState();
        MediaMetadataCompat getMetadata();

        List<QueueItem> getQueue();
        void addQueueItem(MediaDescriptionCompat description);
        void addQueueItem(MediaDescriptionCompat description, int index);
        void removeQueueItem(MediaDescriptionCompat description);
        CharSequence getQueueTitle();
        Bundle getExtras();
        int getRatingType();
        boolean isCaptioningEnabled();
        int getRepeatMode();
        int getShuffleMode();
        long getFlags();
        PlaybackInfo getPlaybackInfo();
        PendingIntent getSessionActivity();

        void setVolumeTo(int value, int flags);
        void adjustVolume(int direction, int flags);
        void sendCommand(String command, Bundle params, ResultReceiver cb);

        boolean isSessionReady();
        String getPackageName();
        Object getMediaController();
    }

    static class MediaControllerImplBase implements MediaControllerImpl {
        private IMediaSession mBinder;
        private TransportControls mTransportControls;

        public MediaControllerImplBase(MediaSessionCompat.Token token) {
            mBinder = IMediaSession.Stub.asInterface((IBinder) token.getToken());
        }

        @Override
        public void registerCallback(Callback callback, Handler handler) {
            if (callback == null) {
                throw new IllegalArgumentException("callback may not be null.");
            }
            try {
                mBinder.asBinder().linkToDeath(callback, 0);
                mBinder.registerCallbackListener((IMediaControllerCallback) callback.mCallbackObj);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in registerCallback.", e);
                callback.onSessionDestroyed();
            }
        }

        @Override
        public void unregisterCallback(Callback callback) {
            if (callback == null) {
                throw new IllegalArgumentException("callback may not be null.");
            }
            try {
                mBinder.unregisterCallbackListener(
                        (IMediaControllerCallback) callback.mCallbackObj);
                mBinder.asBinder().unlinkToDeath(callback, 0);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in unregisterCallback.", e);
            }
        }

        @Override
        public boolean dispatchMediaButtonEvent(KeyEvent event) {
            if (event == null) {
                throw new IllegalArgumentException("event may not be null.");
            }
            try {
                mBinder.sendMediaButton(event);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in dispatchMediaButtonEvent.", e);
            }
            return false;
        }

        @Override
        public TransportControls getTransportControls() {
            if (mTransportControls == null) {
                mTransportControls = new TransportControlsBase(mBinder);
            }

            return mTransportControls;
        }

        @Override
        public PlaybackStateCompat getPlaybackState() {
            try {
                return mBinder.getPlaybackState();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getPlaybackState.", e);
            }
            return null;
        }

        @Override
        public MediaMetadataCompat getMetadata() {
            try {
                return mBinder.getMetadata();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getMetadata.", e);
            }
            return null;
        }

        @Override
        public List<QueueItem> getQueue() {
            try {
                return mBinder.getQueue();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getQueue.", e);
            }
            return null;
        }

        @Override
        public void addQueueItem(MediaDescriptionCompat description) {
            try {
                long flags = mBinder.getFlags();
                if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
                    throw new UnsupportedOperationException(
                            "This session doesn't support queue management operations");
                }
                mBinder.addQueueItem(description);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in addQueueItem.", e);
            }
        }

        @Override
        public void addQueueItem(MediaDescriptionCompat description, int index) {
            try {
                long flags = mBinder.getFlags();
                if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
                    throw new UnsupportedOperationException(
                            "This session doesn't support queue management operations");
                }
                mBinder.addQueueItemAt(description, index);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in addQueueItemAt.", e);
            }
        }

        @Override
        public void removeQueueItem(MediaDescriptionCompat description) {
            try {
                long flags = mBinder.getFlags();
                if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
                    throw new UnsupportedOperationException(
                            "This session doesn't support queue management operations");
                }
                mBinder.removeQueueItem(description);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in removeQueueItem.", e);
            }
        }

        @Override
        public CharSequence getQueueTitle() {
            try {
                return mBinder.getQueueTitle();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getQueueTitle.", e);
            }
            return null;
        }

        @Override
        public Bundle getExtras() {
            try {
                return mBinder.getExtras();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getExtras.", e);
            }
            return null;
        }

        @Override
        public int getRatingType() {
            try {
                return mBinder.getRatingType();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getRatingType.", e);
            }
            return 0;
        }

        @Override
        public boolean isCaptioningEnabled() {
            try {
                return mBinder.isCaptioningEnabled();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in isCaptioningEnabled.", e);
            }
            return false;
        }

        @Override
        public int getRepeatMode() {
            try {
                return mBinder.getRepeatMode();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getRepeatMode.", e);
            }
            return PlaybackStateCompat.REPEAT_MODE_INVALID;
        }

        @Override
        public int getShuffleMode() {
            try {
                return mBinder.getShuffleMode();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getShuffleMode.", e);
            }
            return PlaybackStateCompat.SHUFFLE_MODE_INVALID;
        }

        @Override
        public long getFlags() {
            try {
                return mBinder.getFlags();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getFlags.", e);
            }
            return 0;
        }

        @Override
        public PlaybackInfo getPlaybackInfo() {
            try {
                ParcelableVolumeInfo info = mBinder.getVolumeAttributes();
                PlaybackInfo pi = new PlaybackInfo(info.volumeType, info.audioStream,
                        info.controlType, info.maxVolume, info.currentVolume);
                return pi;
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getPlaybackInfo.", e);
            }
            return null;
        }

        @Override
        public PendingIntent getSessionActivity() {
            try {
                return mBinder.getLaunchPendingIntent();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getSessionActivity.", e);
            }
            return null;
        }

        @Override
        public void setVolumeTo(int value, int flags) {
            try {
                mBinder.setVolumeTo(value, flags, null);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in setVolumeTo.", e);
            }
        }

        @Override
        public void adjustVolume(int direction, int flags) {
            try {
                mBinder.adjustVolume(direction, flags, null);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in adjustVolume.", e);
            }
        }

        @Override
        public void sendCommand(String command, Bundle params, ResultReceiver cb) {
            try {
                mBinder.sendCommand(command, params,
                        new MediaSessionCompat.ResultReceiverWrapper(cb));
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in sendCommand.", e);
            }
        }

        @Override
        public boolean isSessionReady() {
            return true;
        }

        @Override
        public String getPackageName() {
            try {
                return mBinder.getPackageName();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getPackageName.", e);
            }
            return null;
        }

        @Override
        public Object getMediaController() {
            return null;
        }
    }

    static class TransportControlsBase extends TransportControls {
        private IMediaSession mBinder;

        public TransportControlsBase(IMediaSession binder) {
            mBinder = binder;
        }

        @Override
        public void prepare() {
            try {
                mBinder.prepare();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in prepare.", e);
            }
        }

        @Override
        public void prepareFromMediaId(String mediaId, Bundle extras) {
            try {
                mBinder.prepareFromMediaId(mediaId, extras);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in prepareFromMediaId.", e);
            }
        }

        @Override
        public void prepareFromSearch(String query, Bundle extras) {
            try {
                mBinder.prepareFromSearch(query, extras);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in prepareFromSearch.", e);
            }
        }

        @Override
        public void prepareFromUri(Uri uri, Bundle extras) {
            try {
                mBinder.prepareFromUri(uri, extras);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in prepareFromUri.", e);
            }
        }

        @Override
        public void play() {
            try {
                mBinder.play();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in play.", e);
            }
        }

        @Override
        public void playFromMediaId(String mediaId, Bundle extras) {
            try {
                mBinder.playFromMediaId(mediaId, extras);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in playFromMediaId.", e);
            }
        }

        @Override
        public void playFromSearch(String query, Bundle extras) {
            try {
                mBinder.playFromSearch(query, extras);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in playFromSearch.", e);
            }
        }

        @Override
        public void playFromUri(Uri uri, Bundle extras) {
            try {
                mBinder.playFromUri(uri, extras);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in playFromUri.", e);
            }
        }

        @Override
        public void skipToQueueItem(long id) {
            try {
                mBinder.skipToQueueItem(id);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in skipToQueueItem.", e);
            }
        }

        @Override
        public void pause() {
            try {
                mBinder.pause();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in pause.", e);
            }
        }

        @Override
        public void stop() {
            try {
                mBinder.stop();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in stop.", e);
            }
        }

        @Override
        public void seekTo(long pos) {
            try {
                mBinder.seekTo(pos);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in seekTo.", e);
            }
        }

        @Override
        public void fastForward() {
            try {
                mBinder.fastForward();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in fastForward.", e);
            }
        }

        @Override
        public void skipToNext() {
            try {
                mBinder.next();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in skipToNext.", e);
            }
        }

        @Override
        public void rewind() {
            try {
                mBinder.rewind();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in rewind.", e);
            }
        }

        @Override
        public void skipToPrevious() {
            try {
                mBinder.previous();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in skipToPrevious.", e);
            }
        }

        @Override
        public void setRating(RatingCompat rating) {
            try {
                mBinder.rate(rating);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in setRating.", e);
            }
        }

        @Override
        public void setRating(RatingCompat rating, Bundle extras) {
            try {
                mBinder.rateWithExtras(rating, extras);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in setRating.", e);
            }
        }

        @Override
        public void setCaptioningEnabled(boolean enabled) {
            try {
                mBinder.setCaptioningEnabled(enabled);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in setCaptioningEnabled.", e);
            }
        }

        @Override
        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
            try {
                mBinder.setRepeatMode(repeatMode);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in setRepeatMode.", e);
            }
        }

        @Override
        public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
            try {
                mBinder.setShuffleMode(shuffleMode);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in setShuffleMode.", e);
            }
        }

        @Override
        public void sendCustomAction(CustomAction customAction, Bundle args) {
            sendCustomAction(customAction.getAction(), args);
        }

        @Override
        public void sendCustomAction(String action, Bundle args) {
            validateCustomAction(action, args);
            try {
                mBinder.sendCustomAction(action, args);
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in sendCustomAction.", e);
            }
        }
    }

    @RequiresApi(21)
    static class MediaControllerImplApi21 implements MediaControllerImpl {
        protected final Object mControllerObj;

        private final List<Callback> mPendingCallbacks = new ArrayList<>();

        // Extra binder is used for applying the framework change of new APIs and bug fixes
        // after API 21.
        private IMediaSession mExtraBinder;
        private HashMap<Callback, ExtraCallback> mCallbackMap = new HashMap<>();

        public MediaControllerImplApi21(Context context, MediaSessionCompat session) {
            mControllerObj = MediaControllerCompatApi21.fromToken(context,
                    session.getSessionToken().getToken());
            mExtraBinder = session.getSessionToken().getExtraBinder();
            if (mExtraBinder == null) {
                requestExtraBinder();
            }
        }

        public MediaControllerImplApi21(Context context, MediaSessionCompat.Token sessionToken)
                throws RemoteException {
            mControllerObj = MediaControllerCompatApi21.fromToken(context,
                    sessionToken.getToken());
            if (mControllerObj == null) throw new RemoteException();
            mExtraBinder = sessionToken.getExtraBinder();
            if (mExtraBinder == null) {
                requestExtraBinder();
            }
        }

        @Override
        public final void registerCallback(Callback callback, Handler handler) {
            MediaControllerCompatApi21.registerCallback(
                    mControllerObj, callback.mCallbackObj, handler);
            if (mExtraBinder != null) {
                ExtraCallback extraCallback = new ExtraCallback(callback);
                mCallbackMap.put(callback, extraCallback);
                callback.mIControllerCallback = extraCallback;
                try {
                    mExtraBinder.registerCallbackListener(extraCallback);
                } catch (RemoteException e) {
                    Log.e(TAG, "Dead object in registerCallback.", e);
                }
            } else {
                synchronized (mPendingCallbacks) {
                    callback.mIControllerCallback = null;
                    mPendingCallbacks.add(callback);
                }
            }
        }

        @Override
        public final void unregisterCallback(Callback callback) {
            MediaControllerCompatApi21.unregisterCallback(mControllerObj, callback.mCallbackObj);
            if (mExtraBinder != null) {
                try {
                    ExtraCallback extraCallback = mCallbackMap.remove(callback);
                    if (extraCallback != null) {
                        callback.mIControllerCallback = null;
                        mExtraBinder.unregisterCallbackListener(extraCallback);
                    }
                } catch (RemoteException e) {
                    Log.e(TAG, "Dead object in unregisterCallback.", e);
                }
            } else {
                synchronized (mPendingCallbacks) {
                    mPendingCallbacks.remove(callback);
                }
            }
        }

        @Override
        public boolean dispatchMediaButtonEvent(KeyEvent event) {
            return MediaControllerCompatApi21.dispatchMediaButtonEvent(mControllerObj, event);
        }

        @Override
        public TransportControls getTransportControls() {
            Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
            return controlsObj != null ? new TransportControlsApi21(controlsObj) : null;
        }

        @Override
        public PlaybackStateCompat getPlaybackState() {
            if (mExtraBinder != null) {
                try {
                    return mExtraBinder.getPlaybackState();
                } catch (RemoteException e) {
                    Log.e(TAG, "Dead object in getPlaybackState.", e);
                }
            }
            Object stateObj = MediaControllerCompatApi21.getPlaybackState(mControllerObj);
            return stateObj != null ? PlaybackStateCompat.fromPlaybackState(stateObj) : null;
        }

        @Override
        public MediaMetadataCompat getMetadata() {
            Object metadataObj = MediaControllerCompatApi21.getMetadata(mControllerObj);
            return metadataObj != null ? MediaMetadataCompat.fromMediaMetadata(metadataObj) : null;
        }

        @Override
        public List<QueueItem> getQueue() {
            List<Object> queueObjs = MediaControllerCompatApi21.getQueue(mControllerObj);
            return queueObjs != null ? QueueItem.fromQueueItemList(queueObjs) : null;
        }

        @Override
        public void addQueueItem(MediaDescriptionCompat description) {
            long flags = getFlags();
            if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
                throw new UnsupportedOperationException(
                        "This session doesn't support queue management operations");
            }
            Bundle params = new Bundle();
            params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
            sendCommand(COMMAND_ADD_QUEUE_ITEM, params, null);
        }

        @Override
        public void addQueueItem(MediaDescriptionCompat description, int index) {
            long flags = getFlags();
            if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
                throw new UnsupportedOperationException(
                        "This session doesn't support queue management operations");
            }
            Bundle params = new Bundle();
            params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
            params.putInt(COMMAND_ARGUMENT_INDEX, index);
            sendCommand(COMMAND_ADD_QUEUE_ITEM_AT, params, null);
        }

        @Override
        public void removeQueueItem(MediaDescriptionCompat description) {
            long flags = getFlags();
            if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
                throw new UnsupportedOperationException(
                        "This session doesn't support queue management operations");
            }
            Bundle params = new Bundle();
            params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
            sendCommand(COMMAND_REMOVE_QUEUE_ITEM, params, null);
        }

        @Override
        public CharSequence getQueueTitle() {
            return MediaControllerCompatApi21.getQueueTitle(mControllerObj);
        }

        @Override
        public Bundle getExtras() {
            return MediaControllerCompatApi21.getExtras(mControllerObj);
        }

        @Override
        public int getRatingType() {
            if (android.os.Build.VERSION.SDK_INT < 22 && mExtraBinder != null) {
                try {
                    return mExtraBinder.getRatingType();
                } catch (RemoteException e) {
                    Log.e(TAG, "Dead object in getRatingType.", e);
                }
            }
            return MediaControllerCompatApi21.getRatingType(mControllerObj);
        }

        @Override
        public boolean isCaptioningEnabled() {
            if (mExtraBinder != null) {
                try {
                    return mExtraBinder.isCaptioningEnabled();
                } catch (RemoteException e) {
                    Log.e(TAG, "Dead object in isCaptioningEnabled.", e);
                }
            }
            return false;
        }

        @Override
        public int getRepeatMode() {
            if (mExtraBinder != null) {
                try {
                    return mExtraBinder.getRepeatMode();
                } catch (RemoteException e) {
                    Log.e(TAG, "Dead object in getRepeatMode.", e);
                }
            }
            return PlaybackStateCompat.REPEAT_MODE_INVALID;
        }

        @Override
        public int getShuffleMode() {
            if (mExtraBinder != null) {
                try {
                    return mExtraBinder.getShuffleMode();
                } catch (RemoteException e) {
                    Log.e(TAG, "Dead object in getShuffleMode.", e);
                }
            }
            return PlaybackStateCompat.SHUFFLE_MODE_INVALID;
        }

        @Override
        public long getFlags() {
            return MediaControllerCompatApi21.getFlags(mControllerObj);
        }

        @Override
        public PlaybackInfo getPlaybackInfo() {
            Object volumeInfoObj = MediaControllerCompatApi21.getPlaybackInfo(mControllerObj);
            return volumeInfoObj != null ? new PlaybackInfo(
                    MediaControllerCompatApi21.PlaybackInfo.getPlaybackType(volumeInfoObj),
                    MediaControllerCompatApi21.PlaybackInfo.getLegacyAudioStream(volumeInfoObj),
                    MediaControllerCompatApi21.PlaybackInfo.getVolumeControl(volumeInfoObj),
                    MediaControllerCompatApi21.PlaybackInfo.getMaxVolume(volumeInfoObj),
                    MediaControllerCompatApi21.PlaybackInfo.getCurrentVolume(volumeInfoObj)) : null;
        }

        @Override
        public PendingIntent getSessionActivity() {
            return MediaControllerCompatApi21.getSessionActivity(mControllerObj);
        }

        @Override
        public void setVolumeTo(int value, int flags) {
            MediaControllerCompatApi21.setVolumeTo(mControllerObj, value, flags);
        }

        @Override
        public void adjustVolume(int direction, int flags) {
            MediaControllerCompatApi21.adjustVolume(mControllerObj, direction, flags);
        }

        @Override
        public void sendCommand(String command, Bundle params, ResultReceiver cb) {
            MediaControllerCompatApi21.sendCommand(mControllerObj, command, params, cb);
        }

        @Override
        public boolean isSessionReady() {
            return mExtraBinder != null;
        }

        @Override
        public String getPackageName() {
            return MediaControllerCompatApi21.getPackageName(mControllerObj);
        }

        @Override
        public Object getMediaController() {
            return mControllerObj;
        }

        private void requestExtraBinder() {
            sendCommand(COMMAND_GET_EXTRA_BINDER, null,
                    new ExtraBinderRequestResultReceiver(this, new Handler()));
        }

        private void processPendingCallbacks() {
            if (mExtraBinder == null) {
                return;
            }
            synchronized (mPendingCallbacks) {
                for (Callback callback : mPendingCallbacks) {
                    ExtraCallback extraCallback = new ExtraCallback(callback);
                    mCallbackMap.put(callback, extraCallback);
                    callback.mIControllerCallback = extraCallback;
                    try {
                        mExtraBinder.registerCallbackListener(extraCallback);
                    } catch (RemoteException e) {
                        Log.e(TAG, "Dead object in registerCallback.", e);
                        break;
                    }
                    callback.onSessionReady();
                }
                mPendingCallbacks.clear();
            }
        }

        private static class ExtraBinderRequestResultReceiver extends ResultReceiver {
            private WeakReference<MediaControllerImplApi21> mMediaControllerImpl;

            public ExtraBinderRequestResultReceiver(MediaControllerImplApi21 mediaControllerImpl,
                    Handler handler) {
                super(handler);
                mMediaControllerImpl = new WeakReference<>(mediaControllerImpl);
            }

            @Override
            protected void onReceiveResult(int resultCode, Bundle resultData) {
                MediaControllerImplApi21 mediaControllerImpl = mMediaControllerImpl.get();
                if (mediaControllerImpl == null || resultData == null) {
                    return;
                }
                mediaControllerImpl.mExtraBinder = IMediaSession.Stub.asInterface(
                        BundleCompat.getBinder(resultData, MediaSessionCompat.EXTRA_BINDER));
                mediaControllerImpl.processPendingCallbacks();
            }
        }

        private static class ExtraCallback extends Callback.StubCompat {
            ExtraCallback(Callback callback) {
                super(callback);
            }

            @Override
            public void onSessionDestroyed() throws RemoteException {
                // Will not be called.
                throw new AssertionError();
            }

            @Override
            public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException {
                // Will not be called.
                throw new AssertionError();
            }

            @Override
            public void onQueueChanged(List<QueueItem> queue) throws RemoteException {
                // Will not be called.
                throw new AssertionError();
            }

            @Override
            public void onQueueTitleChanged(CharSequence title) throws RemoteException {
                // Will not be called.
                throw new AssertionError();
            }

            @Override
            public void onExtrasChanged(Bundle extras) throws RemoteException {
                // Will not be called.
                throw new AssertionError();
            }

            @Override
            public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException {
                // Will not be called.
                throw new AssertionError();
            }
        }
    }

    static class TransportControlsApi21 extends TransportControls {
        protected final Object mControlsObj;

        public TransportControlsApi21(Object controlsObj) {
            mControlsObj = controlsObj;
        }

        @Override
        public void prepare() {
            sendCustomAction(MediaSessionCompat.ACTION_PREPARE, null);
        }

        @Override
        public void prepareFromMediaId(String mediaId, Bundle extras) {
            Bundle bundle = new Bundle();
            bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_MEDIA_ID, mediaId);
            bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
            sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_MEDIA_ID, bundle);
        }

        @Override
        public void prepareFromSearch(String query, Bundle extras) {
            Bundle bundle = new Bundle();
            bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_QUERY, query);
            bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
            sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_SEARCH, bundle);
        }

        @Override
        public void prepareFromUri(Uri uri, Bundle extras) {
            Bundle bundle = new Bundle();
            bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_URI, uri);
            bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
            sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_URI, bundle);
        }

        @Override
        public void play() {
            MediaControllerCompatApi21.TransportControls.play(mControlsObj);
        }

        @Override
        public void pause() {
            MediaControllerCompatApi21.TransportControls.pause(mControlsObj);
        }

        @Override
        public void stop() {
            MediaControllerCompatApi21.TransportControls.stop(mControlsObj);
        }

        @Override
        public void seekTo(long pos) {
            MediaControllerCompatApi21.TransportControls.seekTo(mControlsObj, pos);
        }

        @Override
        public void fastForward() {
            MediaControllerCompatApi21.TransportControls.fastForward(mControlsObj);
        }

        @Override
        public void rewind() {
            MediaControllerCompatApi21.TransportControls.rewind(mControlsObj);
        }

        @Override
        public void skipToNext() {
            MediaControllerCompatApi21.TransportControls.skipToNext(mControlsObj);
        }

        @Override
        public void skipToPrevious() {
            MediaControllerCompatApi21.TransportControls.skipToPrevious(mControlsObj);
        }

        @Override
        public void setRating(RatingCompat rating) {
            MediaControllerCompatApi21.TransportControls.setRating(mControlsObj,
                    rating != null ? rating.getRating() : null);
        }

        @Override
        public void setRating(RatingCompat rating, Bundle extras) {
            Bundle bundle = new Bundle();
            bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_RATING, rating);
            bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
            sendCustomAction(MediaSessionCompat.ACTION_SET_RATING, bundle);
        }

        @Override
        public void setCaptioningEnabled(boolean enabled) {
            Bundle bundle = new Bundle();
            bundle.putBoolean(MediaSessionCompat.ACTION_ARGUMENT_CAPTIONING_ENABLED, enabled);
            sendCustomAction(MediaSessionCompat.ACTION_SET_CAPTIONING_ENABLED, bundle);
        }

        @Override
        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
            Bundle bundle = new Bundle();
            bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_REPEAT_MODE, repeatMode);
            sendCustomAction(MediaSessionCompat.ACTION_SET_REPEAT_MODE, bundle);
        }

        @Override
        public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
            Bundle bundle = new Bundle();
            bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_SHUFFLE_MODE, shuffleMode);
            sendCustomAction(MediaSessionCompat.ACTION_SET_SHUFFLE_MODE, bundle);
        }

        @Override
        public void playFromMediaId(String mediaId, Bundle extras) {
            MediaControllerCompatApi21.TransportControls.playFromMediaId(mControlsObj, mediaId,
                    extras);
        }

        @Override
        public void playFromSearch(String query, Bundle extras) {
            MediaControllerCompatApi21.TransportControls.playFromSearch(mControlsObj, query,
                    extras);
        }

        @Override
        public void playFromUri(Uri uri, Bundle extras) {
            if (uri == null || Uri.EMPTY.equals(uri)) {
                throw new IllegalArgumentException(
                        "You must specify a non-empty Uri for playFromUri.");
            }
            Bundle bundle = new Bundle();
            bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_URI, uri);
            bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
            sendCustomAction(MediaSessionCompat.ACTION_PLAY_FROM_URI, bundle);
        }

        @Override
        public void skipToQueueItem(long id) {
            MediaControllerCompatApi21.TransportControls.skipToQueueItem(mControlsObj, id);
        }

        @Override
        public void sendCustomAction(CustomAction customAction, Bundle args) {
            validateCustomAction(customAction.getAction(), args);
            MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj,
                    customAction.getAction(), args);
        }

        @Override
        public void sendCustomAction(String action, Bundle args) {
            validateCustomAction(action, args);
            MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj, action,
                    args);
        }
    }

    @RequiresApi(23)
    static class MediaControllerImplApi23 extends MediaControllerImplApi21 {

        public MediaControllerImplApi23(Context context, MediaSessionCompat session) {
            super(context, session);
        }

        public MediaControllerImplApi23(Context context, MediaSessionCompat.Token sessionToken)
                throws RemoteException {
            super(context, sessionToken);
        }

        @Override
        public TransportControls getTransportControls() {
            Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
            return controlsObj != null ? new TransportControlsApi23(controlsObj) : null;
        }
    }

    @RequiresApi(23)
    static class TransportControlsApi23 extends TransportControlsApi21 {

        public TransportControlsApi23(Object controlsObj) {
            super(controlsObj);
        }

        @Override
        public void playFromUri(Uri uri, Bundle extras) {
            MediaControllerCompatApi23.TransportControls.playFromUri(mControlsObj, uri,
                    extras);
        }
    }

    @RequiresApi(24)
    static class MediaControllerImplApi24 extends MediaControllerImplApi23 {

        public MediaControllerImplApi24(Context context, MediaSessionCompat session) {
            super(context, session);
        }

        public MediaControllerImplApi24(Context context, MediaSessionCompat.Token sessionToken)
                throws RemoteException {
            super(context, sessionToken);
        }

        @Override
        public TransportControls getTransportControls() {
            Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
            return controlsObj != null ? new TransportControlsApi24(controlsObj) : null;
        }
    }

    @RequiresApi(24)
    static class TransportControlsApi24 extends TransportControlsApi23 {

        public TransportControlsApi24(Object controlsObj) {
            super(controlsObj);
        }

        @Override
        public void prepare() {
            MediaControllerCompatApi24.TransportControls.prepare(mControlsObj);
        }

        @Override
        public void prepareFromMediaId(String mediaId, Bundle extras) {
            MediaControllerCompatApi24.TransportControls.prepareFromMediaId(
                    mControlsObj, mediaId, extras);
        }

        @Override
        public void prepareFromSearch(String query, Bundle extras) {
            MediaControllerCompatApi24.TransportControls.prepareFromSearch(
                    mControlsObj, query, extras);
        }

        @Override
        public void prepareFromUri(Uri uri, Bundle extras) {
            MediaControllerCompatApi24.TransportControls.prepareFromUri(mControlsObj, uri, extras);
        }
    }
}
