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 on 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 {@link Uri}.
607         *
608         * @param mediaId The uri 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         * Play an item with a specific id in the play queue. If you specify an
648         * id that is not in the play queue, the behavior is undefined.
649         */
650        public void skipToQueueItem(long id) {
651            try {
652                mSessionBinder.skipToQueueItem(id);
653            } catch (RemoteException e) {
654                Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e);
655            }
656        }
657
658        /**
659         * Request that the player pause its playback and stay at its current
660         * position.
661         */
662        public void pause() {
663            try {
664                mSessionBinder.pause();
665            } catch (RemoteException e) {
666                Log.wtf(TAG, "Error calling pause.", e);
667            }
668        }
669
670        /**
671         * Request that the player stop its playback; it may clear its state in
672         * whatever way is appropriate.
673         */
674        public void stop() {
675            try {
676                mSessionBinder.stop();
677            } catch (RemoteException e) {
678                Log.wtf(TAG, "Error calling stop.", e);
679            }
680        }
681
682        /**
683         * Move to a new location in the media stream.
684         *
685         * @param pos Position to move to, in milliseconds.
686         */
687        public void seekTo(long pos) {
688            try {
689                mSessionBinder.seekTo(pos);
690            } catch (RemoteException e) {
691                Log.wtf(TAG, "Error calling seekTo.", e);
692            }
693        }
694
695        /**
696         * Start fast forwarding. If playback is already fast forwarding this
697         * may increase the rate.
698         */
699        public void fastForward() {
700            try {
701                mSessionBinder.fastForward();
702            } catch (RemoteException e) {
703                Log.wtf(TAG, "Error calling fastForward.", e);
704            }
705        }
706
707        /**
708         * Skip to the next item.
709         */
710        public void skipToNext() {
711            try {
712                mSessionBinder.next();
713            } catch (RemoteException e) {
714                Log.wtf(TAG, "Error calling next.", e);
715            }
716        }
717
718        /**
719         * Start rewinding. If playback is already rewinding this may increase
720         * the rate.
721         */
722        public void rewind() {
723            try {
724                mSessionBinder.rewind();
725            } catch (RemoteException e) {
726                Log.wtf(TAG, "Error calling rewind.", e);
727            }
728        }
729
730        /**
731         * Skip to the previous item.
732         */
733        public void skipToPrevious() {
734            try {
735                mSessionBinder.previous();
736            } catch (RemoteException e) {
737                Log.wtf(TAG, "Error calling previous.", e);
738            }
739        }
740
741        /**
742         * Rate the current content. This will cause the rating to be set for
743         * the current user. The Rating type must match the type returned by
744         * {@link #getRatingType()}.
745         *
746         * @param rating The rating to set for the current content
747         */
748        public void setRating(Rating rating) {
749            try {
750                mSessionBinder.rate(rating);
751            } catch (RemoteException e) {
752                Log.wtf(TAG, "Error calling rate.", e);
753            }
754        }
755
756        /**
757         * Send a custom action back for the {@link MediaSession} to perform.
758         *
759         * @param customAction The action to perform.
760         * @param args Optional arguments to supply to the {@link MediaSession} for this
761         *             custom action.
762         */
763        public void sendCustomAction(@NonNull PlaybackState.CustomAction customAction,
764                    @Nullable Bundle args) {
765            if (customAction == null) {
766                throw new IllegalArgumentException("CustomAction cannot be null.");
767            }
768            sendCustomAction(customAction.getAction(), args);
769        }
770
771        /**
772         * Send the id and args from a custom action back for the {@link MediaSession} to perform.
773         *
774         * @see #sendCustomAction(PlaybackState.CustomAction action, Bundle args)
775         * @param action The action identifier of the {@link PlaybackState.CustomAction} as
776         *               specified by the {@link MediaSession}.
777         * @param args Optional arguments to supply to the {@link MediaSession} for this
778         *             custom action.
779         */
780        public void sendCustomAction(@NonNull String action, @Nullable Bundle args) {
781            if (TextUtils.isEmpty(action)) {
782                throw new IllegalArgumentException("CustomAction cannot be null.");
783            }
784            try {
785                mSessionBinder.sendCustomAction(action, args);
786            } catch (RemoteException e) {
787                Log.d(TAG, "Dead object in sendCustomAction.", e);
788            }
789        }
790    }
791
792    /**
793     * Holds information about the current playback and how audio is handled for
794     * this session.
795     */
796    public static final class PlaybackInfo {
797        /**
798         * The session uses remote playback.
799         */
800        public static final int PLAYBACK_TYPE_REMOTE = 2;
801        /**
802         * The session uses local playback.
803         */
804        public static final int PLAYBACK_TYPE_LOCAL = 1;
805
806        private final int mVolumeType;
807        private final int mVolumeControl;
808        private final int mMaxVolume;
809        private final int mCurrentVolume;
810        private final AudioAttributes mAudioAttrs;
811
812        /**
813         * @hide
814         */
815        public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) {
816            mVolumeType = type;
817            mAudioAttrs = attrs;
818            mVolumeControl = control;
819            mMaxVolume = max;
820            mCurrentVolume = current;
821        }
822
823        /**
824         * Get the type of playback which affects volume handling. One of:
825         * <ul>
826         * <li>{@link #PLAYBACK_TYPE_LOCAL}</li>
827         * <li>{@link #PLAYBACK_TYPE_REMOTE}</li>
828         * </ul>
829         *
830         * @return The type of playback this session is using.
831         */
832        public int getPlaybackType() {
833            return mVolumeType;
834        }
835
836        /**
837         * Get the audio attributes for this session. The attributes will affect
838         * volume handling for the session. When the volume type is
839         * {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be ignored by the
840         * remote volume handler.
841         *
842         * @return The attributes for this session.
843         */
844        public AudioAttributes getAudioAttributes() {
845            return mAudioAttrs;
846        }
847
848        /**
849         * Get the type of volume control that can be used. One of:
850         * <ul>
851         * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li>
852         * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li>
853         * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li>
854         * </ul>
855         *
856         * @return The type of volume control that may be used with this
857         *         session.
858         */
859        public int getVolumeControl() {
860            return mVolumeControl;
861        }
862
863        /**
864         * Get the maximum volume that may be set for this session.
865         *
866         * @return The maximum allowed volume where this session is playing.
867         */
868        public int getMaxVolume() {
869            return mMaxVolume;
870        }
871
872        /**
873         * Get the current volume for this session.
874         *
875         * @return The current volume where this session is playing.
876         */
877        public int getCurrentVolume() {
878            return mCurrentVolume;
879        }
880    }
881
882    private final static class CallbackStub extends ISessionControllerCallback.Stub {
883        private final WeakReference<MediaController> mController;
884
885        public CallbackStub(MediaController controller) {
886            mController = new WeakReference<MediaController>(controller);
887        }
888
889        @Override
890        public void onSessionDestroyed() {
891            MediaController controller = mController.get();
892            if (controller != null) {
893                controller.postMessage(MSG_DESTROYED, null, null);
894            }
895        }
896
897        @Override
898        public void onEvent(String event, Bundle extras) {
899            MediaController controller = mController.get();
900            if (controller != null) {
901                controller.postMessage(MSG_EVENT, event, extras);
902            }
903        }
904
905        @Override
906        public void onPlaybackStateChanged(PlaybackState state) {
907            MediaController controller = mController.get();
908            if (controller != null) {
909                controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null);
910            }
911        }
912
913        @Override
914        public void onMetadataChanged(MediaMetadata metadata) {
915            MediaController controller = mController.get();
916            if (controller != null) {
917                controller.postMessage(MSG_UPDATE_METADATA, metadata, null);
918            }
919        }
920
921        @Override
922        public void onQueueChanged(ParceledListSlice parceledQueue) {
923            List<MediaSession.QueueItem> queue = parceledQueue == null ? null : parceledQueue
924                    .getList();
925            MediaController controller = mController.get();
926            if (controller != null) {
927                controller.postMessage(MSG_UPDATE_QUEUE, queue, null);
928            }
929        }
930
931        @Override
932        public void onQueueTitleChanged(CharSequence title) {
933            MediaController controller = mController.get();
934            if (controller != null) {
935                controller.postMessage(MSG_UPDATE_QUEUE_TITLE, title, null);
936            }
937        }
938
939        @Override
940        public void onExtrasChanged(Bundle extras) {
941            MediaController controller = mController.get();
942            if (controller != null) {
943                controller.postMessage(MSG_UPDATE_EXTRAS, extras, null);
944            }
945        }
946
947        @Override
948        public void onVolumeInfoChanged(ParcelableVolumeInfo pvi) {
949            MediaController controller = mController.get();
950            if (controller != null) {
951                PlaybackInfo info = new PlaybackInfo(pvi.volumeType, pvi.audioAttrs, pvi.controlType,
952                        pvi.maxVolume, pvi.currentVolume);
953                controller.postMessage(MSG_UPDATE_VOLUME, info, null);
954            }
955        }
956
957    }
958
959    private final static class MessageHandler extends Handler {
960        private final MediaController.Callback mCallback;
961        private boolean mRegistered = false;
962
963        public MessageHandler(Looper looper, MediaController.Callback cb) {
964            super(looper, null, true);
965            mCallback = cb;
966        }
967
968        @Override
969        public void handleMessage(Message msg) {
970            if (!mRegistered) {
971                return;
972            }
973            switch (msg.what) {
974                case MSG_EVENT:
975                    mCallback.onSessionEvent((String) msg.obj, msg.getData());
976                    break;
977                case MSG_UPDATE_PLAYBACK_STATE:
978                    mCallback.onPlaybackStateChanged((PlaybackState) msg.obj);
979                    break;
980                case MSG_UPDATE_METADATA:
981                    mCallback.onMetadataChanged((MediaMetadata) msg.obj);
982                    break;
983                case MSG_UPDATE_QUEUE:
984                    mCallback.onQueueChanged((List<MediaSession.QueueItem>) msg.obj);
985                    break;
986                case MSG_UPDATE_QUEUE_TITLE:
987                    mCallback.onQueueTitleChanged((CharSequence) msg.obj);
988                    break;
989                case MSG_UPDATE_EXTRAS:
990                    mCallback.onExtrasChanged((Bundle) msg.obj);
991                    break;
992                case MSG_UPDATE_VOLUME:
993                    mCallback.onAudioInfoChanged((PlaybackInfo) msg.obj);
994                    break;
995                case MSG_DESTROYED:
996                    mCallback.onSessionDestroyed();
997                    break;
998            }
999        }
1000
1001        public void post(int what, Object obj, Bundle data) {
1002            obtainMessage(what, obj).sendToTarget();
1003        }
1004    }
1005
1006}
1007