/*
 * Copyright (C) 2014 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 android.app.PendingIntent;
import android.content.Context;
import android.media.AudioManager;
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.MediaMetadataCompat;
import android.support.v4.media.RatingCompat;
import android.support.v4.media.VolumeProviderCompat;
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 java.util.ArrayList;
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.
 */
public final class MediaControllerCompat {
    private static final String TAG = "MediaControllerCompat";

    private final MediaControllerImpl mImpl;
    private final MediaSessionCompat.Token mToken;

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

        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, 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 >= 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);
        }
    }

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

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

    /**
     * Get the current playback state for this session.
     *
     * @return The current PlaybackState or null
     */
    public PlaybackStateCompat getPlaybackState() {
        return mImpl.getPlaybackState();
    }

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

    /**
     * Get 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<MediaSessionCompat.QueueItem> getQueue() {
        return mImpl.getQueue();
    }

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

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

    /**
     * Get 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>
     *
     * @return The supported rating type
     */
    public int getRatingType() {
        return mImpl.getRatingType();
    }

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

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

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

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

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

    /**
     * Adjust 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(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(Callback callback, Handler handler) {
        if (callback == null) {
            throw new IllegalArgumentException("callback cannot be null");
        }
        if (handler == null) {
            handler = new Handler();
        }
        mImpl.registerCallback(callback, handler);
    }

    /**
     * Stop 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(Callback callback) {
        if (callback == null) {
            throw new IllegalArgumentException("callback cannot be null");
        }
        mImpl.unregisterCallback(callback);
    }

    /**
     * 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(String command, Bundle params, ResultReceiver cb) {
        if (TextUtils.isEmpty(command)) {
            throw new IllegalArgumentException("command cannot be null or empty");
        }
        mImpl.sendCommand(command, params, cb);
    }

    /**
     * Get 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;
        private MessageHandler mHandler;

        private boolean mRegistered = false;

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

        /**
         * 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 MediaMetadata
         */
        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<MediaSessionCompat.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 chagnes 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
        public void binderDied() {
            onSessionDestroyed();
        }

        /**
         * Set the handler to use for pre 21 callbacks.
         */
        private void setHandler(Handler handler) {
            mHandler = new MessageHandler(handler.getLooper());
        }

        private class StubApi21 implements MediaControllerCompatApi21.Callback {
            @Override
            public void onSessionDestroyed() {
                Callback.this.onSessionDestroyed();
            }

            @Override
            public void onSessionEvent(String event, Bundle extras) {
                Callback.this.onSessionEvent(event, extras);
            }

            @Override
            public void onPlaybackStateChanged(Object stateObj) {
                Callback.this.onPlaybackStateChanged(
                        PlaybackStateCompat.fromPlaybackState(stateObj));
            }

            @Override
            public void onMetadataChanged(Object metadataObj) {
                Callback.this.onMetadataChanged(
                        MediaMetadataCompat.fromMediaMetadata(metadataObj));
            }
        }

        private class StubCompat extends IMediaControllerCallback.Stub {

            @Override
            public void onEvent(String event, Bundle extras) throws RemoteException {
                mHandler.post(MessageHandler.MSG_EVENT, event, extras);
            }

            @Override
            public void onSessionDestroyed() throws RemoteException {
                mHandler.post(MessageHandler.MSG_DESTROYED, null, null);
            }

            @Override
            public void onPlaybackStateChanged(PlaybackStateCompat state) throws RemoteException {
                mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state, null);
            }

            @Override
            public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException {
                mHandler.post(MessageHandler.MSG_UPDATE_METADATA, metadata, null);
            }

            @Override
            public void onQueueChanged(List<QueueItem> queue) throws RemoteException {
                mHandler.post(MessageHandler.MSG_UPDATE_QUEUE, queue, null);
            }

            @Override
            public void onQueueTitleChanged(CharSequence title) throws RemoteException {
                mHandler.post(MessageHandler.MSG_UPDATE_QUEUE_TITLE, title, null);
            }

            @Override
            public void onExtrasChanged(Bundle extras) throws RemoteException {
                mHandler.post(MessageHandler.MSG_UPDATE_EXTRAS, extras, null);
            }

            @Override
            public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException {
                PlaybackInfo pi = null;
                if (info != null) {
                    pi = new PlaybackInfo(info.volumeType, info.audioStream, info.controlType,
                            info.maxVolume, info.currentVolume);
                }
                mHandler.post(MessageHandler.MSG_UPDATE_VOLUME, pi, 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;

            public 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<MediaSessionCompat.QueueItem>) msg.obj);
                        break;
                    case MSG_UPDATE_QUEUE_TITLE:
                        onQueueTitleChanged((CharSequence) 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;
                }
            }

            public void post(int what, Object obj, Bundle data) {
                Message msg = obtainMessage(what, obj);
                msg.setData(data);
                msg.sendToTarget();
            }
        }
    }

    /**
     * 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 {
        TransportControls() {
        }

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

        /**
         * Play 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();

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

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

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

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

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

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

        /**
         * Send 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);

        /**
         * Send the id and args from a custom action for the
         * {@link MediaSessionCompat} to perform.
         *
         * @see #sendCustomAction(PlaybackStateCompat.CustomAction action,
         *      Bundle args)
         * @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;
        }

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

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

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

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

        /**
         * Get 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<MediaSessionCompat.QueueItem> getQueue();
        CharSequence getQueueTitle();
        Bundle getExtras();
        int getRatingType();
        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);

        String getPackageName();
        Object getMediaController();
    }

    static class MediaControllerImplBase implements MediaControllerImpl {
        private MediaSessionCompat.Token mToken;
        private IMediaSession mBinder;
        private TransportControls mTransportControls;

        public MediaControllerImplBase(MediaSessionCompat.Token token) {
            mToken = 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);
                callback.setHandler(handler);
                callback.mRegistered = true;
            } 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);
                callback.mRegistered = false;
            } 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<MediaSessionCompat.QueueItem> getQueue() {
            try {
                return mBinder.getQueue();
            } catch (RemoteException e) {
                Log.e(TAG, "Dead object in getQueue. " + e);
            }
            return null;
        }

        @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 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 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 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 sendCustomAction(CustomAction customAction, Bundle args) {
            sendCustomAction(customAction.getAction(), args);
        }

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

    static class MediaControllerImplApi21 implements MediaControllerImpl {
        protected final Object mControllerObj;

        public MediaControllerImplApi21(Context context, MediaSessionCompat session) {
            mControllerObj = MediaControllerCompatApi21.fromToken(context,
                    session.getSessionToken().getToken());
        }

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

        @Override
        public void registerCallback(Callback callback, Handler handler) {
            MediaControllerCompatApi21.registerCallback(mControllerObj, callback.mCallbackObj, handler);
        }

        @Override
        public void unregisterCallback(Callback callback) {
            MediaControllerCompatApi21.unregisterCallback(mControllerObj, callback.mCallbackObj);
        }

        @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() {
            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<MediaSessionCompat.QueueItem> getQueue() {
            List<Object> queueObjs = MediaControllerCompatApi21.getQueue(mControllerObj);
            if (queueObjs == null) {
                return null;
            }
            List<MediaSessionCompat.QueueItem> queue =
                    new ArrayList<MediaSessionCompat.QueueItem>();
            for (Object item : queueObjs) {
                queue.add(MediaSessionCompat.QueueItem.obtain(item));
            }
            return queue;
        }

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

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

        @Override
        public int getRatingType() {
            return MediaControllerCompatApi21.getRatingType(mControllerObj);
        }

        @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 String getPackageName() {
            return MediaControllerCompatApi21.getPackageName(mControllerObj);
        }

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

    static class TransportControlsApi21 extends TransportControls {
        protected final Object mControlsObj;

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

        @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 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) {
            MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj,
                    customAction.getAction(), args);
        }

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

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

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