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