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