OSDN Git Service

skip MediaController callbacks if it's been unregistered
[android-x86/frameworks-base.git] / media / java / android / media / session / MediaController.java
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
17 package android.media.session;
18
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.PendingIntent;
22 import android.content.Context;
23 import android.content.pm.ParceledListSlice;
24 import android.media.AudioAttributes;
25 import android.media.AudioManager;
26 import android.media.MediaMetadata;
27 import android.media.Rating;
28 import android.media.VolumeProvider;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.Looper;
33 import android.os.Message;
34 import android.os.RemoteException;
35 import android.os.ResultReceiver;
36 import android.text.TextUtils;
37 import android.util.Log;
38 import android.view.KeyEvent;
39
40 import java.lang.ref.WeakReference;
41 import java.util.ArrayList;
42 import 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  */
56 public 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 }