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.PendingIntent;
20import android.content.Context;
21import android.media.AudioManager;
22import android.net.Uri;
23import android.os.Bundle;
24import android.os.Handler;
25import android.os.IBinder;
26import android.os.Looper;
27import android.os.Message;
28import android.os.RemoteException;
29import android.os.ResultReceiver;
30import android.support.v4.media.MediaMetadataCompat;
31import android.support.v4.media.RatingCompat;
32import android.support.v4.media.VolumeProviderCompat;
33import android.support.v4.media.session.MediaSessionCompat.QueueItem;
34import android.support.v4.media.session.PlaybackStateCompat.CustomAction;
35import android.text.TextUtils;
36import android.util.Log;
37import android.view.KeyEvent;
38
39import java.util.ArrayList;
40import java.util.List;
41
42/**
43 * Allows an app to interact with an ongoing media session. Media buttons and
44 * other commands can be sent to the session. A callback may be registered to
45 * receive updates from the session, such as metadata and play state changes.
46 * <p>
47 * A MediaController can be created if you have a {@link MediaSessionCompat.Token}
48 * from the session owner.
49 * <p>
50 * MediaController objects are thread-safe.
51 * <p>
52 * This is a helper for accessing features in {@link android.media.session.MediaSession}
53 * introduced after API level 4 in a backwards compatible fashion.
54 */
55public final class MediaControllerCompat {
56    private static final String TAG = "MediaControllerCompat";
57
58    private final MediaControllerImpl mImpl;
59    private final MediaSessionCompat.Token mToken;
60
61    /**
62     * Creates a media controller from a session.
63     *
64     * @param session The session to be controlled.
65     */
66    public MediaControllerCompat(Context context, MediaSessionCompat session) {
67        if (session == null) {
68            throw new IllegalArgumentException("session must not be null");
69        }
70        mToken = session.getSessionToken();
71
72        if (android.os.Build.VERSION.SDK_INT >= 21) {
73            mImpl = new MediaControllerImplApi21(context, session);
74        } else {
75            mImpl = new MediaControllerImplBase(mToken);
76        }
77    }
78
79    /**
80     * Creates a media controller from a session token which may have
81     * been obtained from another process.
82     *
83     * @param sessionToken The token of the session to be controlled.
84     * @throws RemoteException if the session is not accessible.
85     */
86    public MediaControllerCompat(Context context, MediaSessionCompat.Token sessionToken)
87            throws RemoteException {
88        if (sessionToken == null) {
89            throw new IllegalArgumentException("sessionToken must not be null");
90        }
91        mToken = sessionToken;
92
93        if (android.os.Build.VERSION.SDK_INT >= 21) {
94            mImpl = new MediaControllerImplApi21(context, sessionToken);
95        } else {
96            mImpl = new MediaControllerImplBase(mToken);
97        }
98    }
99
100    /**
101     * Get a {@link TransportControls} instance for this session.
102     *
103     * @return A controls instance
104     */
105    public TransportControls getTransportControls() {
106        return mImpl.getTransportControls();
107    }
108
109    /**
110     * Send the specified media button event to the session. Only media keys can
111     * be sent by this method, other keys will be ignored.
112     *
113     * @param keyEvent The media button event to dispatch.
114     * @return true if the event was sent to the session, false otherwise.
115     */
116    public boolean dispatchMediaButtonEvent(KeyEvent keyEvent) {
117        if (keyEvent == null) {
118            throw new IllegalArgumentException("KeyEvent may not be null");
119        }
120        return mImpl.dispatchMediaButtonEvent(keyEvent);
121    }
122
123    /**
124     * Get the current playback state for this session.
125     *
126     * @return The current PlaybackState or null
127     */
128    public PlaybackStateCompat getPlaybackState() {
129        return mImpl.getPlaybackState();
130    }
131
132    /**
133     * Get the current metadata for this session.
134     *
135     * @return The current MediaMetadata or null.
136     */
137    public MediaMetadataCompat getMetadata() {
138        return mImpl.getMetadata();
139    }
140
141    /**
142     * Get the current play queue for this session if one is set. If you only
143     * care about the current item {@link #getMetadata()} should be used.
144     *
145     * @return The current play queue or null.
146     */
147    public List<MediaSessionCompat.QueueItem> getQueue() {
148        return mImpl.getQueue();
149    }
150
151    /**
152     * Get the queue title for this session.
153     */
154    public CharSequence getQueueTitle() {
155        return mImpl.getQueueTitle();
156    }
157
158    /**
159     * Get the extras for this session.
160     */
161    public Bundle getExtras() {
162        return mImpl.getExtras();
163    }
164
165    /**
166     * Get the rating type supported by the session. One of:
167     * <ul>
168     * <li>{@link RatingCompat#RATING_NONE}</li>
169     * <li>{@link RatingCompat#RATING_HEART}</li>
170     * <li>{@link RatingCompat#RATING_THUMB_UP_DOWN}</li>
171     * <li>{@link RatingCompat#RATING_3_STARS}</li>
172     * <li>{@link RatingCompat#RATING_4_STARS}</li>
173     * <li>{@link RatingCompat#RATING_5_STARS}</li>
174     * <li>{@link RatingCompat#RATING_PERCENTAGE}</li>
175     * </ul>
176     *
177     * @return The supported rating type
178     */
179    public int getRatingType() {
180        return mImpl.getRatingType();
181    }
182
183    /**
184     * Get the flags for this session. Flags are defined in
185     * {@link MediaSessionCompat}.
186     *
187     * @return The current set of flags for the session.
188     */
189    public long getFlags() {
190        return mImpl.getFlags();
191    }
192
193    /**
194     * Get the current playback info for this session.
195     *
196     * @return The current playback info or null.
197     */
198    public PlaybackInfo getPlaybackInfo() {
199        return mImpl.getPlaybackInfo();
200    }
201
202    /**
203     * Get an intent for launching UI associated with this session if one
204     * exists.
205     *
206     * @return A {@link PendingIntent} to launch UI or null.
207     */
208    public PendingIntent getSessionActivity() {
209        return mImpl.getSessionActivity();
210    }
211
212    /**
213     * Get the token for the session this controller is connected to.
214     *
215     * @return The session's token.
216     */
217    public MediaSessionCompat.Token getSessionToken() {
218        return mToken;
219    }
220
221    /**
222     * Set the volume of the output this session is playing on. The command will
223     * be ignored if it does not support
224     * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in
225     * {@link AudioManager} may be used to affect the handling.
226     *
227     * @see #getPlaybackInfo()
228     * @param value The value to set it to, between 0 and the reported max.
229     * @param flags Flags from {@link AudioManager} to include with the volume
230     *            request.
231     */
232    public void setVolumeTo(int value, int flags) {
233        mImpl.setVolumeTo(value, flags);
234    }
235
236    /**
237     * Adjust the volume of the output this session is playing on. The direction
238     * must be one of {@link AudioManager#ADJUST_LOWER},
239     * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
240     * The command will be ignored if the session does not support
241     * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or
242     * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in
243     * {@link AudioManager} may be used to affect the handling.
244     *
245     * @see #getPlaybackInfo()
246     * @param direction The direction to adjust the volume in.
247     * @param flags Any flags to pass with the command.
248     */
249    public void adjustVolume(int direction, int flags) {
250        mImpl.adjustVolume(direction, flags);
251    }
252
253    /**
254     * Adds a callback to receive updates from the Session. Updates will be
255     * posted on the caller's thread.
256     *
257     * @param callback The callback object, must not be null.
258     */
259    public void registerCallback(Callback callback) {
260        registerCallback(callback, null);
261    }
262
263    /**
264     * Adds a callback to receive updates from the session. Updates will be
265     * posted on the specified handler's thread.
266     *
267     * @param callback The callback object, must not be null.
268     * @param handler The handler to post updates on. If null the callers thread
269     *            will be used.
270     */
271    public void registerCallback(Callback callback, Handler handler) {
272        if (callback == null) {
273            throw new IllegalArgumentException("callback cannot be null");
274        }
275        if (handler == null) {
276            handler = new Handler();
277        }
278        mImpl.registerCallback(callback, handler);
279    }
280
281    /**
282     * Stop receiving updates on the specified callback. If an update has
283     * already been posted you may still receive it after calling this method.
284     *
285     * @param callback The callback to remove
286     */
287    public void unregisterCallback(Callback callback) {
288        if (callback == null) {
289            throw new IllegalArgumentException("callback cannot be null");
290        }
291        mImpl.unregisterCallback(callback);
292    }
293
294    /**
295     * Sends a generic command to the session. It is up to the session creator
296     * to decide what commands and parameters they will support. As such,
297     * commands should only be sent to sessions that the controller owns.
298     *
299     * @param command The command to send
300     * @param params Any parameters to include with the command
301     * @param cb The callback to receive the result on
302     */
303    public void sendCommand(String command, Bundle params, ResultReceiver cb) {
304        if (TextUtils.isEmpty(command)) {
305            throw new IllegalArgumentException("command cannot be null or empty");
306        }
307        mImpl.sendCommand(command, params, cb);
308    }
309
310    /**
311     * Get the session owner's package name.
312     *
313     * @return The package name of of the session owner.
314     */
315    public String getPackageName() {
316        return mImpl.getPackageName();
317    }
318
319    /**
320     * Gets the underlying framework
321     * {@link android.media.session.MediaController} object.
322     * <p>
323     * This method is only supported on API 21+.
324     * </p>
325     *
326     * @return The underlying {@link android.media.session.MediaController}
327     *         object, or null if none.
328     */
329    public Object getMediaController() {
330        return mImpl.getMediaController();
331    }
332
333    /**
334     * Callback for receiving updates on from the session. A Callback can be
335     * registered using {@link #registerCallback}
336     */
337    public static abstract class Callback implements IBinder.DeathRecipient {
338        private final Object mCallbackObj;
339        private MessageHandler mHandler;
340
341        private boolean mRegistered = false;
342
343        public Callback() {
344            if (android.os.Build.VERSION.SDK_INT >= 21) {
345                mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21());
346            } else {
347                mCallbackObj = new StubCompat();
348            }
349        }
350
351        /**
352         * Override to handle the session being destroyed. The session is no
353         * longer valid after this call and calls to it will be ignored.
354         */
355        public void onSessionDestroyed() {
356        }
357
358        /**
359         * Override to handle custom events sent by the session owner without a
360         * specified interface. Controllers should only handle these for
361         * sessions they own.
362         *
363         * @param event The event from the session.
364         * @param extras Optional parameters for the event.
365         */
366        public void onSessionEvent(String event, Bundle extras) {
367        }
368
369        /**
370         * Override to handle changes in playback state.
371         *
372         * @param state The new playback state of the session
373         */
374        public void onPlaybackStateChanged(PlaybackStateCompat state) {
375        }
376
377        /**
378         * Override to handle changes to the current metadata.
379         *
380         * @param metadata The current metadata for the session or null if none.
381         * @see MediaMetadata
382         */
383        public void onMetadataChanged(MediaMetadataCompat metadata) {
384        }
385
386        /**
387         * Override to handle changes to items in the queue.
388         *
389         * @see MediaSessionCompat.QueueItem
390         * @param queue A list of items in the current play queue. It should
391         *            include the currently playing item as well as previous and
392         *            upcoming items if applicable.
393         */
394        public void onQueueChanged(List<MediaSessionCompat.QueueItem> queue) {
395        }
396
397        /**
398         * Override to handle changes to the queue title.
399         *
400         * @param title The title that should be displayed along with the play
401         *            queue such as "Now Playing". May be null if there is no
402         *            such title.
403         */
404        public void onQueueTitleChanged(CharSequence title) {
405        }
406
407        /**
408         * Override to handle chagnes to the {@link MediaSessionCompat} extras.
409         *
410         * @param extras The extras that can include other information
411         *            associated with the {@link MediaSessionCompat}.
412         */
413        public void onExtrasChanged(Bundle extras) {
414        }
415
416        /**
417         * Override to handle changes to the audio info.
418         *
419         * @param info The current audio info for this session.
420         */
421        public void onAudioInfoChanged(PlaybackInfo info) {
422        }
423
424        @Override
425        public void binderDied() {
426            onSessionDestroyed();
427        }
428
429        /**
430         * Set the handler to use for pre 21 callbacks.
431         */
432        private void setHandler(Handler handler) {
433            mHandler = new MessageHandler(handler.getLooper());
434        }
435
436        private class StubApi21 implements MediaControllerCompatApi21.Callback {
437            @Override
438            public void onSessionDestroyed() {
439                Callback.this.onSessionDestroyed();
440            }
441
442            @Override
443            public void onSessionEvent(String event, Bundle extras) {
444                Callback.this.onSessionEvent(event, extras);
445            }
446
447            @Override
448            public void onPlaybackStateChanged(Object stateObj) {
449                Callback.this.onPlaybackStateChanged(
450                        PlaybackStateCompat.fromPlaybackState(stateObj));
451            }
452
453            @Override
454            public void onMetadataChanged(Object metadataObj) {
455                Callback.this.onMetadataChanged(
456                        MediaMetadataCompat.fromMediaMetadata(metadataObj));
457            }
458        }
459
460        private class StubCompat extends IMediaControllerCallback.Stub {
461
462            @Override
463            public void onEvent(String event, Bundle extras) throws RemoteException {
464                mHandler.post(MessageHandler.MSG_EVENT, event, extras);
465            }
466
467            @Override
468            public void onSessionDestroyed() throws RemoteException {
469                mHandler.post(MessageHandler.MSG_DESTROYED, null, null);
470            }
471
472            @Override
473            public void onPlaybackStateChanged(PlaybackStateCompat state) throws RemoteException {
474                mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state, null);
475            }
476
477            @Override
478            public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException {
479                mHandler.post(MessageHandler.MSG_UPDATE_METADATA, metadata, null);
480            }
481
482            @Override
483            public void onQueueChanged(List<QueueItem> queue) throws RemoteException {
484                mHandler.post(MessageHandler.MSG_UPDATE_QUEUE, queue, null);
485            }
486
487            @Override
488            public void onQueueTitleChanged(CharSequence title) throws RemoteException {
489                mHandler.post(MessageHandler.MSG_UPDATE_QUEUE_TITLE, title, null);
490            }
491
492            @Override
493            public void onExtrasChanged(Bundle extras) throws RemoteException {
494                mHandler.post(MessageHandler.MSG_UPDATE_EXTRAS, extras, null);
495            }
496
497            @Override
498            public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException {
499                PlaybackInfo pi = null;
500                if (info != null) {
501                    pi = new PlaybackInfo(info.volumeType, info.audioStream, info.controlType,
502                            info.maxVolume, info.currentVolume);
503                }
504                mHandler.post(MessageHandler.MSG_UPDATE_VOLUME, pi, null);
505            }
506        }
507
508        private class MessageHandler extends Handler {
509            private static final int MSG_EVENT = 1;
510            private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
511            private static final int MSG_UPDATE_METADATA = 3;
512            private static final int MSG_UPDATE_VOLUME = 4;
513            private static final int MSG_UPDATE_QUEUE = 5;
514            private static final int MSG_UPDATE_QUEUE_TITLE = 6;
515            private static final int MSG_UPDATE_EXTRAS = 7;
516            private static final int MSG_DESTROYED = 8;
517
518            public MessageHandler(Looper looper) {
519                super(looper);
520            }
521
522            @Override
523            public void handleMessage(Message msg) {
524                if (!mRegistered) {
525                    return;
526                }
527                switch (msg.what) {
528                    case MSG_EVENT:
529                        onSessionEvent((String) msg.obj, msg.getData());
530                        break;
531                    case MSG_UPDATE_PLAYBACK_STATE:
532                        onPlaybackStateChanged((PlaybackStateCompat) msg.obj);
533                        break;
534                    case MSG_UPDATE_METADATA:
535                        onMetadataChanged((MediaMetadataCompat) msg.obj);
536                        break;
537                    case MSG_UPDATE_QUEUE:
538                        onQueueChanged((List<MediaSessionCompat.QueueItem>) msg.obj);
539                        break;
540                    case MSG_UPDATE_QUEUE_TITLE:
541                        onQueueTitleChanged((CharSequence) msg.obj);
542                        break;
543                    case MSG_UPDATE_EXTRAS:
544                        onExtrasChanged((Bundle) msg.obj);
545                        break;
546                    case MSG_UPDATE_VOLUME:
547                        onAudioInfoChanged((PlaybackInfo) msg.obj);
548                        break;
549                    case MSG_DESTROYED:
550                        onSessionDestroyed();
551                        break;
552                }
553            }
554
555            public void post(int what, Object obj, Bundle data) {
556                obtainMessage(what, obj).sendToTarget();
557            }
558        }
559    }
560
561    /**
562     * Interface for controlling media playback on a session. This allows an app
563     * to send media transport commands to the session.
564     */
565    public static abstract class TransportControls {
566        TransportControls() {
567        }
568
569        /**
570         * Request that the player start its playback at its current position.
571         */
572        public abstract void play();
573
574        /**
575         * Request that the player start playback for a specific {@link Uri}.
576         *
577         * @param mediaId The uri of the requested media.
578         * @param extras Optional extras that can include extra information
579         *            about the media item to be played.
580         */
581        public abstract void playFromMediaId(String mediaId, Bundle extras);
582
583        /**
584         * Request that the player start playback for a specific search query.
585         * An empty or null query should be treated as a request to play any
586         * music.
587         *
588         * @param query The search query.
589         * @param extras Optional extras that can include extra information
590         *            about the query.
591         */
592        public abstract void playFromSearch(String query, Bundle extras);
593
594        /**
595         * Play an item with a specific id in the play queue. If you specify an
596         * id that is not in the play queue, the behavior is undefined.
597         */
598        public abstract void skipToQueueItem(long id);
599
600        /**
601         * Request that the player pause its playback and stay at its current
602         * position.
603         */
604        public abstract void pause();
605
606        /**
607         * Request that the player stop its playback; it may clear its state in
608         * whatever way is appropriate.
609         */
610        public abstract void stop();
611
612        /**
613         * Move to a new location in the media stream.
614         *
615         * @param pos Position to move to, in milliseconds.
616         */
617        public abstract void seekTo(long pos);
618
619        /**
620         * Start fast forwarding. If playback is already fast forwarding this
621         * may increase the rate.
622         */
623        public abstract void fastForward();
624
625        /**
626         * Skip to the next item.
627         */
628        public abstract void skipToNext();
629
630        /**
631         * Start rewinding. If playback is already rewinding this may increase
632         * the rate.
633         */
634        public abstract void rewind();
635
636        /**
637         * Skip to the previous item.
638         */
639        public abstract void skipToPrevious();
640
641        /**
642         * Rate the current content. This will cause the rating to be set for
643         * the current user. The Rating type must match the type returned by
644         * {@link #getRatingType()}.
645         *
646         * @param rating The rating to set for the current content
647         */
648        public abstract void setRating(RatingCompat rating);
649
650        /**
651         * Send a custom action for the {@link MediaSessionCompat} to perform.
652         *
653         * @param customAction The action to perform.
654         * @param args Optional arguments to supply to the
655         *            {@link MediaSessionCompat} for this custom action.
656         */
657        public abstract void sendCustomAction(PlaybackStateCompat.CustomAction customAction,
658                Bundle args);
659
660        /**
661         * Send the id and args from a custom action for the
662         * {@link MediaSessionCompat} to perform.
663         *
664         * @see #sendCustomAction(PlaybackStateCompat.CustomAction action,
665         *      Bundle args)
666         * @param action The action identifier of the
667         *            {@link PlaybackStateCompat.CustomAction} as specified by
668         *            the {@link MediaSessionCompat}.
669         * @param args Optional arguments to supply to the
670         *            {@link MediaSessionCompat} for this custom action.
671         */
672        public abstract void sendCustomAction(String action, Bundle args);
673    }
674
675    /**
676     * Holds information about the way volume is handled for this session.
677     */
678    public static final class PlaybackInfo {
679        /**
680         * The session uses local playback.
681         */
682        public static final int PLAYBACK_TYPE_LOCAL = 1;
683        /**
684         * The session uses remote playback.
685         */
686        public static final int PLAYBACK_TYPE_REMOTE = 2;
687
688        private final int mPlaybackType;
689        // TODO update audio stream with AudioAttributes support version
690        private final int mAudioStream;
691        private final int mVolumeControl;
692        private final int mMaxVolume;
693        private final int mCurrentVolume;
694
695        PlaybackInfo(int type, int stream, int control, int max, int current) {
696            mPlaybackType = type;
697            mAudioStream = stream;
698            mVolumeControl = control;
699            mMaxVolume = max;
700            mCurrentVolume = current;
701        }
702
703        /**
704         * Get the type of volume handling, either local or remote. One of:
705         * <ul>
706         * <li>{@link PlaybackInfo#PLAYBACK_TYPE_LOCAL}</li>
707         * <li>{@link PlaybackInfo#PLAYBACK_TYPE_REMOTE}</li>
708         * </ul>
709         *
710         * @return The type of volume handling this session is using.
711         */
712        public int getPlaybackType() {
713            return mPlaybackType;
714        }
715
716        /**
717         * Get the stream this is currently controlling volume on. When the volume
718         * type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} this value does not
719         * have meaning and should be ignored.
720         *
721         * @return The stream this session is playing on.
722         */
723        public int getAudioStream() {
724            // TODO switch to AudioAttributesCompat when it is added.
725            return mAudioStream;
726        }
727
728        /**
729         * Get the type of volume control that can be used. One of:
730         * <ul>
731         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li>
732         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li>
733         * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}</li>
734         * </ul>
735         *
736         * @return The type of volume control that may be used with this
737         *         session.
738         */
739        public int getVolumeControl() {
740            return mVolumeControl;
741        }
742
743        /**
744         * Get the maximum volume that may be set for this session.
745         *
746         * @return The maximum allowed volume where this session is playing.
747         */
748        public int getMaxVolume() {
749            return mMaxVolume;
750        }
751
752        /**
753         * Get the current volume for this session.
754         *
755         * @return The current volume where this session is playing.
756         */
757        public int getCurrentVolume() {
758            return mCurrentVolume;
759        }
760    }
761
762    interface MediaControllerImpl {
763        void registerCallback(Callback callback, Handler handler);
764
765        void unregisterCallback(Callback callback);
766        boolean dispatchMediaButtonEvent(KeyEvent keyEvent);
767        TransportControls getTransportControls();
768        PlaybackStateCompat getPlaybackState();
769        MediaMetadataCompat getMetadata();
770
771        List<MediaSessionCompat.QueueItem> getQueue();
772        CharSequence getQueueTitle();
773        Bundle getExtras();
774        int getRatingType();
775        long getFlags();
776        PlaybackInfo getPlaybackInfo();
777        PendingIntent getSessionActivity();
778
779        void setVolumeTo(int value, int flags);
780        void adjustVolume(int direction, int flags);
781        void sendCommand(String command, Bundle params, ResultReceiver cb);
782
783        String getPackageName();
784        Object getMediaController();
785    }
786
787    static class MediaControllerImplBase implements MediaControllerImpl {
788        private MediaSessionCompat.Token mToken;
789        private IMediaSession mBinder;
790        private TransportControls mTransportControls;
791
792        public MediaControllerImplBase(MediaSessionCompat.Token token) {
793            mToken = token;
794            mBinder = IMediaSession.Stub.asInterface((IBinder) token.getToken());
795        }
796
797        @Override
798        public void registerCallback(Callback callback, Handler handler) {
799            if (callback == null) {
800                throw new IllegalArgumentException("callback may not be null.");
801            }
802            try {
803                mBinder.asBinder().linkToDeath(callback, 0);
804                mBinder.registerCallbackListener((IMediaControllerCallback) callback.mCallbackObj);
805                callback.setHandler(handler);
806                callback.mRegistered = true;
807            } catch (RemoteException e) {
808                Log.e(TAG, "Dead object in registerCallback. " + e);
809                callback.onSessionDestroyed();
810            }
811        }
812
813        @Override
814        public void unregisterCallback(Callback callback) {
815            if (callback == null) {
816                throw new IllegalArgumentException("callback may not be null.");
817            }
818            try {
819                mBinder.unregisterCallbackListener(
820                        (IMediaControllerCallback) callback.mCallbackObj);
821                mBinder.asBinder().unlinkToDeath(callback, 0);
822                callback.mRegistered = false;
823            } catch (RemoteException e) {
824                Log.e(TAG, "Dead object in unregisterCallback. " + e);
825            }
826        }
827
828        @Override
829        public boolean dispatchMediaButtonEvent(KeyEvent event) {
830            if (event == null) {
831                throw new IllegalArgumentException("event may not be null.");
832            }
833            try {
834                mBinder.sendMediaButton(event);
835            } catch (RemoteException e) {
836                Log.e(TAG, "Dead object in dispatchMediaButtonEvent. " + e);
837            }
838            return false;
839        }
840
841        @Override
842        public TransportControls getTransportControls() {
843            if (mTransportControls == null) {
844                mTransportControls = new TransportControlsBase(mBinder);
845            }
846
847            return mTransportControls;
848        }
849
850        @Override
851        public PlaybackStateCompat getPlaybackState() {
852            try {
853                return mBinder.getPlaybackState();
854            } catch (RemoteException e) {
855                Log.e(TAG, "Dead object in getPlaybackState. " + e);
856            }
857            return null;
858        }
859
860        @Override
861        public MediaMetadataCompat getMetadata() {
862            try {
863                return mBinder.getMetadata();
864            } catch (RemoteException e) {
865                Log.e(TAG, "Dead object in getMetadata. " + e);
866            }
867            return null;
868        }
869
870        @Override
871        public List<MediaSessionCompat.QueueItem> getQueue() {
872            try {
873                return mBinder.getQueue();
874            } catch (RemoteException e) {
875                Log.e(TAG, "Dead object in getQueue. " + e);
876            }
877            return null;
878        }
879
880        @Override
881        public CharSequence getQueueTitle() {
882            try {
883                return mBinder.getQueueTitle();
884            } catch (RemoteException e) {
885                Log.e(TAG, "Dead object in getQueueTitle. " + e);
886            }
887            return null;
888        }
889
890        @Override
891        public Bundle getExtras() {
892            try {
893                return mBinder.getExtras();
894            } catch (RemoteException e) {
895                Log.e(TAG, "Dead object in getExtras. " + e);
896            }
897            return null;
898        }
899
900        @Override
901        public int getRatingType() {
902            try {
903                return mBinder.getRatingType();
904            } catch (RemoteException e) {
905                Log.e(TAG, "Dead object in getRatingType. " + e);
906            }
907            return 0;
908        }
909
910        @Override
911        public long getFlags() {
912            try {
913                return mBinder.getFlags();
914            } catch (RemoteException e) {
915                Log.e(TAG, "Dead object in getFlags. " + e);
916            }
917            return 0;
918        }
919
920        @Override
921        public PlaybackInfo getPlaybackInfo() {
922            try {
923                ParcelableVolumeInfo info = mBinder.getVolumeAttributes();
924                PlaybackInfo pi = new PlaybackInfo(info.volumeType, info.audioStream,
925                        info.controlType, info.maxVolume, info.currentVolume);
926                return pi;
927            } catch (RemoteException e) {
928                Log.e(TAG, "Dead object in getPlaybackInfo. " + e);
929            }
930            return null;
931        }
932
933        @Override
934        public PendingIntent getSessionActivity() {
935            try {
936                return mBinder.getLaunchPendingIntent();
937            } catch (RemoteException e) {
938                Log.e(TAG, "Dead object in getSessionActivity. " + e);
939            }
940            return null;
941        }
942
943        @Override
944        public void setVolumeTo(int value, int flags) {
945            try {
946                mBinder.setVolumeTo(value, flags, null);
947            } catch (RemoteException e) {
948                Log.e(TAG, "Dead object in setVolumeTo. " + e);
949            }
950        }
951
952        @Override
953        public void adjustVolume(int direction, int flags) {
954            try {
955                mBinder.adjustVolume(direction, flags, null);
956            } catch (RemoteException e) {
957                Log.e(TAG, "Dead object in adjustVolume. " + e);
958            }
959        }
960
961        @Override
962        public void sendCommand(String command, Bundle params, ResultReceiver cb) {
963            try {
964                mBinder.sendCommand(command, params,
965                        new MediaSessionCompat.ResultReceiverWrapper(cb));
966            } catch (RemoteException e) {
967                Log.e(TAG, "Dead object in sendCommand. " + e);
968            }
969        }
970
971        @Override
972        public String getPackageName() {
973            try {
974                return mBinder.getPackageName();
975            } catch (RemoteException e) {
976                Log.e(TAG, "Dead object in getPackageName. " + e);
977            }
978            return null;
979        }
980
981        @Override
982        public Object getMediaController() {
983            return null;
984        }
985    }
986
987    static class TransportControlsBase extends TransportControls {
988        private IMediaSession mBinder;
989
990        public TransportControlsBase(IMediaSession binder) {
991            mBinder = binder;
992        }
993
994        @Override
995        public void play() {
996            try {
997                mBinder.play();
998            } catch (RemoteException e) {
999                Log.e(TAG, "Dead object in play. " + e);
1000            }
1001        }
1002
1003        @Override
1004        public void playFromMediaId(String mediaId, Bundle extras) {
1005            try {
1006                mBinder.playFromMediaId(mediaId, extras);
1007            } catch (RemoteException e) {
1008                Log.e(TAG, "Dead object in playFromMediaId. " + e);
1009            }
1010        }
1011
1012        @Override
1013        public void playFromSearch(String query, Bundle extras) {
1014            try {
1015                mBinder.playFromSearch(query, extras);
1016            } catch (RemoteException e) {
1017                Log.e(TAG, "Dead object in playFromSearch. " + e);
1018            }
1019        }
1020
1021        @Override
1022        public void skipToQueueItem(long id) {
1023            try {
1024                mBinder.skipToQueueItem(id);
1025            } catch (RemoteException e) {
1026                Log.e(TAG, "Dead object in skipToQueueItem. " + e);
1027            }
1028        }
1029
1030        @Override
1031        public void pause() {
1032            try {
1033                mBinder.pause();
1034            } catch (RemoteException e) {
1035                Log.e(TAG, "Dead object in pause. " + e);
1036            }
1037        }
1038
1039        @Override
1040        public void stop() {
1041            try {
1042                mBinder.stop();
1043            } catch (RemoteException e) {
1044                Log.e(TAG, "Dead object in stop. " + e);
1045            }
1046        }
1047
1048        @Override
1049        public void seekTo(long pos) {
1050            try {
1051                mBinder.seekTo(pos);
1052            } catch (RemoteException e) {
1053                Log.e(TAG, "Dead object in seekTo. " + e);
1054            }
1055        }
1056
1057        @Override
1058        public void fastForward() {
1059            try {
1060                mBinder.fastForward();
1061            } catch (RemoteException e) {
1062                Log.e(TAG, "Dead object in fastForward. " + e);
1063            }
1064        }
1065
1066        @Override
1067        public void skipToNext() {
1068            try {
1069                mBinder.next();
1070            } catch (RemoteException e) {
1071                Log.e(TAG, "Dead object in skipToNext. " + e);
1072            }
1073        }
1074
1075        @Override
1076        public void rewind() {
1077            try {
1078                mBinder.rewind();
1079            } catch (RemoteException e) {
1080                Log.e(TAG, "Dead object in rewind. " + e);
1081            }
1082        }
1083
1084        @Override
1085        public void skipToPrevious() {
1086            try {
1087                mBinder.previous();
1088            } catch (RemoteException e) {
1089                Log.e(TAG, "Dead object in skipToPrevious. " + e);
1090            }
1091        }
1092
1093        @Override
1094        public void setRating(RatingCompat rating) {
1095            try {
1096                mBinder.rate(rating);
1097            } catch (RemoteException e) {
1098                Log.e(TAG, "Dead object in setRating. " + e);
1099            }
1100        }
1101
1102        @Override
1103        public void sendCustomAction(CustomAction customAction, Bundle args) {
1104            sendCustomAction(customAction.getAction(), args);
1105        }
1106
1107        @Override
1108        public void sendCustomAction(String action, Bundle args) {
1109            try {
1110                mBinder.sendCustomAction(action, args);
1111            } catch (RemoteException e) {
1112                Log.e(TAG, "Dead object in sendCustomAction. " + e);
1113            }
1114        }
1115    }
1116
1117    static class MediaControllerImplApi21 implements MediaControllerImpl {
1118        private final Object mControllerObj;
1119
1120        public MediaControllerImplApi21(Context context, MediaSessionCompat session) {
1121            mControllerObj = MediaControllerCompatApi21.fromToken(context,
1122                    session.getSessionToken().getToken());
1123        }
1124
1125        public MediaControllerImplApi21(Context context, MediaSessionCompat.Token sessionToken)
1126                throws RemoteException {
1127            mControllerObj = MediaControllerCompatApi21.fromToken(context,
1128                    sessionToken.getToken());
1129            if (mControllerObj == null) throw new RemoteException();
1130        }
1131
1132        @Override
1133        public void registerCallback(Callback callback, Handler handler) {
1134            MediaControllerCompatApi21.registerCallback(mControllerObj, callback.mCallbackObj, handler);
1135        }
1136
1137        @Override
1138        public void unregisterCallback(Callback callback) {
1139            MediaControllerCompatApi21.unregisterCallback(mControllerObj, callback.mCallbackObj);
1140        }
1141
1142        @Override
1143        public boolean dispatchMediaButtonEvent(KeyEvent event) {
1144            return MediaControllerCompatApi21.dispatchMediaButtonEvent(mControllerObj, event);
1145        }
1146
1147        @Override
1148        public TransportControls getTransportControls() {
1149            Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
1150            return controlsObj != null ? new TransportControlsApi21(controlsObj) : null;
1151        }
1152
1153        @Override
1154        public PlaybackStateCompat getPlaybackState() {
1155            Object stateObj = MediaControllerCompatApi21.getPlaybackState(mControllerObj);
1156            return stateObj != null ? PlaybackStateCompat.fromPlaybackState(stateObj) : null;
1157        }
1158
1159        @Override
1160        public MediaMetadataCompat getMetadata() {
1161            Object metadataObj = MediaControllerCompatApi21.getMetadata(mControllerObj);
1162            return metadataObj != null ? MediaMetadataCompat.fromMediaMetadata(metadataObj) : null;
1163        }
1164
1165        @Override
1166        public List<MediaSessionCompat.QueueItem> getQueue() {
1167            List<Object> queueObjs = MediaControllerCompatApi21.getQueue(mControllerObj);
1168            if (queueObjs == null) {
1169                return null;
1170            }
1171            List<MediaSessionCompat.QueueItem> queue =
1172                    new ArrayList<MediaSessionCompat.QueueItem>();
1173            for (Object item : queueObjs) {
1174                queue.add(MediaSessionCompat.QueueItem.obtain(item));
1175            }
1176            return queue;
1177        }
1178
1179        @Override
1180        public CharSequence getQueueTitle() {
1181            return MediaControllerCompatApi21.getQueueTitle(mControllerObj);
1182        }
1183
1184        @Override
1185        public Bundle getExtras() {
1186            return MediaControllerCompatApi21.getExtras(mControllerObj);
1187        }
1188
1189        @Override
1190        public int getRatingType() {
1191            return MediaControllerCompatApi21.getRatingType(mControllerObj);
1192        }
1193
1194        @Override
1195        public long getFlags() {
1196            return MediaControllerCompatApi21.getFlags(mControllerObj);
1197        }
1198
1199        @Override
1200        public PlaybackInfo getPlaybackInfo() {
1201            Object volumeInfoObj = MediaControllerCompatApi21.getPlaybackInfo(mControllerObj);
1202            return volumeInfoObj != null ? new PlaybackInfo(
1203                    MediaControllerCompatApi21.PlaybackInfo.getPlaybackType(volumeInfoObj),
1204                    MediaControllerCompatApi21.PlaybackInfo.getLegacyAudioStream(volumeInfoObj),
1205                    MediaControllerCompatApi21.PlaybackInfo.getVolumeControl(volumeInfoObj),
1206                    MediaControllerCompatApi21.PlaybackInfo.getMaxVolume(volumeInfoObj),
1207                    MediaControllerCompatApi21.PlaybackInfo.getCurrentVolume(volumeInfoObj)) : null;
1208        }
1209
1210        @Override
1211        public PendingIntent getSessionActivity() {
1212            return MediaControllerCompatApi21.getSessionActivity(mControllerObj);
1213        }
1214
1215        @Override
1216        public void setVolumeTo(int value, int flags) {
1217            MediaControllerCompatApi21.setVolumeTo(mControllerObj, value, flags);
1218        }
1219
1220        @Override
1221        public void adjustVolume(int direction, int flags) {
1222            MediaControllerCompatApi21.adjustVolume(mControllerObj, direction, flags);
1223        }
1224
1225        @Override
1226        public void sendCommand(String command, Bundle params, ResultReceiver cb) {
1227            MediaControllerCompatApi21.sendCommand(mControllerObj, command, params, cb);
1228        }
1229
1230        @Override
1231        public String getPackageName() {
1232            return MediaControllerCompatApi21.getPackageName(mControllerObj);
1233        }
1234
1235        @Override
1236        public Object getMediaController() {
1237            return mControllerObj;
1238        }
1239    }
1240
1241    static class TransportControlsApi21 extends TransportControls {
1242        private final Object mControlsObj;
1243
1244        public TransportControlsApi21(Object controlsObj) {
1245            mControlsObj = controlsObj;
1246        }
1247
1248        @Override
1249        public void play() {
1250            MediaControllerCompatApi21.TransportControls.play(mControlsObj);
1251        }
1252
1253        @Override
1254        public void pause() {
1255            MediaControllerCompatApi21.TransportControls.pause(mControlsObj);
1256        }
1257
1258        @Override
1259        public void stop() {
1260            MediaControllerCompatApi21.TransportControls.stop(mControlsObj);
1261        }
1262
1263        @Override
1264        public void seekTo(long pos) {
1265            MediaControllerCompatApi21.TransportControls.seekTo(mControlsObj, pos);
1266        }
1267
1268        @Override
1269        public void fastForward() {
1270            MediaControllerCompatApi21.TransportControls.fastForward(mControlsObj);
1271        }
1272
1273        @Override
1274        public void rewind() {
1275            MediaControllerCompatApi21.TransportControls.rewind(mControlsObj);
1276        }
1277
1278        @Override
1279        public void skipToNext() {
1280            MediaControllerCompatApi21.TransportControls.skipToNext(mControlsObj);
1281        }
1282
1283        @Override
1284        public void skipToPrevious() {
1285            MediaControllerCompatApi21.TransportControls.skipToPrevious(mControlsObj);
1286        }
1287
1288        @Override
1289        public void setRating(RatingCompat rating) {
1290            MediaControllerCompatApi21.TransportControls.setRating(mControlsObj,
1291                    rating != null ? rating.getRating() : null);
1292        }
1293
1294        @Override
1295        public void playFromMediaId(String mediaId, Bundle extras) {
1296            MediaControllerCompatApi21.TransportControls.playFromMediaId(mControlsObj, mediaId,
1297                    extras);
1298        }
1299
1300        @Override
1301        public void playFromSearch(String query, Bundle extras) {
1302            MediaControllerCompatApi21.TransportControls.playFromSearch(mControlsObj, query,
1303                    extras);
1304        }
1305
1306        @Override
1307        public void skipToQueueItem(long id) {
1308            MediaControllerCompatApi21.TransportControls.skipToQueueItem(mControlsObj, id);
1309        }
1310
1311        @Override
1312        public void sendCustomAction(CustomAction customAction, Bundle args) {
1313            MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj,
1314                    customAction.getAction(), args);
1315        }
1316
1317        @Override
1318        public void sendCustomAction(String action, Bundle args) {
1319            MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj, action,
1320                    args);
1321        }
1322    }
1323}
1324