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