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