1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v4.media.session;
18
19import android.app.Activity;
20import android.app.PendingIntent;
21import android.content.Context;
22import android.media.AudioManager;
23import android.media.session.MediaController;
24import android.net.Uri;
25import android.os.Bundle;
26import android.os.Handler;
27import android.os.IBinder;
28import android.os.Looper;
29import android.os.Message;
30import android.os.RemoteException;
31import android.os.ResultReceiver;
32import android.support.annotation.NonNull;
33import android.support.annotation.RequiresApi;
34import android.support.v4.app.BundleCompat;
35import android.support.v4.app.SupportActivity;
36import android.support.v4.media.MediaDescriptionCompat;
37import android.support.v4.media.MediaMetadataCompat;
38import android.support.v4.media.RatingCompat;
39import android.support.v4.media.VolumeProviderCompat;
40import android.support.v4.media.session.MediaSessionCompat.QueueItem;
41import android.support.v4.media.session.PlaybackStateCompat.CustomAction;
42import android.text.TextUtils;
43import android.util.Log;
44import android.view.KeyEvent;
45
46import java.lang.ref.WeakReference;
47import java.util.ArrayList;
48import java.util.HashMap;
49import java.util.List;
50
51/**
52 * Allows an app to interact with an ongoing media session. Media buttons and
53 * other commands can be sent to the session. A callback may be registered to
54 * receive updates from the session, such as metadata and play state changes.
55 * <p>
56 * A MediaController can be created if you have a {@link MediaSessionCompat.Token}
57 * from the session owner.
58 * <p>
59 * MediaController objects are thread-safe.
60 * <p>
61 * This is a helper for accessing features in {@link android.media.session.MediaSession}
62 * introduced after API level 4 in a backwards compatible fashion.
63 * <p class="note">
64 * If MediaControllerCompat is created with a {@link MediaSessionCompat.Token session token}
65 * from another process, following methods will not work directly after the creation if the
66 * {@link MediaSessionCompat.Token session token} is not passed through a
67 * {@link android.support.v4.media.MediaBrowserCompat}:
68 * <ul>
69 * <li>{@link #getPlaybackState()}.{@link PlaybackStateCompat#getExtras() getExtras()}</li>
70 * <li>{@link #isCaptioningEnabled()}</li>
71 * <li>{@link #getRepeatMode()}</li>
72 * <li>{@link #isShuffleModeEnabled()}</li>
73 * </ul></p>
74 *
75 * <div class="special reference">
76 * <h3>Developer Guides</h3>
77 * <p>For information about building your media application, read the
78 * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p>
79 * </div>
80 */
81public final class MediaControllerCompat {
82    static final String TAG = "MediaControllerCompat";
83
84    static final String COMMAND_GET_EXTRA_BINDER =
85            "android.support.v4.media.session.command.GET_EXTRA_BINDER";
86    static final String COMMAND_ADD_QUEUE_ITEM =
87            "android.support.v4.media.session.command.ADD_QUEUE_ITEM";
88    static final String COMMAND_ADD_QUEUE_ITEM_AT =
89            "android.support.v4.media.session.command.ADD_QUEUE_ITEM_AT";
90    static final String COMMAND_REMOVE_QUEUE_ITEM =
91            "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM";
92    static final String COMMAND_REMOVE_QUEUE_ITEM_AT =
93            "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM_AT";
94
95    static final String COMMAND_ARGUMENT_MEDIA_DESCRIPTION =
96            "android.support.v4.media.session.command.ARGUMENT_MEDIA_DESCRIPTION";
97    static final String COMMAND_ARGUMENT_INDEX =
98            "android.support.v4.media.session.command.ARGUMENT_INDEX";
99
100    private static class MediaControllerExtraData extends SupportActivity.ExtraData {
101        private final MediaControllerCompat mMediaController;
102
103        MediaControllerExtraData(MediaControllerCompat mediaController) {
104            mMediaController = mediaController;
105        }
106
107        MediaControllerCompat getMediaController() {
108            return mMediaController;
109        }
110    }
111
112    /**
113     * Sets a {@link MediaControllerCompat} in the {@code activity} for later retrieval via
114     * {@link #getMediaController(Activity)}.
115     *
116     * <p>This is compatible with {@link Activity#setMediaController(MediaController)}.
117     * If {@code activity} inherits {@link android.support.v4.app.FragmentActivity}, the
118     * {@code mediaController} will be saved in the {@code activity}. In addition to that,
119     * on API 21 and later, {@link Activity#setMediaController(MediaController)} will be
120     * called.</p>
121     *
122     * @param activity The activity to set the {@code mediaController} in, must not be null.
123     * @param mediaController The controller for the session which should receive
124     *     media keys and volume changes on API 21 and later.
125     * @see #getMediaController(Activity)
126     * @see Activity#setMediaController(android.media.session.MediaController)
127     */
128    public static void setMediaController(@NonNull Activity activity,
129            MediaControllerCompat mediaController) {
130        if (activity instanceof SupportActivity) {
131            ((SupportActivity) activity).putExtraData(
132                    new MediaControllerExtraData(mediaController));
133        }
134        if (android.os.Build.VERSION.SDK_INT >= 21) {
135            Object controllerObj = null;
136            if (mediaController != null) {
137                Object sessionTokenObj = mediaController.getSessionToken().getToken();
138                controllerObj = MediaControllerCompatApi21.fromToken(activity, sessionTokenObj);
139            }
140            MediaControllerCompatApi21.setMediaController(activity, controllerObj);
141        }
142    }
143
144    /**
145     * Retrieves the {@link MediaControllerCompat} set in the activity by
146     * {@link #setMediaController(Activity, MediaControllerCompat)} for sending media key and volume
147     * events.
148     *
149     * <p>This is compatible with {@link Activity#getMediaController()}.</p>
150     *
151     * @param activity The activity to get the media controller from, must not be null.
152     * @return The controller which should receive events.
153     * @see #setMediaController(Activity, MediaControllerCompat)
154     */
155    public static MediaControllerCompat getMediaController(@NonNull Activity activity) {
156        if (activity instanceof SupportActivity) {
157            MediaControllerExtraData extraData =
158                    ((SupportActivity) activity).getExtraData(MediaControllerExtraData.class);
159            return extraData != null ? extraData.getMediaController() : null;
160        } else if (android.os.Build.VERSION.SDK_INT >= 21) {
161            Object controllerObj = MediaControllerCompatApi21.getMediaController(activity);
162            if (controllerObj == null) {
163                return null;
164            }
165            Object sessionTokenObj = MediaControllerCompatApi21.getSessionToken(controllerObj);
166            try {
167                return new MediaControllerCompat(activity,
168                        MediaSessionCompat.Token.fromToken(sessionTokenObj));
169            } catch (RemoteException e) {
170                Log.e(TAG, "Dead object in getMediaController.", e);
171            }
172        }
173        return null;
174    }
175
176    private static void validateCustomAction(String action, Bundle args) {
177        if (action == null) {
178            return;
179        }
180        switch(action) {
181            case MediaSessionCompat.ACTION_FOLLOW:
182            case MediaSessionCompat.ACTION_UNFOLLOW:
183                if (args == null
184                        || !args.containsKey(MediaSessionCompat.ACTION_ARGUMENT_MEDIA_ATTRIBUTE)) {
185                    throw new IllegalArgumentException("An extra field "
186                            + MediaSessionCompat.ACTION_ARGUMENT_MEDIA_ATTRIBUTE + " is required "
187                            + "for this action " + action + ".");
188                }
189                break;
190        }
191    }
192
193    private final MediaControllerImpl mImpl;
194    private final MediaSessionCompat.Token mToken;
195
196    /**
197     * Creates a media controller from a session.
198     *
199     * @param session The session to be controlled.
200     */
201    public MediaControllerCompat(Context context, @NonNull MediaSessionCompat session) {
202        if (session == null) {
203            throw new IllegalArgumentException("session must not be null");
204        }
205        mToken = session.getSessionToken();
206
207        if (android.os.Build.VERSION.SDK_INT >= 24) {
208            mImpl = new MediaControllerImplApi24(context, session);
209        } else if (android.os.Build.VERSION.SDK_INT >= 23) {
210            mImpl = new MediaControllerImplApi23(context, session);
211        } else if (android.os.Build.VERSION.SDK_INT >= 21) {
212            mImpl = new MediaControllerImplApi21(context, session);
213        } else {
214            mImpl = new MediaControllerImplBase(mToken);
215        }
216    }
217
218    /**
219     * Creates a media controller from a session token which may have
220     * been obtained from another process.
221     *
222     * @param sessionToken The token of the session to be controlled.
223     * @throws RemoteException if the session is not accessible.
224     */
225    public MediaControllerCompat(Context context, @NonNull MediaSessionCompat.Token sessionToken)
226            throws RemoteException {
227        if (sessionToken == null) {
228            throw new IllegalArgumentException("sessionToken must not be null");
229        }
230        mToken = sessionToken;
231
232        if (android.os.Build.VERSION.SDK_INT >= 24) {
233            mImpl = new MediaControllerImplApi24(context, sessionToken);
234        } else if (android.os.Build.VERSION.SDK_INT >= 23) {
235            mImpl = new MediaControllerImplApi23(context, sessionToken);
236        } else if (android.os.Build.VERSION.SDK_INT >= 21) {
237            mImpl = new MediaControllerImplApi21(context, sessionToken);
238        } else {
239            mImpl = new MediaControllerImplBase(mToken);
240        }
241    }
242
243    /**
244     * Get a {@link TransportControls} instance for this session.
245     *
246     * @return A controls instance
247     */
248    public TransportControls getTransportControls() {
249        return mImpl.getTransportControls();
250    }
251
252    /**
253     * Send the specified media button event to the session. Only media keys can
254     * be sent by this method, other keys will be ignored.
255     *
256     * @param keyEvent The media button event to dispatch.
257     * @return true if the event was sent to the session, false otherwise.
258     */
259    public boolean dispatchMediaButtonEvent(KeyEvent keyEvent) {
260        if (keyEvent == null) {
261            throw new IllegalArgumentException("KeyEvent may not be null");
262        }
263        return mImpl.dispatchMediaButtonEvent(keyEvent);
264    }
265
266    /**
267     * Get the current playback state for this session.
268     *
269     * @return The current PlaybackState or null
270     */
271    public PlaybackStateCompat getPlaybackState() {
272        return mImpl.getPlaybackState();
273    }
274
275    /**
276     * Get the current metadata for this session.
277     *
278     * @return The current MediaMetadata or null.
279     */
280    public MediaMetadataCompat getMetadata() {
281        return mImpl.getMetadata();
282    }
283
284    /**
285     * Get the current play queue for this session if one is set. If you only
286     * care about the current item {@link #getMetadata()} should be used.
287     *
288     * @return The current play queue or null.
289     */
290    public List<QueueItem> getQueue() {
291        return mImpl.getQueue();
292    }
293
294    /**
295     * Add a queue item from the given {@code description} at the end of the play queue
296     * of this session. Not all sessions may support this. To know whether the session supports
297     * this, get the session's flags with {@link #getFlags()} and check that the flag
298     * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
299     *
300     * @param description The {@link MediaDescriptionCompat} for creating the
301     *            {@link MediaSessionCompat.QueueItem} to be inserted.
302     * @throws UnsupportedOperationException If this session doesn't support this.
303     * @see #getFlags()
304     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
305     */
306    public void addQueueItem(MediaDescriptionCompat description) {
307        mImpl.addQueueItem(description);
308    }
309
310    /**
311     * Add a queue item from the given {@code description} at the specified position
312     * in the play queue of this session. Shifts the queue item currently at that position
313     * (if any) and any subsequent queue items to the right (adds one to their indices).
314     * Not all sessions may support this. To know whether the session supports this,
315     * get the session's flags with {@link #getFlags()} and check that the flag
316     * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
317     *
318     * @param description The {@link MediaDescriptionCompat} for creating the
319     *            {@link MediaSessionCompat.QueueItem} to be inserted.
320     * @param index The index at which the created {@link MediaSessionCompat.QueueItem}
321     *            is to be inserted.
322     * @throws UnsupportedOperationException If this session doesn't support this.
323     * @see #getFlags()
324     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
325     */
326    public void addQueueItem(MediaDescriptionCompat description, int index) {
327        mImpl.addQueueItem(description, index);
328    }
329
330    /**
331     * Remove the first occurrence of the specified {@link MediaSessionCompat.QueueItem}
332     * with the given {@link MediaDescriptionCompat description} in the play queue of the
333     * associated session. Not all sessions may support this. To know whether the session supports
334     * this, get the session's flags with {@link #getFlags()} and check that the flag
335     * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
336     *
337     * @param description The {@link MediaDescriptionCompat} for denoting the
338     *            {@link MediaSessionCompat.QueueItem} to be removed.
339     * @throws UnsupportedOperationException If this session doesn't support this.
340     * @see #getFlags()
341     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
342     */
343    public void removeQueueItem(MediaDescriptionCompat description) {
344        mImpl.removeQueueItem(description);
345    }
346
347    /**
348     * Remove an queue item at the specified position in the play queue
349     * of this session. Not all sessions may support this. To know whether the session supports
350     * this, get the session's flags with {@link #getFlags()} and check that the flag
351     * {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set.
352     *
353     * @param index The index of the element to be removed.
354     * @throws UnsupportedOperationException If this session doesn't support this.
355     * @see #getFlags()
356     * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS
357     * @deprecated Use {@link #removeQueueItem(MediaDescriptionCompat)} instead.
358     */
359    @Deprecated
360    public void removeQueueItemAt(int index) {
361        List<QueueItem> queue = getQueue();
362        if (queue != null && index >= 0 && index < queue.size()) {
363            QueueItem item = queue.get(index);
364            if (item != null) {
365                removeQueueItem(item.getDescription());
366            }
367        }
368    }
369
370    /**
371     * Get the queue title for this session.
372     */
373    public CharSequence getQueueTitle() {
374        return mImpl.getQueueTitle();
375    }
376
377    /**
378     * Get the extras for this session.
379     */
380    public Bundle getExtras() {
381        return mImpl.getExtras();
382    }
383
384    /**
385     * Get the rating type supported by the session. One of:
386     * <ul>
387     * <li>{@link RatingCompat#RATING_NONE}</li>
388     * <li>{@link RatingCompat#RATING_HEART}</li>
389     * <li>{@link RatingCompat#RATING_THUMB_UP_DOWN}</li>
390     * <li>{@link RatingCompat#RATING_3_STARS}</li>
391     * <li>{@link RatingCompat#RATING_4_STARS}</li>
392     * <li>{@link RatingCompat#RATING_5_STARS}</li>
393     * <li>{@link RatingCompat#RATING_PERCENTAGE}</li>
394     * </ul>
395     *
396     * @return The supported rating type
397     */
398    public int getRatingType() {
399        return mImpl.getRatingType();
400    }
401
402    /**
403     * Return whether captioning is enabled for this session.
404     *
405     * @return {@code true} if captioning is enabled, {@code false} if disabled or not set.
406     */
407    public boolean isCaptioningEnabled() {
408        return mImpl.isCaptioningEnabled();
409    }
410
411    /**
412     * Get the repeat mode for this session.
413     *
414     * @return The latest repeat mode set to the session, or
415     *         {@link PlaybackStateCompat#REPEAT_MODE_NONE} if not set.
416     */
417    public int getRepeatMode() {
418        return mImpl.getRepeatMode();
419    }
420
421    /**
422     * Return whether the shuffle mode is enabled for this session.
423     *
424     * @return {@code true} if the shuffle mode is enabled, {@code false} if disabled or not set.
425     * @deprecated Use {@link #getShuffleMode} instead.
426     */
427    @Deprecated
428    public boolean isShuffleModeEnabled() {
429        return mImpl.isShuffleModeEnabled();
430    }
431
432    /**
433     * Get the shuffle mode for this session.
434     *
435     * @return The latest shuffle mode set to the session, or
436     *         {@link PlaybackStateCompat#SHUFFLE_MODE_NONE} if not set.
437     */
438    public int getShuffleMode() {
439        return mImpl.getShuffleMode();
440    }
441
442    /**
443     * Get the flags for this session. Flags are defined in
444     * {@link MediaSessionCompat}.
445     *
446     * @return The current set of flags for the session.
447     */
448    public long getFlags() {
449        return mImpl.getFlags();
450    }
451
452    /**
453     * Get the current playback info for this session.
454     *
455     * @return The current playback info or null.
456     */
457    public PlaybackInfo getPlaybackInfo() {
458        return mImpl.getPlaybackInfo();
459    }
460
461    /**
462     * Get an intent for launching UI associated with this session if one
463     * exists.
464     *
465     * @return A {@link PendingIntent} to launch UI or null.
466     */
467    public PendingIntent getSessionActivity() {
468        return mImpl.getSessionActivity();
469    }
470
471    /**
472     * Get the token for the session this controller is connected to.
473     *
474     * @return The session's token.
475     */
476    public MediaSessionCompat.Token getSessionToken() {
477        return mToken;
478    }
479
480    /**
481     * Set the volume of the output this session is playing on. The command will
482     * be ignored if it does not support
483     * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in
484     * {@link AudioManager} may be used to affect the handling.
485     *
486     * @see #getPlaybackInfo()
487     * @param value The value to set it to, between 0 and the reported max.
488     * @param flags Flags from {@link AudioManager} to include with the volume
489     *            request.
490     */
491    public void setVolumeTo(int value, int flags) {
492        mImpl.setVolumeTo(value, flags);
493    }
494
495    /**
496     * Adjust the volume of the output this session is playing on. The direction
497     * must be one of {@link AudioManager#ADJUST_LOWER},
498     * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
499     * The command will be ignored if the session does not support
500     * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
501     * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in
502     * {@link AudioManager} may be used to affect the handling.
503     *
504     * @see #getPlaybackInfo()
505     * @param direction The direction to adjust the volume in.
506     * @param flags Any flags to pass with the command.
507     */
508    public void adjustVolume(int direction, int flags) {
509        mImpl.adjustVolume(direction, flags);
510    }
511
512    /**
513     * Adds a callback to receive updates from the Session. Updates will be
514     * posted on the caller's thread.
515     *
516     * @param callback The callback object, must not be null.
517     */
518    public void registerCallback(@NonNull Callback callback) {
519        registerCallback(callback, null);
520    }
521
522    /**
523     * Adds a callback to receive updates from the session. Updates will be
524     * posted on the specified handler's thread.
525     *
526     * @param callback The callback object, must not be null.
527     * @param handler The handler to post updates on. If null the callers thread
528     *            will be used.
529     */
530    public void registerCallback(@NonNull Callback callback, Handler handler) {
531        if (callback == null) {
532            throw new IllegalArgumentException("callback must not be null");
533        }
534        if (handler == null) {
535            handler = new Handler();
536        }
537        mImpl.registerCallback(callback, handler);
538    }
539
540    /**
541     * Stop receiving updates on the specified callback. If an update has
542     * already been posted you may still receive it after calling this method.
543     *
544     * @param callback The callback to remove
545     */
546    public void unregisterCallback(@NonNull Callback callback) {
547        if (callback == null) {
548            throw new IllegalArgumentException("callback must not be null");
549        }
550        mImpl.unregisterCallback(callback);
551    }
552
553    /**
554     * Sends a generic command to the session. It is up to the session creator
555     * to decide what commands and parameters they will support. As such,
556     * commands should only be sent to sessions that the controller owns.
557     *
558     * @param command The command to send
559     * @param params Any parameters to include with the command
560     * @param cb The callback to receive the result on
561     */
562    public void sendCommand(@NonNull String command, Bundle params, ResultReceiver cb) {
563        if (TextUtils.isEmpty(command)) {
564            throw new IllegalArgumentException("command must neither be null nor empty");
565        }
566        mImpl.sendCommand(command, params, cb);
567    }
568
569    /**
570     * Get the session owner's package name.
571     *
572     * @return The package name of of the session owner.
573     */
574    public String getPackageName() {
575        return mImpl.getPackageName();
576    }
577
578    /**
579     * Gets the underlying framework
580     * {@link android.media.session.MediaController} object.
581     * <p>
582     * This method is only supported on API 21+.
583     * </p>
584     *
585     * @return The underlying {@link android.media.session.MediaController}
586     *         object, or null if none.
587     */
588    public Object getMediaController() {
589        return mImpl.getMediaController();
590    }
591
592    /**
593     * Callback for receiving updates on from the session. A Callback can be
594     * registered using {@link #registerCallback}
595     */
596    public static abstract class Callback implements IBinder.DeathRecipient {
597        private final Object mCallbackObj;
598        MessageHandler mHandler;
599        boolean mHasExtraCallback;
600
601        boolean mRegistered = false;
602
603        public Callback() {
604            if (android.os.Build.VERSION.SDK_INT >= 21) {
605                mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21());
606            } else {
607                mCallbackObj = new StubCompat();
608            }
609        }
610
611        /**
612         * Override to handle the session being destroyed. The session is no
613         * longer valid after this call and calls to it will be ignored.
614         */
615        public void onSessionDestroyed() {
616        }
617
618        /**
619         * Override to handle custom events sent by the session owner without a
620         * specified interface. Controllers should only handle these for
621         * sessions they own.
622         *
623         * @param event The event from the session.
624         * @param extras Optional parameters for the event.
625         */
626        public void onSessionEvent(String event, Bundle extras) {
627        }
628
629        /**
630         * Override to handle changes in playback state.
631         *
632         * @param state The new playback state of the session
633         */
634        public void onPlaybackStateChanged(PlaybackStateCompat state) {
635        }
636
637        /**
638         * Override to handle changes to the current metadata.
639         *
640         * @param metadata The current metadata for the session or null if none.
641         * @see MediaMetadataCompat
642         */
643        public void onMetadataChanged(MediaMetadataCompat metadata) {
644        }
645
646        /**
647         * Override to handle changes to items in the queue.
648         *
649         * @see MediaSessionCompat.QueueItem
650         * @param queue A list of items in the current play queue. It should
651         *            include the currently playing item as well as previous and
652         *            upcoming items if applicable.
653         */
654        public void onQueueChanged(List<QueueItem> queue) {
655        }
656
657        /**
658         * Override to handle changes to the queue title.
659         *
660         * @param title The title that should be displayed along with the play
661         *            queue such as "Now Playing". May be null if there is no
662         *            such title.
663         */
664        public void onQueueTitleChanged(CharSequence title) {
665        }
666
667        /**
668         * Override to handle chagnes to the {@link MediaSessionCompat} extras.
669         *
670         * @param extras The extras that can include other information
671         *            associated with the {@link MediaSessionCompat}.
672         */
673        public void onExtrasChanged(Bundle extras) {
674        }
675
676        /**
677         * Override to handle changes to the audio info.
678         *
679         * @param info The current audio info for this session.
680         */
681        public void onAudioInfoChanged(PlaybackInfo info) {
682        }
683
684        /**
685         * Override to handle changes to the captioning enabled status.
686         *
687         * @param enabled {@code true} if captioning is enabled, {@code false} otherwise.
688         */
689        public void onCaptioningEnabledChanged(boolean enabled) {
690        }
691
692        /**
693         * Override to handle changes to the repeat mode.
694         *
695         * @param repeatMode The repeat mode. It should be one of followings:
696         *                   {@link PlaybackStateCompat#REPEAT_MODE_NONE},
697         *                   {@link PlaybackStateCompat#REPEAT_MODE_ONE},
698         *                   {@link PlaybackStateCompat#REPEAT_MODE_ALL},
699         *                   {@link PlaybackStateCompat#REPEAT_MODE_GROUP}
700         */
701        public void onRepeatModeChanged(@PlaybackStateCompat.RepeatMode int repeatMode) {
702        }
703
704        /**
705         * Override to handle changes to the shuffle mode.
706         *
707         * @param enabled {@code true} if the shuffle mode is enabled, {@code false} otherwise.
708         * @deprecated Use {@link #onShuffleModeChanged(int)} instead.
709         */
710        @Deprecated
711        public void onShuffleModeChanged(boolean enabled) {
712        }
713
714        /**
715         * Override to handle changes to the shuffle mode.
716         *
717         * @param shuffleMode The shuffle mode. Must be one of the followings:
718         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_NONE},
719         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_ALL},
720         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP}
721         */
722        public void onShuffleModeChanged(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
723        }
724
725        @Override
726        public void binderDied() {
727            onSessionDestroyed();
728        }
729
730        /**
731         * Set the handler to use for pre 21 callbacks.
732         */
733        private void setHandler(Handler handler) {
734            mHandler = new MessageHandler(handler.getLooper());
735        }
736
737        private class StubApi21 implements MediaControllerCompatApi21.Callback {
738            StubApi21() {
739            }
740
741            @Override
742            public void onSessionDestroyed() {
743                Callback.this.onSessionDestroyed();
744            }
745
746            @Override
747            public void onSessionEvent(String event, Bundle extras) {
748                if (mHasExtraCallback && android.os.Build.VERSION.SDK_INT < 23) {
749                    // Ignore. ExtraCallback will handle this.
750                } else {
751                    Callback.this.onSessionEvent(event, extras);
752                }
753            }
754
755            @Override
756            public void onPlaybackStateChanged(Object stateObj) {
757                if (mHasExtraCallback) {
758                    // Ignore. ExtraCallback will handle this.
759                } else {
760                    Callback.this.onPlaybackStateChanged(
761                            PlaybackStateCompat.fromPlaybackState(stateObj));
762                }
763            }
764
765            @Override
766            public void onMetadataChanged(Object metadataObj) {
767                Callback.this.onMetadataChanged(MediaMetadataCompat.fromMediaMetadata(metadataObj));
768            }
769
770            @Override
771            public void onQueueChanged(List<?> queue) {
772                Callback.this.onQueueChanged(QueueItem.fromQueueItemList(queue));
773            }
774
775            @Override
776            public void onQueueTitleChanged(CharSequence title) {
777                Callback.this.onQueueTitleChanged(title);
778            }
779
780            @Override
781            public void onExtrasChanged(Bundle extras) {
782                Callback.this.onExtrasChanged(extras);
783            }
784
785            @Override
786            public void onAudioInfoChanged(
787                    int type, int stream, int control, int max, int current) {
788                Callback.this.onAudioInfoChanged(
789                        new PlaybackInfo(type, stream, control, max, current));
790            }
791        }
792
793        private class StubCompat extends IMediaControllerCallback.Stub {
794
795            StubCompat() {
796            }
797
798            @Override
799            public void onEvent(String event, Bundle extras) throws RemoteException {
800                mHandler.post(MessageHandler.MSG_EVENT, event, extras);
801            }
802
803            @Override
804            public void onSessionDestroyed() throws RemoteException {
805                mHandler.post(MessageHandler.MSG_DESTROYED, null, null);
806            }
807
808            @Override
809            public void onPlaybackStateChanged(PlaybackStateCompat state) throws RemoteException {
810                mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state, null);
811            }
812
813            @Override
814            public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException {
815                mHandler.post(MessageHandler.MSG_UPDATE_METADATA, metadata, null);
816            }
817
818            @Override
819            public void onQueueChanged(List<QueueItem> queue) throws RemoteException {
820                mHandler.post(MessageHandler.MSG_UPDATE_QUEUE, queue, null);
821            }
822
823            @Override
824            public void onQueueTitleChanged(CharSequence title) throws RemoteException {
825                mHandler.post(MessageHandler.MSG_UPDATE_QUEUE_TITLE, title, null);
826            }
827
828            @Override
829            public void onCaptioningEnabledChanged(boolean enabled) throws RemoteException {
830                mHandler.post(MessageHandler.MSG_UPDATE_CAPTIONING_ENABLED, enabled, null);
831            }
832
833            @Override
834            public void onRepeatModeChanged(int repeatMode) throws RemoteException {
835                mHandler.post(MessageHandler.MSG_UPDATE_REPEAT_MODE, repeatMode, null);
836            }
837
838            @Override
839            public void onShuffleModeChangedDeprecated(boolean enabled) throws RemoteException {
840                mHandler.post(MessageHandler.MSG_UPDATE_SHUFFLE_MODE_DEPRECATED, enabled, null);
841            }
842
843            @Override
844            public void onShuffleModeChanged(int shuffleMode) throws RemoteException {
845                mHandler.post(MessageHandler.MSG_UPDATE_SHUFFLE_MODE, shuffleMode, null);
846            }
847
848            @Override
849            public void onExtrasChanged(Bundle extras) throws RemoteException {
850                mHandler.post(MessageHandler.MSG_UPDATE_EXTRAS, extras, null);
851            }
852
853            @Override
854            public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException {
855                PlaybackInfo pi = null;
856                if (info != null) {
857                    pi = new PlaybackInfo(info.volumeType, info.audioStream, info.controlType,
858                            info.maxVolume, info.currentVolume);
859                }
860                mHandler.post(MessageHandler.MSG_UPDATE_VOLUME, pi, null);
861            }
862        }
863
864        private class MessageHandler extends Handler {
865            private static final int MSG_EVENT = 1;
866            private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
867            private static final int MSG_UPDATE_METADATA = 3;
868            private static final int MSG_UPDATE_VOLUME = 4;
869            private static final int MSG_UPDATE_QUEUE = 5;
870            private static final int MSG_UPDATE_QUEUE_TITLE = 6;
871            private static final int MSG_UPDATE_EXTRAS = 7;
872            private static final int MSG_DESTROYED = 8;
873            private static final int MSG_UPDATE_REPEAT_MODE = 9;
874            private static final int MSG_UPDATE_SHUFFLE_MODE_DEPRECATED = 10;
875            private static final int MSG_UPDATE_CAPTIONING_ENABLED = 11;
876            private static final int MSG_UPDATE_SHUFFLE_MODE = 12;
877
878            public MessageHandler(Looper looper) {
879                super(looper);
880            }
881
882            @Override
883            public void handleMessage(Message msg) {
884                if (!mRegistered) {
885                    return;
886                }
887                switch (msg.what) {
888                    case MSG_EVENT:
889                        onSessionEvent((String) msg.obj, msg.getData());
890                        break;
891                    case MSG_UPDATE_PLAYBACK_STATE:
892                        onPlaybackStateChanged((PlaybackStateCompat) msg.obj);
893                        break;
894                    case MSG_UPDATE_METADATA:
895                        onMetadataChanged((MediaMetadataCompat) msg.obj);
896                        break;
897                    case MSG_UPDATE_QUEUE:
898                        onQueueChanged((List<QueueItem>) msg.obj);
899                        break;
900                    case MSG_UPDATE_QUEUE_TITLE:
901                        onQueueTitleChanged((CharSequence) msg.obj);
902                        break;
903                    case MSG_UPDATE_CAPTIONING_ENABLED:
904                        onCaptioningEnabledChanged((boolean) msg.obj);
905                        break;
906                    case MSG_UPDATE_REPEAT_MODE:
907                        onRepeatModeChanged((int) msg.obj);
908                        break;
909                    case MSG_UPDATE_SHUFFLE_MODE_DEPRECATED:
910                        onShuffleModeChanged((boolean) msg.obj);
911                        break;
912                    case MSG_UPDATE_SHUFFLE_MODE:
913                        onShuffleModeChanged((int) msg.obj);
914                        break;
915                    case MSG_UPDATE_EXTRAS:
916                        onExtrasChanged((Bundle) msg.obj);
917                        break;
918                    case MSG_UPDATE_VOLUME:
919                        onAudioInfoChanged((PlaybackInfo) msg.obj);
920                        break;
921                    case MSG_DESTROYED:
922                        onSessionDestroyed();
923                        break;
924                }
925            }
926
927            public void post(int what, Object obj, Bundle data) {
928                Message msg = obtainMessage(what, obj);
929                msg.setData(data);
930                msg.sendToTarget();
931            }
932        }
933    }
934
935    /**
936     * Interface for controlling media playback on a session. This allows an app
937     * to send media transport commands to the session.
938     */
939    public static abstract class TransportControls {
940        TransportControls() {
941        }
942
943        /**
944         * Request that the player prepare its playback without audio focus. In other words, other
945         * session can continue to play during the preparation of this session. This method can be
946         * used to speed up the start of the playback. Once the preparation is done, the session
947         * will change its playback state to {@link PlaybackStateCompat#STATE_PAUSED}. Afterwards,
948         * {@link #play} can be called to start playback. If the preparation is not needed,
949         * {@link #play} can be directly called without this method.
950         */
951        public abstract void prepare();
952
953        /**
954         * Request that the player prepare playback for a specific media id. In other words, other
955         * session can continue to play during the preparation of this session. This method can be
956         * used to speed up the start of the playback. Once the preparation is
957         * done, the session will change its playback state to
958         * {@link PlaybackStateCompat#STATE_PAUSED}. Afterwards, {@link #play} can be called to
959         * start playback. If the preparation is not needed, {@link #playFromMediaId} can
960         * be directly called without this method.
961         *
962         * @param mediaId The id of the requested media.
963         * @param extras Optional extras that can include extra information about the media item
964         *               to be prepared.
965         */
966        public abstract void prepareFromMediaId(String mediaId, Bundle extras);
967
968        /**
969         * Request that the player prepare playback for a specific search query.
970         * An empty or null query should be treated as a request to prepare any
971         * music. In other words, other session can continue to play during
972         * the preparation of this session. This method can be used to speed up the start of the
973         * playback. Once the preparation is done, the session will change its playback state to
974         * {@link PlaybackStateCompat#STATE_PAUSED}. Afterwards, {@link #play} can be called to
975         * start playback. If the preparation is not needed, {@link #playFromSearch} can be directly
976         * called without this method.
977         *
978         * @param query The search query.
979         * @param extras Optional extras that can include extra information
980         *               about the query.
981         */
982        public abstract void prepareFromSearch(String query, Bundle extras);
983
984        /**
985         * Request that the player prepare playback for a specific {@link Uri}.
986         * In other words, other session can continue to play during the preparation of this
987         * session. This method can be used to speed up the start of the playback.
988         * Once the preparation is done, the session will change its playback state to
989         * {@link PlaybackStateCompat#STATE_PAUSED}. Afterwards, {@link #play} can be called to
990         * start playback. If the preparation is not needed, {@link #playFromUri} can be directly
991         * called without this method.
992         *
993         * @param uri The URI of the requested media.
994         * @param extras Optional extras that can include extra information about the media item
995         *               to be prepared.
996         */
997        public abstract void prepareFromUri(Uri uri, Bundle extras);
998
999        /**
1000         * Request that the player start its playback at its current position.
1001         */
1002        public abstract void play();
1003
1004        /**
1005         * Request that the player start playback for a specific {@link Uri}.
1006         *
1007         * @param mediaId The uri of the requested media.
1008         * @param extras Optional extras that can include extra information
1009         *            about the media item to be played.
1010         */
1011        public abstract void playFromMediaId(String mediaId, Bundle extras);
1012
1013        /**
1014         * Request that the player start playback for a specific search query.
1015         * An empty or null query should be treated as a request to play any
1016         * music.
1017         *
1018         * @param query The search query.
1019         * @param extras Optional extras that can include extra information
1020         *            about the query.
1021         */
1022        public abstract void playFromSearch(String query, Bundle extras);
1023
1024        /**
1025         * Request that the player start playback for a specific {@link Uri}.
1026         *
1027         * @param uri  The URI of the requested media.
1028         * @param extras Optional extras that can include extra information about the media item
1029         *               to be played.
1030         */
1031        public abstract void playFromUri(Uri uri, Bundle extras);
1032
1033        /**
1034         * Play an item with a specific id in the play queue. If you specify an
1035         * id that is not in the play queue, the behavior is undefined.
1036         */
1037        public abstract void skipToQueueItem(long id);
1038
1039        /**
1040         * Request that the player pause its playback and stay at its current
1041         * position.
1042         */
1043        public abstract void pause();
1044
1045        /**
1046         * Request that the player stop its playback; it may clear its state in
1047         * whatever way is appropriate.
1048         */
1049        public abstract void stop();
1050
1051        /**
1052         * Move to a new location in the media stream.
1053         *
1054         * @param pos Position to move to, in milliseconds.
1055         */
1056        public abstract void seekTo(long pos);
1057
1058        /**
1059         * Start fast forwarding. If playback is already fast forwarding this
1060         * may increase the rate.
1061         */
1062        public abstract void fastForward();
1063
1064        /**
1065         * Skip to the next item.
1066         */
1067        public abstract void skipToNext();
1068
1069        /**
1070         * Start rewinding. If playback is already rewinding this may increase
1071         * the rate.
1072         */
1073        public abstract void rewind();
1074
1075        /**
1076         * Skip to the previous item.
1077         */
1078        public abstract void skipToPrevious();
1079
1080        /**
1081         * Rate the current content. This will cause the rating to be set for
1082         * the current user. The Rating type must match the type returned by
1083         * {@link #getRatingType()}.
1084         *
1085         * @param rating The rating to set for the current content
1086         */
1087        public abstract void setRating(RatingCompat rating);
1088
1089        /**
1090         * Enable/disable captioning for this session.
1091         *
1092         * @param enabled {@code true} to enable captioning, {@code false} to disable.
1093         */
1094        public abstract void setCaptioningEnabled(boolean enabled);
1095
1096        /**
1097         * Set the repeat mode for this session.
1098         *
1099         * @param repeatMode The repeat mode. Must be one of the followings:
1100         *                   {@link PlaybackStateCompat#REPEAT_MODE_NONE},
1101         *                   {@link PlaybackStateCompat#REPEAT_MODE_ONE},
1102         *                   {@link PlaybackStateCompat#REPEAT_MODE_ALL},
1103         *                   {@link PlaybackStateCompat#REPEAT_MODE_GROUP}
1104         */
1105        public abstract void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode);
1106
1107        /**
1108         * Set the shuffle mode for this session.
1109         *
1110         * @param enabled {@code true} to enable the shuffle mode, {@code false} to disable.
1111         * @deprecated Use {@link #setShuffleMode} instead.
1112         */
1113        @Deprecated
1114        public abstract void setShuffleModeEnabled(boolean enabled);
1115
1116        /**
1117         * Set the shuffle mode for this session.
1118         *
1119         * @param shuffleMode The shuffle mode. Must be one of the followings:
1120         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_NONE},
1121         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_ALL},
1122         *                    {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP}
1123         */
1124        public abstract void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode);
1125
1126        /**
1127         * Send a custom action for the {@link MediaSessionCompat} to perform.
1128         *
1129         * @param customAction The action to perform.
1130         * @param args Optional arguments to supply to the
1131         *            {@link MediaSessionCompat} for this custom action.
1132         */
1133        public abstract void sendCustomAction(PlaybackStateCompat.CustomAction customAction,
1134                Bundle args);
1135
1136        /**
1137         * Send the id and args from a custom action for the
1138         * {@link MediaSessionCompat} to perform.
1139         *
1140         * @see #sendCustomAction(PlaybackStateCompat.CustomAction action,
1141         *      Bundle args)
1142         * @see MediaSessionCompat#ACTION_FLAG_AS_INAPPROPRIATE
1143         * @see MediaSessionCompat#ACTION_SKIP_AD
1144         * @see MediaSessionCompat#ACTION_FOLLOW
1145         * @see MediaSessionCompat#ACTION_UNFOLLOW
1146         * @param action The action identifier of the
1147         *            {@link PlaybackStateCompat.CustomAction} as specified by
1148         *            the {@link MediaSessionCompat}.
1149         * @param args Optional arguments to supply to the
1150         *            {@link MediaSessionCompat} for this custom action.
1151         */
1152        public abstract void sendCustomAction(String action, Bundle args);
1153    }
1154
1155    /**
1156     * Holds information about the way volume is handled for this session.
1157     */
1158    public static final class PlaybackInfo {
1159        /**
1160         * The session uses local playback.
1161         */
1162        public static final int PLAYBACK_TYPE_LOCAL = 1;
1163        /**
1164         * The session uses remote playback.
1165         */
1166        public static final int PLAYBACK_TYPE_REMOTE = 2;
1167
1168        private final int mPlaybackType;
1169        // TODO update audio stream with AudioAttributes support version
1170        private final int mAudioStream;
1171        private final int mVolumeControl;
1172        private final int mMaxVolume;
1173        private final int mCurrentVolume;
1174
1175        PlaybackInfo(int type, int stream, int control, int max, int current) {
1176            mPlaybackType = type;
1177            mAudioStream = stream;
1178            mVolumeControl = control;
1179            mMaxVolume = max;
1180            mCurrentVolume = current;
1181        }
1182
1183        /**
1184         * Get the type of volume handling, either local or remote. One of:
1185         * <ul>
1186         * <li>{@link PlaybackInfo#PLAYBACK_TYPE_LOCAL}</li>
1187         * <li>{@link PlaybackInfo#PLAYBACK_TYPE_REMOTE}</li>
1188         * </ul>
1189         *
1190         * @return The type of volume handling this session is using.
1191         */
1192        public int getPlaybackType() {
1193            return mPlaybackType;
1194        }
1195
1196        /**
1197         * Get the stream this is currently controlling volume on. When the volume
1198         * type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} this value does not
1199         * have meaning and should be ignored.
1200         *
1201         * @return The stream this session is playing on.
1202         */
1203        public int getAudioStream() {
1204            // TODO switch to AudioAttributesCompat when it is added.
1205            return mAudioStream;
1206        }
1207
1208        /**
1209         * Get the type of volume control that can be used. One of:
1210         * <ul>
1211         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li>
1212         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li>
1213         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}</li>
1214         * </ul>
1215         *
1216         * @return The type of volume control that may be used with this
1217         *         session.
1218         */
1219        public int getVolumeControl() {
1220            return mVolumeControl;
1221        }
1222
1223        /**
1224         * Get the maximum volume that may be set for this session.
1225         *
1226         * @return The maximum allowed volume where this session is playing.
1227         */
1228        public int getMaxVolume() {
1229            return mMaxVolume;
1230        }
1231
1232        /**
1233         * Get the current volume for this session.
1234         *
1235         * @return The current volume where this session is playing.
1236         */
1237        public int getCurrentVolume() {
1238            return mCurrentVolume;
1239        }
1240    }
1241
1242    interface MediaControllerImpl {
1243        void registerCallback(Callback callback, Handler handler);
1244
1245        void unregisterCallback(Callback callback);
1246        boolean dispatchMediaButtonEvent(KeyEvent keyEvent);
1247        TransportControls getTransportControls();
1248        PlaybackStateCompat getPlaybackState();
1249        MediaMetadataCompat getMetadata();
1250
1251        List<QueueItem> getQueue();
1252        void addQueueItem(MediaDescriptionCompat description);
1253        void addQueueItem(MediaDescriptionCompat description, int index);
1254        void removeQueueItem(MediaDescriptionCompat description);
1255        CharSequence getQueueTitle();
1256        Bundle getExtras();
1257        int getRatingType();
1258        boolean isCaptioningEnabled();
1259        int getRepeatMode();
1260        boolean isShuffleModeEnabled();
1261        int getShuffleMode();
1262        long getFlags();
1263        PlaybackInfo getPlaybackInfo();
1264        PendingIntent getSessionActivity();
1265
1266        void setVolumeTo(int value, int flags);
1267        void adjustVolume(int direction, int flags);
1268        void sendCommand(String command, Bundle params, ResultReceiver cb);
1269
1270        String getPackageName();
1271        Object getMediaController();
1272    }
1273
1274    static class MediaControllerImplBase implements MediaControllerImpl {
1275        private IMediaSession mBinder;
1276        private TransportControls mTransportControls;
1277
1278        public MediaControllerImplBase(MediaSessionCompat.Token token) {
1279            mBinder = IMediaSession.Stub.asInterface((IBinder) token.getToken());
1280        }
1281
1282        @Override
1283        public void registerCallback(Callback callback, Handler handler) {
1284            if (callback == null) {
1285                throw new IllegalArgumentException("callback may not be null.");
1286            }
1287            try {
1288                mBinder.asBinder().linkToDeath(callback, 0);
1289                mBinder.registerCallbackListener((IMediaControllerCallback) callback.mCallbackObj);
1290                callback.setHandler(handler);
1291                callback.mRegistered = true;
1292            } catch (RemoteException e) {
1293                Log.e(TAG, "Dead object in registerCallback.", e);
1294                callback.onSessionDestroyed();
1295            }
1296        }
1297
1298        @Override
1299        public void unregisterCallback(Callback callback) {
1300            if (callback == null) {
1301                throw new IllegalArgumentException("callback may not be null.");
1302            }
1303            try {
1304                mBinder.unregisterCallbackListener(
1305                        (IMediaControllerCallback) callback.mCallbackObj);
1306                mBinder.asBinder().unlinkToDeath(callback, 0);
1307                callback.mRegistered = false;
1308            } catch (RemoteException e) {
1309                Log.e(TAG, "Dead object in unregisterCallback.", e);
1310            }
1311        }
1312
1313        @Override
1314        public boolean dispatchMediaButtonEvent(KeyEvent event) {
1315            if (event == null) {
1316                throw new IllegalArgumentException("event may not be null.");
1317            }
1318            try {
1319                mBinder.sendMediaButton(event);
1320            } catch (RemoteException e) {
1321                Log.e(TAG, "Dead object in dispatchMediaButtonEvent.", e);
1322            }
1323            return false;
1324        }
1325
1326        @Override
1327        public TransportControls getTransportControls() {
1328            if (mTransportControls == null) {
1329                mTransportControls = new TransportControlsBase(mBinder);
1330            }
1331
1332            return mTransportControls;
1333        }
1334
1335        @Override
1336        public PlaybackStateCompat getPlaybackState() {
1337            try {
1338                return mBinder.getPlaybackState();
1339            } catch (RemoteException e) {
1340                Log.e(TAG, "Dead object in getPlaybackState.", e);
1341            }
1342            return null;
1343        }
1344
1345        @Override
1346        public MediaMetadataCompat getMetadata() {
1347            try {
1348                return mBinder.getMetadata();
1349            } catch (RemoteException e) {
1350                Log.e(TAG, "Dead object in getMetadata.", e);
1351            }
1352            return null;
1353        }
1354
1355        @Override
1356        public List<QueueItem> getQueue() {
1357            try {
1358                return mBinder.getQueue();
1359            } catch (RemoteException e) {
1360                Log.e(TAG, "Dead object in getQueue.", e);
1361            }
1362            return null;
1363        }
1364
1365        @Override
1366        public void addQueueItem(MediaDescriptionCompat description) {
1367            try {
1368                long flags = mBinder.getFlags();
1369                if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
1370                    throw new UnsupportedOperationException(
1371                            "This session doesn't support queue management operations");
1372                }
1373                mBinder.addQueueItem(description);
1374            } catch (RemoteException e) {
1375                Log.e(TAG, "Dead object in addQueueItem.", e);
1376            }
1377        }
1378
1379        @Override
1380        public void addQueueItem(MediaDescriptionCompat description, int index) {
1381            try {
1382                long flags = mBinder.getFlags();
1383                if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
1384                    throw new UnsupportedOperationException(
1385                            "This session doesn't support queue management operations");
1386                }
1387                mBinder.addQueueItemAt(description, index);
1388            } catch (RemoteException e) {
1389                Log.e(TAG, "Dead object in addQueueItemAt.", e);
1390            }
1391        }
1392
1393        @Override
1394        public void removeQueueItem(MediaDescriptionCompat description) {
1395            try {
1396                long flags = mBinder.getFlags();
1397                if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
1398                    throw new UnsupportedOperationException(
1399                            "This session doesn't support queue management operations");
1400                }
1401                mBinder.removeQueueItem(description);
1402            } catch (RemoteException e) {
1403                Log.e(TAG, "Dead object in removeQueueItem.", e);
1404            }
1405        }
1406
1407        @Override
1408        public CharSequence getQueueTitle() {
1409            try {
1410                return mBinder.getQueueTitle();
1411            } catch (RemoteException e) {
1412                Log.e(TAG, "Dead object in getQueueTitle.", e);
1413            }
1414            return null;
1415        }
1416
1417        @Override
1418        public Bundle getExtras() {
1419            try {
1420                return mBinder.getExtras();
1421            } catch (RemoteException e) {
1422                Log.e(TAG, "Dead object in getExtras.", e);
1423            }
1424            return null;
1425        }
1426
1427        @Override
1428        public int getRatingType() {
1429            try {
1430                return mBinder.getRatingType();
1431            } catch (RemoteException e) {
1432                Log.e(TAG, "Dead object in getRatingType.", e);
1433            }
1434            return 0;
1435        }
1436
1437        @Override
1438        public boolean isCaptioningEnabled() {
1439            try {
1440                return mBinder.isCaptioningEnabled();
1441            } catch (RemoteException e) {
1442                Log.e(TAG, "Dead object in isCaptioningEnabled.", e);
1443            }
1444            return false;
1445        }
1446
1447        @Override
1448        public int getRepeatMode() {
1449            try {
1450                return mBinder.getRepeatMode();
1451            } catch (RemoteException e) {
1452                Log.e(TAG, "Dead object in getRepeatMode.", e);
1453            }
1454            return 0;
1455        }
1456
1457        @Override
1458        public boolean isShuffleModeEnabled() {
1459            try {
1460                return mBinder.isShuffleModeEnabledDeprecated();
1461            } catch (RemoteException e) {
1462                Log.e(TAG, "Dead object in isShuffleModeEnabled.", e);
1463            }
1464            return false;
1465        }
1466
1467        @Override
1468        public int getShuffleMode() {
1469            try {
1470                return mBinder.getShuffleMode();
1471            } catch (RemoteException e) {
1472                Log.e(TAG, "Dead object in getShuffleMode.", e);
1473            }
1474            return 0;
1475        }
1476
1477        @Override
1478        public long getFlags() {
1479            try {
1480                return mBinder.getFlags();
1481            } catch (RemoteException e) {
1482                Log.e(TAG, "Dead object in getFlags.", e);
1483            }
1484            return 0;
1485        }
1486
1487        @Override
1488        public PlaybackInfo getPlaybackInfo() {
1489            try {
1490                ParcelableVolumeInfo info = mBinder.getVolumeAttributes();
1491                PlaybackInfo pi = new PlaybackInfo(info.volumeType, info.audioStream,
1492                        info.controlType, info.maxVolume, info.currentVolume);
1493                return pi;
1494            } catch (RemoteException e) {
1495                Log.e(TAG, "Dead object in getPlaybackInfo.", e);
1496            }
1497            return null;
1498        }
1499
1500        @Override
1501        public PendingIntent getSessionActivity() {
1502            try {
1503                return mBinder.getLaunchPendingIntent();
1504            } catch (RemoteException e) {
1505                Log.e(TAG, "Dead object in getSessionActivity.", e);
1506            }
1507            return null;
1508        }
1509
1510        @Override
1511        public void setVolumeTo(int value, int flags) {
1512            try {
1513                mBinder.setVolumeTo(value, flags, null);
1514            } catch (RemoteException e) {
1515                Log.e(TAG, "Dead object in setVolumeTo.", e);
1516            }
1517        }
1518
1519        @Override
1520        public void adjustVolume(int direction, int flags) {
1521            try {
1522                mBinder.adjustVolume(direction, flags, null);
1523            } catch (RemoteException e) {
1524                Log.e(TAG, "Dead object in adjustVolume.", e);
1525            }
1526        }
1527
1528        @Override
1529        public void sendCommand(String command, Bundle params, ResultReceiver cb) {
1530            try {
1531                mBinder.sendCommand(command, params,
1532                        new MediaSessionCompat.ResultReceiverWrapper(cb));
1533            } catch (RemoteException e) {
1534                Log.e(TAG, "Dead object in sendCommand.", e);
1535            }
1536        }
1537
1538        @Override
1539        public String getPackageName() {
1540            try {
1541                return mBinder.getPackageName();
1542            } catch (RemoteException e) {
1543                Log.e(TAG, "Dead object in getPackageName.", e);
1544            }
1545            return null;
1546        }
1547
1548        @Override
1549        public Object getMediaController() {
1550            return null;
1551        }
1552    }
1553
1554    static class TransportControlsBase extends TransportControls {
1555        private IMediaSession mBinder;
1556
1557        public TransportControlsBase(IMediaSession binder) {
1558            mBinder = binder;
1559        }
1560
1561        @Override
1562        public void prepare() {
1563            try {
1564                mBinder.prepare();
1565            } catch (RemoteException e) {
1566                Log.e(TAG, "Dead object in prepare.", e);
1567            }
1568        }
1569
1570        @Override
1571        public void prepareFromMediaId(String mediaId, Bundle extras) {
1572            try {
1573                mBinder.prepareFromMediaId(mediaId, extras);
1574            } catch (RemoteException e) {
1575                Log.e(TAG, "Dead object in prepareFromMediaId.", e);
1576            }
1577        }
1578
1579        @Override
1580        public void prepareFromSearch(String query, Bundle extras) {
1581            try {
1582                mBinder.prepareFromSearch(query, extras);
1583            } catch (RemoteException e) {
1584                Log.e(TAG, "Dead object in prepareFromSearch.", e);
1585            }
1586        }
1587
1588        @Override
1589        public void prepareFromUri(Uri uri, Bundle extras) {
1590            try {
1591                mBinder.prepareFromUri(uri, extras);
1592            } catch (RemoteException e) {
1593                Log.e(TAG, "Dead object in prepareFromUri.", e);
1594            }
1595        }
1596
1597        @Override
1598        public void play() {
1599            try {
1600                mBinder.play();
1601            } catch (RemoteException e) {
1602                Log.e(TAG, "Dead object in play.", e);
1603            }
1604        }
1605
1606        @Override
1607        public void playFromMediaId(String mediaId, Bundle extras) {
1608            try {
1609                mBinder.playFromMediaId(mediaId, extras);
1610            } catch (RemoteException e) {
1611                Log.e(TAG, "Dead object in playFromMediaId.", e);
1612            }
1613        }
1614
1615        @Override
1616        public void playFromSearch(String query, Bundle extras) {
1617            try {
1618                mBinder.playFromSearch(query, extras);
1619            } catch (RemoteException e) {
1620                Log.e(TAG, "Dead object in playFromSearch.", e);
1621            }
1622        }
1623
1624        @Override
1625        public void playFromUri(Uri uri, Bundle extras) {
1626            try {
1627                mBinder.playFromUri(uri, extras);
1628            } catch (RemoteException e) {
1629                Log.e(TAG, "Dead object in playFromUri.", e);
1630            }
1631        }
1632
1633        @Override
1634        public void skipToQueueItem(long id) {
1635            try {
1636                mBinder.skipToQueueItem(id);
1637            } catch (RemoteException e) {
1638                Log.e(TAG, "Dead object in skipToQueueItem.", e);
1639            }
1640        }
1641
1642        @Override
1643        public void pause() {
1644            try {
1645                mBinder.pause();
1646            } catch (RemoteException e) {
1647                Log.e(TAG, "Dead object in pause.", e);
1648            }
1649        }
1650
1651        @Override
1652        public void stop() {
1653            try {
1654                mBinder.stop();
1655            } catch (RemoteException e) {
1656                Log.e(TAG, "Dead object in stop.", e);
1657            }
1658        }
1659
1660        @Override
1661        public void seekTo(long pos) {
1662            try {
1663                mBinder.seekTo(pos);
1664            } catch (RemoteException e) {
1665                Log.e(TAG, "Dead object in seekTo.", e);
1666            }
1667        }
1668
1669        @Override
1670        public void fastForward() {
1671            try {
1672                mBinder.fastForward();
1673            } catch (RemoteException e) {
1674                Log.e(TAG, "Dead object in fastForward.", e);
1675            }
1676        }
1677
1678        @Override
1679        public void skipToNext() {
1680            try {
1681                mBinder.next();
1682            } catch (RemoteException e) {
1683                Log.e(TAG, "Dead object in skipToNext.", e);
1684            }
1685        }
1686
1687        @Override
1688        public void rewind() {
1689            try {
1690                mBinder.rewind();
1691            } catch (RemoteException e) {
1692                Log.e(TAG, "Dead object in rewind.", e);
1693            }
1694        }
1695
1696        @Override
1697        public void skipToPrevious() {
1698            try {
1699                mBinder.previous();
1700            } catch (RemoteException e) {
1701                Log.e(TAG, "Dead object in skipToPrevious.", e);
1702            }
1703        }
1704
1705        @Override
1706        public void setRating(RatingCompat rating) {
1707            try {
1708                mBinder.rate(rating);
1709            } catch (RemoteException e) {
1710                Log.e(TAG, "Dead object in setRating.", e);
1711            }
1712        }
1713
1714        @Override
1715        public void setCaptioningEnabled(boolean enabled) {
1716            try {
1717                mBinder.setCaptioningEnabled(enabled);
1718            } catch (RemoteException e) {
1719                Log.e(TAG, "Dead object in setCaptioningEnabled.", e);
1720            }
1721        }
1722
1723        @Override
1724        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
1725            try {
1726                mBinder.setRepeatMode(repeatMode);
1727            } catch (RemoteException e) {
1728                Log.e(TAG, "Dead object in setRepeatMode.", e);
1729            }
1730        }
1731
1732        @Override
1733        public void setShuffleModeEnabled(boolean enabled) {
1734            try {
1735                mBinder.setShuffleModeEnabledDeprecated(enabled);
1736            } catch (RemoteException e) {
1737                Log.e(TAG, "Dead object in setShuffleModeEnabled.", e);
1738            }
1739        }
1740
1741        @Override
1742        public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
1743            try {
1744                mBinder.setShuffleMode(shuffleMode);
1745            } catch (RemoteException e) {
1746                Log.e(TAG, "Dead object in setShuffleMode.", e);
1747            }
1748        }
1749
1750        @Override
1751        public void sendCustomAction(CustomAction customAction, Bundle args) {
1752            sendCustomAction(customAction.getAction(), args);
1753        }
1754
1755        @Override
1756        public void sendCustomAction(String action, Bundle args) {
1757            validateCustomAction(action, args);
1758            try {
1759                mBinder.sendCustomAction(action, args);
1760            } catch (RemoteException e) {
1761                Log.e(TAG, "Dead object in sendCustomAction.", e);
1762            }
1763        }
1764    }
1765
1766    @RequiresApi(21)
1767    static class MediaControllerImplApi21 implements MediaControllerImpl {
1768        protected final Object mControllerObj;
1769
1770        private final List<Callback> mPendingCallbacks = new ArrayList<>();
1771
1772        // Extra binder is used for applying the framework change of new APIs and bug fixes
1773        // after API 21.
1774        private IMediaSession mExtraBinder;
1775        private HashMap<Callback, ExtraCallback> mCallbackMap = new HashMap<>();
1776
1777        public MediaControllerImplApi21(Context context, MediaSessionCompat session) {
1778            mControllerObj = MediaControllerCompatApi21.fromToken(context,
1779                    session.getSessionToken().getToken());
1780            mExtraBinder = session.getSessionToken().getExtraBinder();
1781            if (mExtraBinder == null) {
1782                requestExtraBinder();
1783            }
1784        }
1785
1786        public MediaControllerImplApi21(Context context, MediaSessionCompat.Token sessionToken)
1787                throws RemoteException {
1788            mControllerObj = MediaControllerCompatApi21.fromToken(context,
1789                    sessionToken.getToken());
1790            if (mControllerObj == null) throw new RemoteException();
1791            mExtraBinder = sessionToken.getExtraBinder();
1792            if (mExtraBinder == null) {
1793                requestExtraBinder();
1794            }
1795        }
1796
1797        @Override
1798        public final void registerCallback(Callback callback, Handler handler) {
1799            MediaControllerCompatApi21.registerCallback(
1800                    mControllerObj, callback.mCallbackObj, handler);
1801            if (mExtraBinder != null) {
1802                callback.setHandler(handler);
1803                ExtraCallback extraCallback = new ExtraCallback(callback);
1804                mCallbackMap.put(callback, extraCallback);
1805                callback.mHasExtraCallback = true;
1806                try {
1807                    mExtraBinder.registerCallbackListener(extraCallback);
1808                } catch (RemoteException e) {
1809                    Log.e(TAG, "Dead object in registerCallback.", e);
1810                }
1811            } else {
1812                callback.setHandler(handler);
1813                synchronized (mPendingCallbacks) {
1814                    mPendingCallbacks.add(callback);
1815                }
1816            }
1817        }
1818
1819        @Override
1820        public final void unregisterCallback(Callback callback) {
1821            MediaControllerCompatApi21.unregisterCallback(mControllerObj, callback.mCallbackObj);
1822            if (mExtraBinder != null) {
1823                try {
1824                    ExtraCallback extraCallback = mCallbackMap.remove(callback);
1825                    if (extraCallback != null) {
1826                        mExtraBinder.unregisterCallbackListener(extraCallback);
1827                    }
1828                } catch (RemoteException e) {
1829                    Log.e(TAG, "Dead object in unregisterCallback.", e);
1830                }
1831            } else {
1832                synchronized (mPendingCallbacks) {
1833                    mPendingCallbacks.remove(callback);
1834                }
1835            }
1836        }
1837
1838        @Override
1839        public boolean dispatchMediaButtonEvent(KeyEvent event) {
1840            return MediaControllerCompatApi21.dispatchMediaButtonEvent(mControllerObj, event);
1841        }
1842
1843        @Override
1844        public TransportControls getTransportControls() {
1845            Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
1846            return controlsObj != null ? new TransportControlsApi21(controlsObj) : null;
1847        }
1848
1849        @Override
1850        public PlaybackStateCompat getPlaybackState() {
1851            if (mExtraBinder != null) {
1852                try {
1853                    return mExtraBinder.getPlaybackState();
1854                } catch (RemoteException e) {
1855                    Log.e(TAG, "Dead object in getPlaybackState.", e);
1856                }
1857            }
1858            Object stateObj = MediaControllerCompatApi21.getPlaybackState(mControllerObj);
1859            return stateObj != null ? PlaybackStateCompat.fromPlaybackState(stateObj) : null;
1860        }
1861
1862        @Override
1863        public MediaMetadataCompat getMetadata() {
1864            Object metadataObj = MediaControllerCompatApi21.getMetadata(mControllerObj);
1865            return metadataObj != null ? MediaMetadataCompat.fromMediaMetadata(metadataObj) : null;
1866        }
1867
1868        @Override
1869        public List<QueueItem> getQueue() {
1870            List<Object> queueObjs = MediaControllerCompatApi21.getQueue(mControllerObj);
1871            return queueObjs != null ? QueueItem.fromQueueItemList(queueObjs) : null;
1872        }
1873
1874        @Override
1875        public void addQueueItem(MediaDescriptionCompat description) {
1876            long flags = getFlags();
1877            if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
1878                throw new UnsupportedOperationException(
1879                        "This session doesn't support queue management operations");
1880            }
1881            Bundle params = new Bundle();
1882            params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
1883            sendCommand(COMMAND_ADD_QUEUE_ITEM, params, null);
1884        }
1885
1886        @Override
1887        public void addQueueItem(MediaDescriptionCompat description, int index) {
1888            long flags = getFlags();
1889            if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
1890                throw new UnsupportedOperationException(
1891                        "This session doesn't support queue management operations");
1892            }
1893            Bundle params = new Bundle();
1894            params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
1895            params.putInt(COMMAND_ARGUMENT_INDEX, index);
1896            sendCommand(COMMAND_ADD_QUEUE_ITEM_AT, params, null);
1897        }
1898
1899        @Override
1900        public void removeQueueItem(MediaDescriptionCompat description) {
1901            long flags = getFlags();
1902            if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) {
1903                throw new UnsupportedOperationException(
1904                        "This session doesn't support queue management operations");
1905            }
1906            Bundle params = new Bundle();
1907            params.putParcelable(COMMAND_ARGUMENT_MEDIA_DESCRIPTION, description);
1908            sendCommand(COMMAND_REMOVE_QUEUE_ITEM, params, null);
1909        }
1910
1911        @Override
1912        public CharSequence getQueueTitle() {
1913            return MediaControllerCompatApi21.getQueueTitle(mControllerObj);
1914        }
1915
1916        @Override
1917        public Bundle getExtras() {
1918            return MediaControllerCompatApi21.getExtras(mControllerObj);
1919        }
1920
1921        @Override
1922        public int getRatingType() {
1923            if (android.os.Build.VERSION.SDK_INT < 22 && mExtraBinder != null) {
1924                try {
1925                    return mExtraBinder.getRatingType();
1926                } catch (RemoteException e) {
1927                    Log.e(TAG, "Dead object in getRatingType.", e);
1928                }
1929            }
1930            return MediaControllerCompatApi21.getRatingType(mControllerObj);
1931        }
1932
1933        @Override
1934        public boolean isCaptioningEnabled() {
1935            if (mExtraBinder != null) {
1936                try {
1937                    return mExtraBinder.isCaptioningEnabled();
1938                } catch (RemoteException e) {
1939                    Log.e(TAG, "Dead object in isCaptioningEnabled.", e);
1940                }
1941            }
1942            return false;
1943        }
1944
1945        @Override
1946        public int getRepeatMode() {
1947            if (mExtraBinder != null) {
1948                try {
1949                    return mExtraBinder.getRepeatMode();
1950                } catch (RemoteException e) {
1951                    Log.e(TAG, "Dead object in getRepeatMode.", e);
1952                }
1953            }
1954            return PlaybackStateCompat.REPEAT_MODE_NONE;
1955        }
1956
1957        @Override
1958        public boolean isShuffleModeEnabled() {
1959            if (mExtraBinder != null) {
1960                try {
1961                    return mExtraBinder.isShuffleModeEnabledDeprecated();
1962                } catch (RemoteException e) {
1963                    Log.e(TAG, "Dead object in isShuffleModeEnabled.", e);
1964                }
1965            }
1966            return false;
1967        }
1968
1969        @Override
1970        public int getShuffleMode() {
1971            if (mExtraBinder != null) {
1972                try {
1973                    return mExtraBinder.getShuffleMode();
1974                } catch (RemoteException e) {
1975                    Log.e(TAG, "Dead object in getShuffleMode.", e);
1976                }
1977            }
1978            return PlaybackStateCompat.SHUFFLE_MODE_NONE;
1979        }
1980
1981        @Override
1982        public long getFlags() {
1983            return MediaControllerCompatApi21.getFlags(mControllerObj);
1984        }
1985
1986        @Override
1987        public PlaybackInfo getPlaybackInfo() {
1988            Object volumeInfoObj = MediaControllerCompatApi21.getPlaybackInfo(mControllerObj);
1989            return volumeInfoObj != null ? new PlaybackInfo(
1990                    MediaControllerCompatApi21.PlaybackInfo.getPlaybackType(volumeInfoObj),
1991                    MediaControllerCompatApi21.PlaybackInfo.getLegacyAudioStream(volumeInfoObj),
1992                    MediaControllerCompatApi21.PlaybackInfo.getVolumeControl(volumeInfoObj),
1993                    MediaControllerCompatApi21.PlaybackInfo.getMaxVolume(volumeInfoObj),
1994                    MediaControllerCompatApi21.PlaybackInfo.getCurrentVolume(volumeInfoObj)) : null;
1995        }
1996
1997        @Override
1998        public PendingIntent getSessionActivity() {
1999            return MediaControllerCompatApi21.getSessionActivity(mControllerObj);
2000        }
2001
2002        @Override
2003        public void setVolumeTo(int value, int flags) {
2004            MediaControllerCompatApi21.setVolumeTo(mControllerObj, value, flags);
2005        }
2006
2007        @Override
2008        public void adjustVolume(int direction, int flags) {
2009            MediaControllerCompatApi21.adjustVolume(mControllerObj, direction, flags);
2010        }
2011
2012        @Override
2013        public void sendCommand(String command, Bundle params, ResultReceiver cb) {
2014            MediaControllerCompatApi21.sendCommand(mControllerObj, command, params, cb);
2015        }
2016
2017        @Override
2018        public String getPackageName() {
2019            return MediaControllerCompatApi21.getPackageName(mControllerObj);
2020        }
2021
2022        @Override
2023        public Object getMediaController() {
2024            return mControllerObj;
2025        }
2026
2027        private void requestExtraBinder() {
2028            sendCommand(COMMAND_GET_EXTRA_BINDER, null,
2029                    new ExtraBinderRequestResultReceiver(this, new Handler()));
2030        }
2031
2032        private void processPendingCallbacks() {
2033            if (mExtraBinder == null) {
2034                return;
2035            }
2036            synchronized (mPendingCallbacks) {
2037                for (Callback callback : mPendingCallbacks) {
2038                    ExtraCallback extraCallback = new ExtraCallback(callback);
2039                    mCallbackMap.put(callback, extraCallback);
2040                    callback.mHasExtraCallback = true;
2041                    try {
2042                        mExtraBinder.registerCallbackListener(extraCallback);
2043                    } catch (RemoteException e) {
2044                        Log.e(TAG, "Dead object in registerCallback.", e);
2045                        break;
2046                    }
2047                }
2048                mPendingCallbacks.clear();
2049            }
2050        }
2051
2052        private static class ExtraBinderRequestResultReceiver extends ResultReceiver {
2053            private WeakReference<MediaControllerImplApi21> mMediaControllerImpl;
2054
2055            public ExtraBinderRequestResultReceiver(MediaControllerImplApi21 mediaControllerImpl,
2056                    Handler handler) {
2057                super(handler);
2058                mMediaControllerImpl = new WeakReference<>(mediaControllerImpl);
2059            }
2060
2061            @Override
2062            protected void onReceiveResult(int resultCode, Bundle resultData) {
2063                MediaControllerImplApi21 mediaControllerImpl = mMediaControllerImpl.get();
2064                if (mediaControllerImpl == null || resultData == null) {
2065                    return;
2066                }
2067                mediaControllerImpl.mExtraBinder = IMediaSession.Stub.asInterface(
2068                        BundleCompat.getBinder(resultData, MediaSessionCompat.EXTRA_BINDER));
2069                mediaControllerImpl.processPendingCallbacks();
2070            }
2071        }
2072
2073        private static class ExtraCallback extends IMediaControllerCallback.Stub {
2074            private Callback mCallback;
2075
2076            ExtraCallback(Callback callback) {
2077                mCallback = callback;
2078            }
2079
2080            @Override
2081            public void onEvent(final String event, final Bundle extras) throws RemoteException {
2082                mCallback.mHandler.post(new Runnable() {
2083                    @Override
2084                    public void run() {
2085                        mCallback.onSessionEvent(event, extras);
2086                    }
2087                });
2088            }
2089
2090            @Override
2091            public void onSessionDestroyed() throws RemoteException {
2092                // Will not be called.
2093                throw new AssertionError();
2094            }
2095
2096            @Override
2097            public void onPlaybackStateChanged(final PlaybackStateCompat state)
2098                    throws RemoteException {
2099                mCallback.mHandler.post(new Runnable() {
2100                    @Override
2101                    public void run() {
2102                        mCallback.onPlaybackStateChanged(state);
2103                    }
2104                });
2105            }
2106
2107            @Override
2108            public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException {
2109                // Will not be called.
2110                throw new AssertionError();
2111            }
2112
2113            @Override
2114            public void onQueueChanged(List<QueueItem> queue) throws RemoteException {
2115                // Will not be called.
2116                throw new AssertionError();
2117            }
2118
2119            @Override
2120            public void onQueueTitleChanged(CharSequence title) throws RemoteException {
2121                // Will not be called.
2122                throw new AssertionError();
2123            }
2124
2125            @Override
2126            public void onCaptioningEnabledChanged(final boolean enabled) throws RemoteException {
2127                mCallback.mHandler.post(new Runnable() {
2128                    @Override
2129                    public void run() {
2130                        mCallback.onCaptioningEnabledChanged(enabled);
2131                    }
2132                });
2133            }
2134
2135            @Override
2136            public void onRepeatModeChanged(final int repeatMode) throws RemoteException {
2137                mCallback.mHandler.post(new Runnable() {
2138                    @Override
2139                    public void run() {
2140                        mCallback.onRepeatModeChanged(repeatMode);
2141                    }
2142                });
2143            }
2144
2145            @Override
2146            public void onShuffleModeChangedDeprecated(final boolean enabled)
2147                    throws RemoteException {
2148                mCallback.mHandler.post(new Runnable() {
2149                    @Override
2150                    public void run() {
2151                        mCallback.onShuffleModeChanged(enabled);
2152                    }
2153                });
2154            }
2155
2156            @Override
2157            public void onShuffleModeChanged(final int shuffleMode) throws RemoteException {
2158                mCallback.mHandler.post(new Runnable() {
2159                    @Override
2160                    public void run() {
2161                        mCallback.onShuffleModeChanged(shuffleMode);
2162                    }
2163                });
2164            }
2165
2166            @Override
2167            public void onExtrasChanged(Bundle extras) throws RemoteException {
2168                // Will not be called.
2169                throw new AssertionError();
2170            }
2171
2172            @Override
2173            public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException {
2174                // Will not be called.
2175                throw new AssertionError();
2176            }
2177        }
2178    }
2179
2180    static class TransportControlsApi21 extends TransportControls {
2181        protected final Object mControlsObj;
2182
2183        public TransportControlsApi21(Object controlsObj) {
2184            mControlsObj = controlsObj;
2185        }
2186
2187        @Override
2188        public void prepare() {
2189            sendCustomAction(MediaSessionCompat.ACTION_PREPARE, null);
2190        }
2191
2192        @Override
2193        public void prepareFromMediaId(String mediaId, Bundle extras) {
2194            Bundle bundle = new Bundle();
2195            bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_MEDIA_ID, mediaId);
2196            bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
2197            sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_MEDIA_ID, bundle);
2198        }
2199
2200        @Override
2201        public void prepareFromSearch(String query, Bundle extras) {
2202            Bundle bundle = new Bundle();
2203            bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_QUERY, query);
2204            bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
2205            sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_SEARCH, bundle);
2206        }
2207
2208        @Override
2209        public void prepareFromUri(Uri uri, Bundle extras) {
2210            Bundle bundle = new Bundle();
2211            bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_URI, uri);
2212            bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
2213            sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_URI, bundle);
2214        }
2215
2216        @Override
2217        public void play() {
2218            MediaControllerCompatApi21.TransportControls.play(mControlsObj);
2219        }
2220
2221        @Override
2222        public void pause() {
2223            MediaControllerCompatApi21.TransportControls.pause(mControlsObj);
2224        }
2225
2226        @Override
2227        public void stop() {
2228            MediaControllerCompatApi21.TransportControls.stop(mControlsObj);
2229        }
2230
2231        @Override
2232        public void seekTo(long pos) {
2233            MediaControllerCompatApi21.TransportControls.seekTo(mControlsObj, pos);
2234        }
2235
2236        @Override
2237        public void fastForward() {
2238            MediaControllerCompatApi21.TransportControls.fastForward(mControlsObj);
2239        }
2240
2241        @Override
2242        public void rewind() {
2243            MediaControllerCompatApi21.TransportControls.rewind(mControlsObj);
2244        }
2245
2246        @Override
2247        public void skipToNext() {
2248            MediaControllerCompatApi21.TransportControls.skipToNext(mControlsObj);
2249        }
2250
2251        @Override
2252        public void skipToPrevious() {
2253            MediaControllerCompatApi21.TransportControls.skipToPrevious(mControlsObj);
2254        }
2255
2256        @Override
2257        public void setRating(RatingCompat rating) {
2258            MediaControllerCompatApi21.TransportControls.setRating(mControlsObj,
2259                    rating != null ? rating.getRating() : null);
2260        }
2261
2262        @Override
2263        public void setCaptioningEnabled(boolean enabled) {
2264            Bundle bundle = new Bundle();
2265            bundle.putBoolean(MediaSessionCompat.ACTION_ARGUMENT_CAPTIONING_ENABLED, enabled);
2266            sendCustomAction(MediaSessionCompat.ACTION_SET_CAPTIONING_ENABLED, bundle);
2267        }
2268
2269        @Override
2270        public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {
2271            Bundle bundle = new Bundle();
2272            bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_REPEAT_MODE, repeatMode);
2273            sendCustomAction(MediaSessionCompat.ACTION_SET_REPEAT_MODE, bundle);
2274        }
2275
2276        @Override
2277        public void setShuffleModeEnabled(boolean enabled) {
2278            Bundle bundle = new Bundle();
2279            bundle.putBoolean(MediaSessionCompat.ACTION_ARGUMENT_SHUFFLE_MODE_ENABLED, enabled);
2280            sendCustomAction(MediaSessionCompat.ACTION_SET_SHUFFLE_MODE_ENABLED, bundle);
2281        }
2282
2283        @Override
2284        public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {
2285            Bundle bundle = new Bundle();
2286            bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_SHUFFLE_MODE, shuffleMode);
2287            sendCustomAction(MediaSessionCompat.ACTION_SET_SHUFFLE_MODE, bundle);
2288        }
2289
2290        @Override
2291        public void playFromMediaId(String mediaId, Bundle extras) {
2292            MediaControllerCompatApi21.TransportControls.playFromMediaId(mControlsObj, mediaId,
2293                    extras);
2294        }
2295
2296        @Override
2297        public void playFromSearch(String query, Bundle extras) {
2298            MediaControllerCompatApi21.TransportControls.playFromSearch(mControlsObj, query,
2299                    extras);
2300        }
2301
2302        @Override
2303        public void playFromUri(Uri uri, Bundle extras) {
2304            if (uri == null || Uri.EMPTY.equals(uri)) {
2305                throw new IllegalArgumentException(
2306                        "You must specify a non-empty Uri for playFromUri.");
2307            }
2308            Bundle bundle = new Bundle();
2309            bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_URI, uri);
2310            bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras);
2311            sendCustomAction(MediaSessionCompat.ACTION_PLAY_FROM_URI, bundle);
2312        }
2313
2314        @Override
2315        public void skipToQueueItem(long id) {
2316            MediaControllerCompatApi21.TransportControls.skipToQueueItem(mControlsObj, id);
2317        }
2318
2319        @Override
2320        public void sendCustomAction(CustomAction customAction, Bundle args) {
2321            validateCustomAction(customAction.getAction(), args);
2322            MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj,
2323                    customAction.getAction(), args);
2324        }
2325
2326        @Override
2327        public void sendCustomAction(String action, Bundle args) {
2328            validateCustomAction(action, args);
2329            MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj, action,
2330                    args);
2331        }
2332    }
2333
2334    @RequiresApi(23)
2335    static class MediaControllerImplApi23 extends MediaControllerImplApi21 {
2336
2337        public MediaControllerImplApi23(Context context, MediaSessionCompat session) {
2338            super(context, session);
2339        }
2340
2341        public MediaControllerImplApi23(Context context, MediaSessionCompat.Token sessionToken)
2342                throws RemoteException {
2343            super(context, sessionToken);
2344        }
2345
2346        @Override
2347        public TransportControls getTransportControls() {
2348            Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
2349            return controlsObj != null ? new TransportControlsApi23(controlsObj) : null;
2350        }
2351    }
2352
2353    @RequiresApi(23)
2354    static class TransportControlsApi23 extends TransportControlsApi21 {
2355
2356        public TransportControlsApi23(Object controlsObj) {
2357            super(controlsObj);
2358        }
2359
2360        @Override
2361        public void playFromUri(Uri uri, Bundle extras) {
2362            MediaControllerCompatApi23.TransportControls.playFromUri(mControlsObj, uri,
2363                    extras);
2364        }
2365    }
2366
2367    @RequiresApi(24)
2368    static class MediaControllerImplApi24 extends MediaControllerImplApi23 {
2369
2370        public MediaControllerImplApi24(Context context, MediaSessionCompat session) {
2371            super(context, session);
2372        }
2373
2374        public MediaControllerImplApi24(Context context, MediaSessionCompat.Token sessionToken)
2375                throws RemoteException {
2376            super(context, sessionToken);
2377        }
2378
2379        @Override
2380        public TransportControls getTransportControls() {
2381            Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
2382            return controlsObj != null ? new TransportControlsApi24(controlsObj) : null;
2383        }
2384    }
2385
2386    @RequiresApi(24)
2387    static class TransportControlsApi24 extends TransportControlsApi23 {
2388
2389        public TransportControlsApi24(Object controlsObj) {
2390            super(controlsObj);
2391        }
2392
2393        @Override
2394        public void prepare() {
2395            MediaControllerCompatApi24.TransportControls.prepare(mControlsObj);
2396        }
2397
2398        @Override
2399        public void prepareFromMediaId(String mediaId, Bundle extras) {
2400            MediaControllerCompatApi24.TransportControls.prepareFromMediaId(
2401                    mControlsObj, mediaId, extras);
2402        }
2403
2404        @Override
2405        public void prepareFromSearch(String query, Bundle extras) {
2406            MediaControllerCompatApi24.TransportControls.prepareFromSearch(
2407                    mControlsObj, query, extras);
2408        }
2409
2410        @Override
2411        public void prepareFromUri(Uri uri, Bundle extras) {
2412            MediaControllerCompatApi24.TransportControls.prepareFromUri(mControlsObj, uri, extras);
2413        }
2414    }
2415}
2416