2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android.media.session;
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;
40 import java.lang.ref.WeakReference;
41 import java.util.ArrayList;
42 import java.util.List;
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.
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.
54 * MediaController objects are thread-safe.
56 public final class MediaController {
57 private static final String TAG = "MediaController";
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;
68 private final ISessionController mSessionBinder;
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();
76 private boolean mCbRegistered = false;
77 private String mPackageName;
80 private final TransportControls mTransportControls;
83 * Call for creating a MediaController directly from a binder. Should only
84 * be used by framework code.
88 public MediaController(Context context, ISessionController sessionBinder) {
89 if (sessionBinder == null) {
90 throw new IllegalArgumentException("Session token cannot be null");
92 if (context == null) {
93 throw new IllegalArgumentException("Context cannot be null");
95 mSessionBinder = sessionBinder;
96 mTransportControls = new TransportControls();
97 mToken = new MediaSession.Token(sessionBinder);
102 * Create a new MediaController from a session's token.
104 * @param context The caller's context.
105 * @param token The token for the session.
107 public MediaController(@NonNull Context context, @NonNull MediaSession.Token token) {
108 this(context, token.getBinder());
112 * Get a {@link TransportControls} instance to send transport actions to
113 * the associated session.
115 * @return A transport controls instance.
117 public @NonNull TransportControls getTransportControls() {
118 return mTransportControls;
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.
125 * @param keyEvent The media button event to dispatch.
126 * @return true if the event was sent to the session, false otherwise.
128 public boolean dispatchMediaButtonEvent(@NonNull KeyEvent keyEvent) {
129 if (keyEvent == null) {
130 throw new IllegalArgumentException("KeyEvent may not be null");
132 if (!KeyEvent.isMediaKey(keyEvent.getKeyCode())) {
136 return mSessionBinder.sendMediaButton(keyEvent);
137 } catch (RemoteException e) {
138 // System is dead. =(
144 * Get the current playback state for this session.
146 * @return The current PlaybackState or null
148 public @Nullable PlaybackState getPlaybackState() {
150 return mSessionBinder.getPlaybackState();
151 } catch (RemoteException e) {
152 Log.wtf(TAG, "Error calling getPlaybackState.", e);
158 * Get the current metadata for this session.
160 * @return The current MediaMetadata or null.
162 public @Nullable MediaMetadata getMetadata() {
164 return mSessionBinder.getMetadata();
165 } catch (RemoteException e) {
166 Log.wtf(TAG, "Error calling getMetadata.", e);
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.
175 * @return The current play queue or null.
177 public @Nullable List<MediaSession.QueueItem> getQueue() {
179 ParceledListSlice queue = mSessionBinder.getQueue();
181 return queue.getList();
183 } catch (RemoteException e) {
184 Log.wtf(TAG, "Error calling getQueue.", e);
190 * Get the queue title for this session.
192 public @Nullable CharSequence getQueueTitle() {
194 return mSessionBinder.getQueueTitle();
195 } catch (RemoteException e) {
196 Log.wtf(TAG, "Error calling getQueueTitle", e);
202 * Get the extras for this session.
204 public @Nullable Bundle getExtras() {
206 return mSessionBinder.getExtras();
207 } catch (RemoteException e) {
208 Log.wtf(TAG, "Error calling getExtras", e);
214 * Get the rating type supported by the session. One of:
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>
225 * @return The supported rating type
227 public int getRatingType() {
229 return mSessionBinder.getRatingType();
230 } catch (RemoteException e) {
231 Log.wtf(TAG, "Error calling getRatingType.", e);
232 return Rating.RATING_NONE;
237 * Get the flags for this session. Flags are defined in {@link MediaSession}.
239 * @return The current set of flags for the session.
241 public @MediaSession.SessionFlags long getFlags() {
243 return mSessionBinder.getFlags();
244 } catch (RemoteException e) {
245 Log.wtf(TAG, "Error calling getFlags.", e);
251 * Get the current playback info for this session.
253 * @return The current playback info or null.
255 public @Nullable PlaybackInfo getPlaybackInfo() {
257 ParcelableVolumeInfo result = mSessionBinder.getVolumeAttributes();
258 return new PlaybackInfo(result.volumeType, result.audioAttrs, result.controlType,
259 result.maxVolume, result.currentVolume);
261 } catch (RemoteException e) {
262 Log.wtf(TAG, "Error calling getAudioInfo.", e);
268 * Get an intent for launching UI associated with this session if one
271 * @return A {@link PendingIntent} to launch UI or null.
273 public @Nullable PendingIntent getSessionActivity() {
275 return mSessionBinder.getLaunchPendingIntent();
276 } catch (RemoteException e) {
277 Log.wtf(TAG, "Error calling getPendingIntent.", e);
283 * Get the token for the session this is connected to.
285 * @return The token for the connected session.
287 public @NonNull MediaSession.Token getSessionToken() {
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.
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
302 public void setVolumeTo(int value, int flags) {
304 mSessionBinder.setVolumeTo(value, flags, mContext.getPackageName());
305 } catch (RemoteException e) {
306 Log.wtf(TAG, "Error calling setVolumeTo.", e);
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.
319 * @see #getPlaybackInfo()
320 * @param direction The direction to adjust the volume in.
321 * @param flags Any flags to pass with the command.
323 public void adjustVolume(int direction, int flags) {
325 mSessionBinder.adjustVolume(direction, flags, mContext.getPackageName());
326 } catch (RemoteException e) {
327 Log.wtf(TAG, "Error calling adjustVolumeBy.", e);
332 * Registers a callback to receive updates from the Session. Updates will be
333 * posted on the caller's thread.
335 * @param callback The callback object, must not be null.
337 public void registerCallback(@NonNull Callback callback) {
338 registerCallback(callback, null);
342 * Registers a callback to receive updates from the session. Updates will be
343 * posted on the specified handler's thread.
345 * @param callback The callback object, must not be null.
346 * @param handler The handler to post updates on. If null the callers thread
349 public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
350 if (callback == null) {
351 throw new IllegalArgumentException("callback must not be null");
353 if (handler == null) {
354 handler = new Handler();
356 synchronized (mLock) {
357 addCallbackLocked(callback, handler);
362 * Unregisters the specified callback. If an update has already been posted
363 * you may still receive it after calling this method.
365 * @param callback The callback to remove.
367 public void unregisterCallback(@NonNull Callback callback) {
368 if (callback == null) {
369 throw new IllegalArgumentException("callback must not be null");
371 synchronized (mLock) {
372 removeCallbackLocked(callback);
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.
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
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");
391 mSessionBinder.sendCommand(command, args, cb);
392 } catch (RemoteException e) {
393 Log.d(TAG, "Dead object in sendCommand.", e);
398 * Get the session owner's package name.
400 * @return The package name of of the session owner.
402 public String getPackageName() {
403 if (mPackageName == null) {
405 mPackageName = mSessionBinder.getPackageName();
406 } catch (RemoteException e) {
407 Log.d(TAG, "Dead object in getPackageName.", e);
414 * Get the session's tag for debugging purposes.
416 * @return The session's tag.
419 public String getTag() {
422 mTag = mSessionBinder.getTag();
423 } catch (RemoteException e) {
424 Log.d(TAG, "Dead object in getTag.", e);
433 ISessionController getSessionBinder() {
434 return mSessionBinder;
440 public boolean controlsSameSession(MediaController other) {
441 if (other == null) return false;
442 return mSessionBinder.asBinder() == other.getSessionBinder().asBinder();
445 private void addCallbackLocked(Callback cb, Handler handler) {
446 if (getHandlerForCallbackLocked(cb) != null) {
447 Log.w(TAG, "Callback is already added, ignoring");
450 MessageHandler holder = new MessageHandler(handler.getLooper(), cb);
451 mCallbacks.add(holder);
452 holder.mRegistered = true;
454 if (!mCbRegistered) {
456 mSessionBinder.registerCallbackListener(mCbStub);
457 mCbRegistered = true;
458 } catch (RemoteException e) {
459 Log.e(TAG, "Dead object in registerCallback", e);
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);
471 handler.mRegistered = false;
474 if (mCbRegistered && mCallbacks.size() == 0) {
476 mSessionBinder.unregisterCallbackListener(mCbStub);
477 } catch (RemoteException e) {
478 Log.e(TAG, "Dead object in removeCallbackLocked");
480 mCbRegistered = false;
485 private MessageHandler getHandlerForCallbackLocked(Callback cb) {
487 throw new IllegalArgumentException("Callback cannot be null");
489 for (int i = mCallbacks.size() - 1; i >= 0; i--) {
490 MessageHandler handler = mCallbacks.get(i);
491 if (cb == handler.mCallback) {
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);
507 * Callback for receiving updates on from the session. A Callback can be
508 * registered using {@link #registerCallback}
510 public static abstract class Callback {
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.
515 public void onSessionDestroyed() {
519 * Override to handle custom events sent by the session owner without a
520 * specified interface. Controllers should only handle these for
523 * @param event The event from the session.
524 * @param extras Optional parameters for the event, may be null.
526 public void onSessionEvent(@NonNull String event, @Nullable Bundle extras) {
530 * Override to handle changes in playback state.
532 * @param state The new playback state of the session
534 public void onPlaybackStateChanged(@NonNull PlaybackState state) {
538 * Override to handle changes to the current metadata.
540 * @param metadata The current metadata for the session or null if none.
543 public void onMetadataChanged(@Nullable MediaMetadata metadata) {
547 * Override to handle changes to items in the queue.
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
554 public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) {
558 * Override to handle changes to the queue title.
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.
563 public void onQueueTitleChanged(@Nullable CharSequence title) {
567 * Override to handle changes to the {@link MediaSession} extras.
569 * @param extras The extras that can include other information associated with the
570 * {@link MediaSession}.
572 public void onExtrasChanged(@Nullable Bundle extras) {
576 * Override to handle changes to the audio info.
578 * @param info The current audio info for this session.
580 public void onAudioInfoChanged(PlaybackInfo info) {
585 * Interface for controlling media playback on a session. This allows an app
586 * to send media transport commands to the session.
588 public final class TransportControls {
589 private static final String TAG = "TransportController";
591 private TransportControls() {
595 * Request that the player start its playback at its current position.
599 mSessionBinder.play();
600 } catch (RemoteException e) {
601 Log.wtf(TAG, "Error calling play.", e);
606 * Request that the player start playback for a specific {@link Uri}.
608 * @param mediaId The uri of the requested media.
609 * @param extras Optional extras that can include extra information about the media item
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.");
618 mSessionBinder.playFromMediaId(mediaId, extras);
619 } catch (RemoteException e) {
620 Log.wtf(TAG, "Error calling play(" + mediaId + ").", e);
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
629 * @param query The search query.
630 * @param extras Optional extras that can include extra information
633 public void playFromSearch(String query, Bundle extras) {
635 // This is to remain compatible with
636 // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
640 mSessionBinder.playFromSearch(query, extras);
641 } catch (RemoteException e) {
642 Log.wtf(TAG, "Error calling play(" + query + ").", e);
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.
650 public void skipToQueueItem(long id) {
652 mSessionBinder.skipToQueueItem(id);
653 } catch (RemoteException e) {
654 Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e);
659 * Request that the player pause its playback and stay at its current
662 public void pause() {
664 mSessionBinder.pause();
665 } catch (RemoteException e) {
666 Log.wtf(TAG, "Error calling pause.", e);
671 * Request that the player stop its playback; it may clear its state in
672 * whatever way is appropriate.
676 mSessionBinder.stop();
677 } catch (RemoteException e) {
678 Log.wtf(TAG, "Error calling stop.", e);
683 * Move to a new location in the media stream.
685 * @param pos Position to move to, in milliseconds.
687 public void seekTo(long pos) {
689 mSessionBinder.seekTo(pos);
690 } catch (RemoteException e) {
691 Log.wtf(TAG, "Error calling seekTo.", e);
696 * Start fast forwarding. If playback is already fast forwarding this
697 * may increase the rate.
699 public void fastForward() {
701 mSessionBinder.fastForward();
702 } catch (RemoteException e) {
703 Log.wtf(TAG, "Error calling fastForward.", e);
708 * Skip to the next item.
710 public void skipToNext() {
712 mSessionBinder.next();
713 } catch (RemoteException e) {
714 Log.wtf(TAG, "Error calling next.", e);
719 * Start rewinding. If playback is already rewinding this may increase
722 public void rewind() {
724 mSessionBinder.rewind();
725 } catch (RemoteException e) {
726 Log.wtf(TAG, "Error calling rewind.", e);
731 * Skip to the previous item.
733 public void skipToPrevious() {
735 mSessionBinder.previous();
736 } catch (RemoteException e) {
737 Log.wtf(TAG, "Error calling previous.", e);
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()}.
746 * @param rating The rating to set for the current content
748 public void setRating(Rating rating) {
750 mSessionBinder.rate(rating);
751 } catch (RemoteException e) {
752 Log.wtf(TAG, "Error calling rate.", e);
757 * Send a custom action back for the {@link MediaSession} to perform.
759 * @param customAction The action to perform.
760 * @param args Optional arguments to supply to the {@link MediaSession} for this
763 public void sendCustomAction(@NonNull PlaybackState.CustomAction customAction,
764 @Nullable Bundle args) {
765 if (customAction == null) {
766 throw new IllegalArgumentException("CustomAction cannot be null.");
768 sendCustomAction(customAction.getAction(), args);
772 * Send the id and args from a custom action back for the {@link MediaSession} to perform.
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
780 public void sendCustomAction(@NonNull String action, @Nullable Bundle args) {
781 if (TextUtils.isEmpty(action)) {
782 throw new IllegalArgumentException("CustomAction cannot be null.");
785 mSessionBinder.sendCustomAction(action, args);
786 } catch (RemoteException e) {
787 Log.d(TAG, "Dead object in sendCustomAction.", e);
793 * Holds information about the current playback and how audio is handled for
796 public static final class PlaybackInfo {
798 * The session uses remote playback.
800 public static final int PLAYBACK_TYPE_REMOTE = 2;
802 * The session uses local playback.
804 public static final int PLAYBACK_TYPE_LOCAL = 1;
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;
815 public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) {
818 mVolumeControl = control;
820 mCurrentVolume = current;
824 * Get the type of playback which affects volume handling. One of:
826 * <li>{@link #PLAYBACK_TYPE_LOCAL}</li>
827 * <li>{@link #PLAYBACK_TYPE_REMOTE}</li>
830 * @return The type of playback this session is using.
832 public int getPlaybackType() {
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.
842 * @return The attributes for this session.
844 public AudioAttributes getAudioAttributes() {
849 * Get the type of volume control that can be used. One of:
851 * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li>
852 * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li>
853 * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li>
856 * @return The type of volume control that may be used with this
859 public int getVolumeControl() {
860 return mVolumeControl;
864 * Get the maximum volume that may be set for this session.
866 * @return The maximum allowed volume where this session is playing.
868 public int getMaxVolume() {
873 * Get the current volume for this session.
875 * @return The current volume where this session is playing.
877 public int getCurrentVolume() {
878 return mCurrentVolume;
882 private final static class CallbackStub extends ISessionControllerCallback.Stub {
883 private final WeakReference<MediaController> mController;
885 public CallbackStub(MediaController controller) {
886 mController = new WeakReference<MediaController>(controller);
890 public void onSessionDestroyed() {
891 MediaController controller = mController.get();
892 if (controller != null) {
893 controller.postMessage(MSG_DESTROYED, null, null);
898 public void onEvent(String event, Bundle extras) {
899 MediaController controller = mController.get();
900 if (controller != null) {
901 controller.postMessage(MSG_EVENT, event, extras);
906 public void onPlaybackStateChanged(PlaybackState state) {
907 MediaController controller = mController.get();
908 if (controller != null) {
909 controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null);
914 public void onMetadataChanged(MediaMetadata metadata) {
915 MediaController controller = mController.get();
916 if (controller != null) {
917 controller.postMessage(MSG_UPDATE_METADATA, metadata, null);
922 public void onQueueChanged(ParceledListSlice parceledQueue) {
923 List<MediaSession.QueueItem> queue = parceledQueue == null ? null : parceledQueue
925 MediaController controller = mController.get();
926 if (controller != null) {
927 controller.postMessage(MSG_UPDATE_QUEUE, queue, null);
932 public void onQueueTitleChanged(CharSequence title) {
933 MediaController controller = mController.get();
934 if (controller != null) {
935 controller.postMessage(MSG_UPDATE_QUEUE_TITLE, title, null);
940 public void onExtrasChanged(Bundle extras) {
941 MediaController controller = mController.get();
942 if (controller != null) {
943 controller.postMessage(MSG_UPDATE_EXTRAS, extras, null);
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);
959 private final static class MessageHandler extends Handler {
960 private final MediaController.Callback mCallback;
961 private boolean mRegistered = false;
963 public MessageHandler(Looper looper, MediaController.Callback cb) {
964 super(looper, null, true);
969 public void handleMessage(Message msg) {
975 mCallback.onSessionEvent((String) msg.obj, msg.getData());
977 case MSG_UPDATE_PLAYBACK_STATE:
978 mCallback.onPlaybackStateChanged((PlaybackState) msg.obj);
980 case MSG_UPDATE_METADATA:
981 mCallback.onMetadataChanged((MediaMetadata) msg.obj);
983 case MSG_UPDATE_QUEUE:
984 mCallback.onQueueChanged((List<MediaSession.QueueItem>) msg.obj);
986 case MSG_UPDATE_QUEUE_TITLE:
987 mCallback.onQueueTitleChanged((CharSequence) msg.obj);
989 case MSG_UPDATE_EXTRAS:
990 mCallback.onExtrasChanged((Bundle) msg.obj);
992 case MSG_UPDATE_VOLUME:
993 mCallback.onAudioInfoChanged((PlaybackInfo) msg.obj);
996 mCallback.onSessionDestroyed();
1001 public void post(int what, Object obj, Bundle data) {
1002 obtainMessage(what, obj).sendToTarget();