MediaController.java revision 060b4e1f17db00c8cfb25f4d81726c37d580f723
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.media.session;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.app.PendingIntent;
22import android.content.Context;
23import android.content.pm.ParceledListSlice;
24import android.media.AudioAttributes;
25import android.media.AudioManager;
26import android.media.MediaMetadata;
27import android.media.Rating;
28import android.media.VolumeProvider;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.Handler;
32import android.os.Looper;
33import android.os.Message;
34import android.os.RemoteException;
35import android.os.ResultReceiver;
36import android.text.TextUtils;
37import android.util.Log;
38import android.view.KeyEvent;
39
40import java.lang.ref.WeakReference;
41import java.util.ArrayList;
42import java.util.List;
43
44/**
45 * Allows an app to interact with an ongoing media session. Media buttons and
46 * other commands can be sent to the session. A callback may be registered to
47 * receive updates from the session, such as metadata and play state changes.
48 * <p>
49 * A MediaController can be created through {@link MediaSessionManager} if you
50 * hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or are an
51 * enabled notification listener or by getting a {@link MediaSession.Token}
52 * directly from the session owner.
53 * <p>
54 * MediaController objects are thread-safe.
55 */
56public final class MediaController {
57    private static final String TAG = "MediaController";
58
59    private static final int MSG_EVENT = 1;
60    private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
61    private static final int MSG_UPDATE_METADATA = 3;
62    private static final int MSG_UPDATE_VOLUME = 4;
63    private static final int MSG_UPDATE_QUEUE = 5;
64    private static final int MSG_UPDATE_QUEUE_TITLE = 6;
65    private static final int MSG_UPDATE_EXTRAS = 7;
66    private static final int MSG_DESTROYED = 8;
67
68    private final ISessionController mSessionBinder;
69
70    private final MediaSession.Token mToken;
71    private final Context mContext;
72    private final CallbackStub mCbStub = new CallbackStub(this);
73    private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>();
74    private final Object mLock = new Object();
75
76    private boolean mCbRegistered = false;
77    private String mPackageName;
78    private String mTag;
79
80    private final TransportControls mTransportControls;
81
82    /**
83     * Call for creating a MediaController directly from a binder. Should only
84     * be used by framework code.
85     *
86     * @hide
87     */
88    public MediaController(Context context, ISessionController sessionBinder) {
89        if (sessionBinder == null) {
90            throw new IllegalArgumentException("Session token cannot be null");
91        }
92        if (context == null) {
93            throw new IllegalArgumentException("Context cannot be null");
94        }
95        mSessionBinder = sessionBinder;
96        mTransportControls = new TransportControls();
97        mToken = new MediaSession.Token(sessionBinder);
98        mContext = context;
99    }
100
101    /**
102     * Create a new MediaController from a session's token.
103     *
104     * @param context The caller's context.
105     * @param token The token for the session.
106     */
107    public MediaController(@NonNull Context context, @NonNull MediaSession.Token token) {
108        this(context, token.getBinder());
109    }
110
111    /**
112     * Get a {@link TransportControls} instance to send transport actions to
113     * the associated session.
114     *
115     * @return A transport controls instance.
116     */
117    public @NonNull TransportControls getTransportControls() {
118        return mTransportControls;
119    }
120
121    /**
122     * Send the specified media button event to the session. Only media keys can
123     * be sent by this method, other keys will be ignored.
124     *
125     * @param keyEvent The media button event to dispatch.
126     * @return true if the event was sent to the session, false otherwise.
127     */
128    public boolean dispatchMediaButtonEvent(@NonNull KeyEvent keyEvent) {
129        if (keyEvent == null) {
130            throw new IllegalArgumentException("KeyEvent may not be null");
131        }
132        if (!KeyEvent.isMediaKey(keyEvent.getKeyCode())) {
133            return false;
134        }
135        try {
136            return mSessionBinder.sendMediaButton(keyEvent);
137        } catch (RemoteException e) {
138            // System is dead. =(
139        }
140        return false;
141    }
142
143    /**
144     * Get the current playback state for this session.
145     *
146     * @return The current PlaybackState or null
147     */
148    public @Nullable PlaybackState getPlaybackState() {
149        try {
150            return mSessionBinder.getPlaybackState();
151        } catch (RemoteException e) {
152            Log.wtf(TAG, "Error calling getPlaybackState.", e);
153            return null;
154        }
155    }
156
157    /**
158     * Get the current metadata for this session.
159     *
160     * @return The current MediaMetadata or null.
161     */
162    public @Nullable MediaMetadata getMetadata() {
163        try {
164            return mSessionBinder.getMetadata();
165        } catch (RemoteException e) {
166            Log.wtf(TAG, "Error calling getMetadata.", e);
167            return null;
168        }
169    }
170
171    /**
172     * Get the current play queue for this session if one is set. If you only
173     * care about the current item {@link #getMetadata()} should be used.
174     *
175     * @return The current play queue or null.
176     */
177    public @Nullable List<MediaSession.QueueItem> getQueue() {
178        try {
179            ParceledListSlice queue = mSessionBinder.getQueue();
180            if (queue != null) {
181                return queue.getList();
182            }
183        } catch (RemoteException e) {
184            Log.wtf(TAG, "Error calling getQueue.", e);
185        }
186        return null;
187    }
188
189    /**
190     * Get the queue title for this session.
191     */
192    public @Nullable CharSequence getQueueTitle() {
193        try {
194            return mSessionBinder.getQueueTitle();
195        } catch (RemoteException e) {
196            Log.wtf(TAG, "Error calling getQueueTitle", e);
197        }
198        return null;
199    }
200
201    /**
202     * Get the extras for this session.
203     */
204    public @Nullable Bundle getExtras() {
205        try {
206            return mSessionBinder.getExtras();
207        } catch (RemoteException e) {
208            Log.wtf(TAG, "Error calling getExtras", e);
209        }
210        return null;
211    }
212
213    /**
214     * Get the rating type supported by the session. One of:
215     * <ul>
216     * <li>{@link Rating#RATING_NONE}</li>
217     * <li>{@link Rating#RATING_HEART}</li>
218     * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li>
219     * <li>{@link Rating#RATING_3_STARS}</li>
220     * <li>{@link Rating#RATING_4_STARS}</li>
221     * <li>{@link Rating#RATING_5_STARS}</li>
222     * <li>{@link Rating#RATING_PERCENTAGE}</li>
223     * </ul>
224     *
225     * @return The supported rating type
226     */
227    public int getRatingType() {
228        try {
229            return mSessionBinder.getRatingType();
230        } catch (RemoteException e) {
231            Log.wtf(TAG, "Error calling getRatingType.", e);
232            return Rating.RATING_NONE;
233        }
234    }
235
236    /**
237     * Get the flags for this session. Flags are defined in {@link MediaSession}.
238     *
239     * @return The current set of flags for the session.
240     */
241    public @MediaSession.SessionFlags long getFlags() {
242        try {
243            return mSessionBinder.getFlags();
244        } catch (RemoteException e) {
245            Log.wtf(TAG, "Error calling getFlags.", e);
246        }
247        return 0;
248    }
249
250    /**
251     * Get the current playback info for this session.
252     *
253     * @return The current playback info or null.
254     */
255    public @Nullable PlaybackInfo getPlaybackInfo() {
256        try {
257            ParcelableVolumeInfo result = mSessionBinder.getVolumeAttributes();
258            return new PlaybackInfo(result.volumeType, result.audioAttrs, result.controlType,
259                    result.maxVolume, result.currentVolume);
260
261        } catch (RemoteException e) {
262            Log.wtf(TAG, "Error calling getAudioInfo.", e);
263        }
264        return null;
265    }
266
267    /**
268     * Get an intent for launching UI associated with this session if one
269     * exists.
270     *
271     * @return A {@link PendingIntent} to launch UI or null.
272     */
273    public @Nullable PendingIntent getSessionActivity() {
274        try {
275            return mSessionBinder.getLaunchPendingIntent();
276        } catch (RemoteException e) {
277            Log.wtf(TAG, "Error calling getPendingIntent.", e);
278        }
279        return null;
280    }
281
282    /**
283     * Get the token for the session this is connected to.
284     *
285     * @return The token for the connected session.
286     */
287    public @NonNull MediaSession.Token getSessionToken() {
288        return mToken;
289    }
290
291    /**
292     * Set the volume of the output this session is playing on. The command will
293     * be ignored if it does not support
294     * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in
295     * {@link AudioManager} may be used to affect the handling.
296     *
297     * @see #getPlaybackInfo()
298     * @param value The value to set it to, between 0 and the reported max.
299     * @param flags Flags from {@link AudioManager} to include with the volume
300     *            request.
301     */
302    public void setVolumeTo(int value, int flags) {
303        try {
304            mSessionBinder.setVolumeTo(value, flags, mContext.getPackageName());
305        } catch (RemoteException e) {
306            Log.wtf(TAG, "Error calling setVolumeTo.", e);
307        }
308    }
309
310    /**
311     * Adjust the volume of the output this session is playing on. The direction
312     * must be one of {@link AudioManager#ADJUST_LOWER},
313     * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
314     * The command will be ignored if the session does not support
315     * {@link VolumeProvider#VOLUME_CONTROL_RELATIVE} or
316     * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in
317     * {@link AudioManager} may be used to affect the handling.
318     *
319     * @see #getPlaybackInfo()
320     * @param direction The direction to adjust the volume in.
321     * @param flags Any flags to pass with the command.
322     */
323    public void adjustVolume(int direction, int flags) {
324        try {
325            mSessionBinder.adjustVolume(direction, flags, mContext.getPackageName());
326        } catch (RemoteException e) {
327            Log.wtf(TAG, "Error calling adjustVolumeBy.", e);
328        }
329    }
330
331    /**
332     * Registers a callback to receive updates from the Session. Updates will be
333     * posted on the caller's thread.
334     *
335     * @param callback The callback object, must not be null.
336     */
337    public void registerCallback(@NonNull Callback callback) {
338        registerCallback(callback, null);
339    }
340
341    /**
342     * Registers a callback to receive updates from the session. Updates will be
343     * posted on the specified handler's thread.
344     *
345     * @param callback The callback object, must not be null.
346     * @param handler The handler to post updates on. If null the callers thread
347     *            will be used.
348     */
349    public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
350        if (callback == null) {
351            throw new IllegalArgumentException("callback must not be null");
352        }
353        if (handler == null) {
354            handler = new Handler();
355        }
356        synchronized (mLock) {
357            addCallbackLocked(callback, handler);
358        }
359    }
360
361    /**
362     * Unregisters the specified callback. If an update has already been posted
363     * you may still receive it after calling this method.
364     *
365     * @param callback The callback to remove.
366     */
367    public void unregisterCallback(@NonNull Callback callback) {
368        if (callback == null) {
369            throw new IllegalArgumentException("callback must not be null");
370        }
371        synchronized (mLock) {
372            removeCallbackLocked(callback);
373        }
374    }
375
376    /**
377     * Sends a generic command to the session. It is up to the session creator
378     * to decide what commands and parameters they will support. As such,
379     * commands should only be sent to sessions that the controller owns.
380     *
381     * @param command The command to send
382     * @param args Any parameters to include with the command
383     * @param cb The callback to receive the result on
384     */
385    public void sendCommand(@NonNull String command, @Nullable Bundle args,
386            @Nullable ResultReceiver cb) {
387        if (TextUtils.isEmpty(command)) {
388            throw new IllegalArgumentException("command cannot be null or empty");
389        }
390        try {
391            mSessionBinder.sendCommand(command, args, cb);
392        } catch (RemoteException e) {
393            Log.d(TAG, "Dead object in sendCommand.", e);
394        }
395    }
396
397    /**
398     * Get the session owner's package name.
399     *
400     * @return The package name of of the session owner.
401     */
402    public String getPackageName() {
403        if (mPackageName == null) {
404            try {
405                mPackageName = mSessionBinder.getPackageName();
406            } catch (RemoteException e) {
407                Log.d(TAG, "Dead object in getPackageName.", e);
408            }
409        }
410        return mPackageName;
411    }
412
413    /**
414     * Get the session's tag for debugging purposes.
415     *
416     * @return The session's tag.
417     * @hide
418     */
419    public String getTag() {
420        if (mTag == null) {
421            try {
422                mTag = mSessionBinder.getTag();
423            } catch (RemoteException e) {
424                Log.d(TAG, "Dead object in getTag.", e);
425            }
426        }
427        return mTag;
428    }
429
430    /*
431     * @hide
432     */
433    ISessionController getSessionBinder() {
434        return mSessionBinder;
435    }
436
437    /**
438     * @hide
439     */
440    public boolean controlsSameSession(MediaController other) {
441        if (other == null) return false;
442        return mSessionBinder.asBinder() == other.getSessionBinder().asBinder();
443    }
444
445    private void addCallbackLocked(Callback cb, Handler handler) {
446        if (getHandlerForCallbackLocked(cb) != null) {
447            Log.w(TAG, "Callback is already added, ignoring");
448            return;
449        }
450        MessageHandler holder = new MessageHandler(handler.getLooper(), cb);
451        mCallbacks.add(holder);
452        holder.mRegistered = true;
453
454        if (!mCbRegistered) {
455            try {
456                mSessionBinder.registerCallbackListener(mCbStub);
457                mCbRegistered = true;
458            } catch (RemoteException e) {
459                Log.e(TAG, "Dead object in registerCallback", e);
460            }
461        }
462    }
463
464    private boolean removeCallbackLocked(Callback cb) {
465        boolean success = false;
466        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
467            MessageHandler handler = mCallbacks.get(i);
468            if (cb == handler.mCallback) {
469                mCallbacks.remove(i);
470                success = true;
471                handler.mRegistered = false;
472            }
473        }
474        if (mCbRegistered && mCallbacks.size() == 0) {
475            try {
476                mSessionBinder.unregisterCallbackListener(mCbStub);
477            } catch (RemoteException e) {
478                Log.e(TAG, "Dead object in removeCallbackLocked");
479            }
480            mCbRegistered = false;
481        }
482        return success;
483    }
484
485    private MessageHandler getHandlerForCallbackLocked(Callback cb) {
486        if (cb == null) {
487            throw new IllegalArgumentException("Callback cannot be null");
488        }
489        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
490            MessageHandler handler = mCallbacks.get(i);
491            if (cb == handler.mCallback) {
492                return handler;
493            }
494        }
495        return null;
496    }
497
498    private final void postMessage(int what, Object obj, Bundle data) {
499        synchronized (mLock) {
500            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
501                mCallbacks.get(i).post(what, obj, data);
502            }
503        }
504    }
505
506    /**
507     * Callback for receiving updates from the session. A Callback can be
508     * registered using {@link #registerCallback}.
509     */
510    public static abstract class Callback {
511        /**
512         * Override to handle the session being destroyed. The session is no
513         * longer valid after this call and calls to it will be ignored.
514         */
515        public void onSessionDestroyed() {
516        }
517
518        /**
519         * Override to handle custom events sent by the session owner without a
520         * specified interface. Controllers should only handle these for
521         * sessions they own.
522         *
523         * @param event The event from the session.
524         * @param extras Optional parameters for the event, may be null.
525         */
526        public void onSessionEvent(@NonNull String event, @Nullable Bundle extras) {
527        }
528
529        /**
530         * Override to handle changes in playback state.
531         *
532         * @param state The new playback state of the session
533         */
534        public void onPlaybackStateChanged(@NonNull PlaybackState state) {
535        }
536
537        /**
538         * Override to handle changes to the current metadata.
539         *
540         * @param metadata The current metadata for the session or null if none.
541         * @see MediaMetadata
542         */
543        public void onMetadataChanged(@Nullable MediaMetadata metadata) {
544        }
545
546        /**
547         * Override to handle changes to items in the queue.
548         *
549         * @param queue A list of items in the current play queue. It should
550         *            include the currently playing item as well as previous and
551         *            upcoming items if applicable.
552         * @see MediaSession.QueueItem
553         */
554        public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) {
555        }
556
557        /**
558         * Override to handle changes to the queue title.
559         *
560         * @param title The title that should be displayed along with the play queue such as
561         *              "Now Playing". May be null if there is no such title.
562         */
563        public void onQueueTitleChanged(@Nullable CharSequence title) {
564        }
565
566        /**
567         * Override to handle changes to the {@link MediaSession} extras.
568         *
569         * @param extras The extras that can include other information associated with the
570         *               {@link MediaSession}.
571         */
572        public void onExtrasChanged(@Nullable Bundle extras) {
573        }
574
575        /**
576         * Override to handle changes to the audio info.
577         *
578         * @param info The current audio info for this session.
579         */
580        public void onAudioInfoChanged(PlaybackInfo info) {
581        }
582    }
583
584    /**
585     * Interface for controlling media playback on a session. This allows an app
586     * to send media transport commands to the session.
587     */
588    public final class TransportControls {
589        private static final String TAG = "TransportController";
590
591        private TransportControls() {
592        }
593
594        /**
595         * Request that the player start its playback at its current position.
596         */
597        public void play() {
598            try {
599                mSessionBinder.play();
600            } catch (RemoteException e) {
601                Log.wtf(TAG, "Error calling play.", e);
602            }
603        }
604
605        /**
606         * Request that the player start playback for a specific media id.
607         *
608         * @param mediaId The id of the requested media.
609         * @param extras Optional extras that can include extra information about the media item
610         *               to be played.
611         */
612        public void playFromMediaId(String mediaId, Bundle extras) {
613            if (TextUtils.isEmpty(mediaId)) {
614                throw new IllegalArgumentException(
615                        "You must specify a non-empty String for playFromMediaId.");
616            }
617            try {
618                mSessionBinder.playFromMediaId(mediaId, extras);
619            } catch (RemoteException e) {
620                Log.wtf(TAG, "Error calling play(" + mediaId + ").", e);
621            }
622        }
623
624        /**
625         * Request that the player start playback for a specific search query.
626         * An empty or null query should be treated as a request to play any
627         * music.
628         *
629         * @param query The search query.
630         * @param extras Optional extras that can include extra information
631         *            about the query.
632         */
633        public void playFromSearch(String query, Bundle extras) {
634            if (query == null) {
635                // This is to remain compatible with
636                // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
637                query = "";
638            }
639            try {
640                mSessionBinder.playFromSearch(query, extras);
641            } catch (RemoteException e) {
642                Log.wtf(TAG, "Error calling play(" + query + ").", e);
643            }
644        }
645
646        /**
647         * Request that the player start playback for a specific {@link Uri}.
648         *
649         * @param uri  The URI of the requested media.
650         * @param extras Optional extras that can include extra information about the media item
651         *               to be played.
652         */
653        public void playFromUri(Uri uri, Bundle extras) {
654            if (uri == null || Uri.EMPTY.equals(uri)) {
655                throw new IllegalArgumentException(
656                        "You must specify a non-empty Uri for playFromUri.");
657            }
658            try {
659                mSessionBinder.playFromUri(uri, extras);
660            } catch (RemoteException e) {
661                Log.wtf(TAG, "Error calling play(" + uri + ").", e);
662            }
663        }
664
665        /**
666         * Play an item with a specific id in the play queue. If you specify an
667         * id that is not in the play queue, the behavior is undefined.
668         */
669        public void skipToQueueItem(long id) {
670            try {
671                mSessionBinder.skipToQueueItem(id);
672            } catch (RemoteException e) {
673                Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e);
674            }
675        }
676
677        /**
678         * Request that the player pause its playback and stay at its current
679         * position.
680         */
681        public void pause() {
682            try {
683                mSessionBinder.pause();
684            } catch (RemoteException e) {
685                Log.wtf(TAG, "Error calling pause.", e);
686            }
687        }
688
689        /**
690         * Request that the player stop its playback; it may clear its state in
691         * whatever way is appropriate.
692         */
693        public void stop() {
694            try {
695                mSessionBinder.stop();
696            } catch (RemoteException e) {
697                Log.wtf(TAG, "Error calling stop.", e);
698            }
699        }
700
701        /**
702         * Move to a new location in the media stream.
703         *
704         * @param pos Position to move to, in milliseconds.
705         */
706        public void seekTo(long pos) {
707            try {
708                mSessionBinder.seekTo(pos);
709            } catch (RemoteException e) {
710                Log.wtf(TAG, "Error calling seekTo.", e);
711            }
712        }
713
714        /**
715         * Start fast forwarding. If playback is already fast forwarding this
716         * may increase the rate.
717         */
718        public void fastForward() {
719            try {
720                mSessionBinder.fastForward();
721            } catch (RemoteException e) {
722                Log.wtf(TAG, "Error calling fastForward.", e);
723            }
724        }
725
726        /**
727         * Skip to the next item.
728         */
729        public void skipToNext() {
730            try {
731                mSessionBinder.next();
732            } catch (RemoteException e) {
733                Log.wtf(TAG, "Error calling next.", e);
734            }
735        }
736
737        /**
738         * Start rewinding. If playback is already rewinding this may increase
739         * the rate.
740         */
741        public void rewind() {
742            try {
743                mSessionBinder.rewind();
744            } catch (RemoteException e) {
745                Log.wtf(TAG, "Error calling rewind.", e);
746            }
747        }
748
749        /**
750         * Skip to the previous item.
751         */
752        public void skipToPrevious() {
753            try {
754                mSessionBinder.previous();
755            } catch (RemoteException e) {
756                Log.wtf(TAG, "Error calling previous.", e);
757            }
758        }
759
760        /**
761         * Rate the current content. This will cause the rating to be set for
762         * the current user. The Rating type must match the type returned by
763         * {@link #getRatingType()}.
764         *
765         * @param rating The rating to set for the current content
766         */
767        public void setRating(Rating rating) {
768            try {
769                mSessionBinder.rate(rating);
770            } catch (RemoteException e) {
771                Log.wtf(TAG, "Error calling rate.", e);
772            }
773        }
774
775        /**
776         * Send a custom action back for the {@link MediaSession} to perform.
777         *
778         * @param customAction The action to perform.
779         * @param args Optional arguments to supply to the {@link MediaSession} for this
780         *             custom action.
781         */
782        public void sendCustomAction(@NonNull PlaybackState.CustomAction customAction,
783                    @Nullable Bundle args) {
784            if (customAction == null) {
785                throw new IllegalArgumentException("CustomAction cannot be null.");
786            }
787            sendCustomAction(customAction.getAction(), args);
788        }
789
790        /**
791         * Send the id and args from a custom action back for the {@link MediaSession} to perform.
792         *
793         * @see #sendCustomAction(PlaybackState.CustomAction action, Bundle args)
794         * @param action The action identifier of the {@link PlaybackState.CustomAction} as
795         *               specified by the {@link MediaSession}.
796         * @param args Optional arguments to supply to the {@link MediaSession} for this
797         *             custom action.
798         */
799        public void sendCustomAction(@NonNull String action, @Nullable Bundle args) {
800            if (TextUtils.isEmpty(action)) {
801                throw new IllegalArgumentException("CustomAction cannot be null.");
802            }
803            try {
804                mSessionBinder.sendCustomAction(action, args);
805            } catch (RemoteException e) {
806                Log.d(TAG, "Dead object in sendCustomAction.", e);
807            }
808        }
809    }
810
811    /**
812     * Holds information about the current playback and how audio is handled for
813     * this session.
814     */
815    public static final class PlaybackInfo {
816        /**
817         * The session uses remote playback.
818         */
819        public static final int PLAYBACK_TYPE_REMOTE = 2;
820        /**
821         * The session uses local playback.
822         */
823        public static final int PLAYBACK_TYPE_LOCAL = 1;
824
825        private final int mVolumeType;
826        private final int mVolumeControl;
827        private final int mMaxVolume;
828        private final int mCurrentVolume;
829        private final AudioAttributes mAudioAttrs;
830
831        /**
832         * @hide
833         */
834        public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) {
835            mVolumeType = type;
836            mAudioAttrs = attrs;
837            mVolumeControl = control;
838            mMaxVolume = max;
839            mCurrentVolume = current;
840        }
841
842        /**
843         * Get the type of playback which affects volume handling. One of:
844         * <ul>
845         * <li>{@link #PLAYBACK_TYPE_LOCAL}</li>
846         * <li>{@link #PLAYBACK_TYPE_REMOTE}</li>
847         * </ul>
848         *
849         * @return The type of playback this session is using.
850         */
851        public int getPlaybackType() {
852            return mVolumeType;
853        }
854
855        /**
856         * Get the audio attributes for this session. The attributes will affect
857         * volume handling for the session. When the volume type is
858         * {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be ignored by the
859         * remote volume handler.
860         *
861         * @return The attributes for this session.
862         */
863        public AudioAttributes getAudioAttributes() {
864            return mAudioAttrs;
865        }
866
867        /**
868         * Get the type of volume control that can be used. One of:
869         * <ul>
870         * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li>
871         * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li>
872         * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li>
873         * </ul>
874         *
875         * @return The type of volume control that may be used with this
876         *         session.
877         */
878        public int getVolumeControl() {
879            return mVolumeControl;
880        }
881
882        /**
883         * Get the maximum volume that may be set for this session.
884         *
885         * @return The maximum allowed volume where this session is playing.
886         */
887        public int getMaxVolume() {
888            return mMaxVolume;
889        }
890
891        /**
892         * Get the current volume for this session.
893         *
894         * @return The current volume where this session is playing.
895         */
896        public int getCurrentVolume() {
897            return mCurrentVolume;
898        }
899    }
900
901    private final static class CallbackStub extends ISessionControllerCallback.Stub {
902        private final WeakReference<MediaController> mController;
903
904        public CallbackStub(MediaController controller) {
905            mController = new WeakReference<MediaController>(controller);
906        }
907
908        @Override
909        public void onSessionDestroyed() {
910            MediaController controller = mController.get();
911            if (controller != null) {
912                controller.postMessage(MSG_DESTROYED, null, null);
913            }
914        }
915
916        @Override
917        public void onEvent(String event, Bundle extras) {
918            MediaController controller = mController.get();
919            if (controller != null) {
920                controller.postMessage(MSG_EVENT, event, extras);
921            }
922        }
923
924        @Override
925        public void onPlaybackStateChanged(PlaybackState state) {
926            MediaController controller = mController.get();
927            if (controller != null) {
928                controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null);
929            }
930        }
931
932        @Override
933        public void onMetadataChanged(MediaMetadata metadata) {
934            MediaController controller = mController.get();
935            if (controller != null) {
936                controller.postMessage(MSG_UPDATE_METADATA, metadata, null);
937            }
938        }
939
940        @Override
941        public void onQueueChanged(ParceledListSlice parceledQueue) {
942            List<MediaSession.QueueItem> queue = parceledQueue == null ? null : parceledQueue
943                    .getList();
944            MediaController controller = mController.get();
945            if (controller != null) {
946                controller.postMessage(MSG_UPDATE_QUEUE, queue, null);
947            }
948        }
949
950        @Override
951        public void onQueueTitleChanged(CharSequence title) {
952            MediaController controller = mController.get();
953            if (controller != null) {
954                controller.postMessage(MSG_UPDATE_QUEUE_TITLE, title, null);
955            }
956        }
957
958        @Override
959        public void onExtrasChanged(Bundle extras) {
960            MediaController controller = mController.get();
961            if (controller != null) {
962                controller.postMessage(MSG_UPDATE_EXTRAS, extras, null);
963            }
964        }
965
966        @Override
967        public void onVolumeInfoChanged(ParcelableVolumeInfo pvi) {
968            MediaController controller = mController.get();
969            if (controller != null) {
970                PlaybackInfo info = new PlaybackInfo(pvi.volumeType, pvi.audioAttrs, pvi.controlType,
971                        pvi.maxVolume, pvi.currentVolume);
972                controller.postMessage(MSG_UPDATE_VOLUME, info, null);
973            }
974        }
975
976    }
977
978    private final static class MessageHandler extends Handler {
979        private final MediaController.Callback mCallback;
980        private boolean mRegistered = false;
981
982        public MessageHandler(Looper looper, MediaController.Callback cb) {
983            super(looper, null, true);
984            mCallback = cb;
985        }
986
987        @Override
988        public void handleMessage(Message msg) {
989            if (!mRegistered) {
990                return;
991            }
992            switch (msg.what) {
993                case MSG_EVENT:
994                    mCallback.onSessionEvent((String) msg.obj, msg.getData());
995                    break;
996                case MSG_UPDATE_PLAYBACK_STATE:
997                    mCallback.onPlaybackStateChanged((PlaybackState) msg.obj);
998                    break;
999                case MSG_UPDATE_METADATA:
1000                    mCallback.onMetadataChanged((MediaMetadata) msg.obj);
1001                    break;
1002                case MSG_UPDATE_QUEUE:
1003                    mCallback.onQueueChanged((List<MediaSession.QueueItem>) msg.obj);
1004                    break;
1005                case MSG_UPDATE_QUEUE_TITLE:
1006                    mCallback.onQueueTitleChanged((CharSequence) msg.obj);
1007                    break;
1008                case MSG_UPDATE_EXTRAS:
1009                    mCallback.onExtrasChanged((Bundle) msg.obj);
1010                    break;
1011                case MSG_UPDATE_VOLUME:
1012                    mCallback.onAudioInfoChanged((PlaybackInfo) msg.obj);
1013                    break;
1014                case MSG_DESTROYED:
1015                    mCallback.onSessionDestroyed();
1016                    break;
1017            }
1018        }
1019
1020        public void post(int what, Object obj, Bundle data) {
1021            Message msg = obtainMessage(what, obj);
1022            msg.setData(data);
1023            msg.sendToTarget();
1024        }
1025    }
1026
1027}
1028