MediaSessionRecord.java revision a8f951462791a16f47e8c07e552232f31dcefac5
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 com.android.server.media;
18
19import android.content.Intent;
20import android.content.pm.PackageManager;
21import android.media.routeprovider.RouteRequest;
22import android.media.session.ISessionController;
23import android.media.session.ISessionControllerCallback;
24import android.media.session.ISession;
25import android.media.session.ISessionCallback;
26import android.media.session.SessionController;
27import android.media.session.MediaMetadata;
28import android.media.session.RouteCommand;
29import android.media.session.RouteInfo;
30import android.media.session.RouteOptions;
31import android.media.session.RouteEvent;
32import android.media.session.Session;
33import android.media.session.SessionInfo;
34import android.media.session.RouteInterface;
35import android.media.session.PlaybackState;
36import android.media.Rating;
37import android.os.Bundle;
38import android.os.Handler;
39import android.os.IBinder;
40import android.os.Looper;
41import android.os.Message;
42import android.os.RemoteException;
43import android.os.ResultReceiver;
44import android.os.SystemClock;
45import android.text.TextUtils;
46import android.util.Log;
47import android.util.Pair;
48import android.util.Slog;
49import android.view.KeyEvent;
50
51import java.io.PrintWriter;
52import java.util.ArrayList;
53import java.util.List;
54import java.util.UUID;
55
56/**
57 * This is the system implementation of a Session. Apps will interact with the
58 * MediaSession wrapper class instead.
59 */
60public class MediaSessionRecord implements IBinder.DeathRecipient {
61    private static final String TAG = "MediaSessionRecord";
62
63    /**
64     * These are the playback states that count as currently active.
65     */
66    private static final int[] ACTIVE_STATES = {
67            PlaybackState.PLAYSTATE_FAST_FORWARDING,
68            PlaybackState.PLAYSTATE_REWINDING,
69            PlaybackState.PLAYSTATE_SKIPPING_BACKWARDS,
70            PlaybackState.PLAYSTATE_SKIPPING_FORWARDS,
71            PlaybackState.PLAYSTATE_BUFFERING,
72            PlaybackState.PLAYSTATE_CONNECTING,
73            PlaybackState.PLAYSTATE_PLAYING };
74
75    /**
76     * The length of time a session will still be considered active after
77     * pausing in ms.
78     */
79    private static final int ACTIVE_BUFFER = 30000;
80
81    private final MessageHandler mHandler;
82
83    private final int mPid;
84    private final SessionInfo mSessionInfo;
85    private final String mTag;
86    private final ControllerStub mController;
87    private final SessionStub mSession;
88    private final SessionCb mSessionCb;
89    private final MediaSessionService mService;
90
91    private final Object mLock = new Object();
92    private final ArrayList<ISessionControllerCallback> mControllerCallbacks =
93            new ArrayList<ISessionControllerCallback>();
94    private final ArrayList<RouteRequest> mRequests = new ArrayList<RouteRequest>();
95
96    private RouteInfo mRoute;
97    private RouteOptions mRequest;
98    private RouteConnectionRecord mConnection;
99    // TODO define a RouteState class with relevant info
100    private int mRouteState;
101    private long mFlags;
102
103    // TransportPerformer fields
104
105    private MediaMetadata mMetadata;
106    private PlaybackState mPlaybackState;
107    private int mRatingType;
108    private long mLastActiveTime;
109    // End TransportPerformer fields
110
111    private boolean mIsActive = false;
112
113    public MediaSessionRecord(int pid, String packageName, ISessionCallback cb, String tag,
114            MediaSessionService service, Handler handler) {
115        mPid = pid;
116        mSessionInfo = new SessionInfo(UUID.randomUUID().toString(), packageName);
117        mTag = tag;
118        mController = new ControllerStub();
119        mSession = new SessionStub();
120        mSessionCb = new SessionCb(cb);
121        mService = service;
122        mHandler = new MessageHandler(handler.getLooper());
123    }
124
125    /**
126     * Get the binder for the {@link Session}.
127     *
128     * @return The session binder apps talk to.
129     */
130    public ISession getSessionBinder() {
131        return mSession;
132    }
133
134    /**
135     * Get the binder for the {@link SessionController}.
136     *
137     * @return The controller binder apps talk to.
138     */
139    public ISessionController getControllerBinder() {
140        return mController;
141    }
142
143    /**
144     * Get the set of route requests this session is interested in.
145     *
146     * @return The list of RouteRequests
147     */
148    public List<RouteRequest> getRouteRequests() {
149        return mRequests;
150    }
151
152    /**
153     * Get the route this session is currently on.
154     *
155     * @return The route the session is on.
156     */
157    public RouteInfo getRoute() {
158        return mRoute;
159    }
160
161    /**
162     * Get the info for this session.
163     *
164     * @return Info that identifies this session.
165     */
166    public SessionInfo getSessionInfo() {
167        return mSessionInfo;
168    }
169
170    /**
171     * Get this session's flags.
172     *
173     * @return The flags for this session.
174     */
175    public long getFlags() {
176        return mFlags;
177    }
178
179    /**
180     * Check if this session has the specified flag.
181     *
182     * @param flag The flag to check.
183     * @return True if this session has that flag set, false otherwise.
184     */
185    public boolean hasFlag(int flag) {
186        return (mFlags & flag) != 0;
187    }
188
189    /**
190     * Check if this session has system priorty and should receive media buttons
191     * before any other sessions.
192     *
193     * @return True if this is a system priority session, false otherwise
194     */
195    public boolean isSystemPriority() {
196        return (mFlags & Session.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0;
197    }
198
199    /**
200     * Set the selected route. This does not connect to the route, just notifies
201     * the app that a new route has been selected.
202     *
203     * @param route The route that was selected.
204     */
205    public void selectRoute(RouteInfo route) {
206        synchronized (mLock) {
207            if (route != mRoute) {
208                if (mConnection != null) {
209                    mConnection.disconnect();
210                    mConnection = null;
211                }
212            }
213            mRoute = route;
214        }
215        mSessionCb.sendRouteChange(route);
216    }
217
218    /**
219     * Update the state of the route this session is using and notify the
220     * session.
221     *
222     * @param state The new state of the route.
223     */
224    public void setRouteState(int state) {
225        mSessionCb.sendRouteStateChange(state);
226    }
227
228    /**
229     * Send an event to this session from the route it is using.
230     *
231     * @param event The event to send.
232     */
233    public void sendRouteEvent(RouteEvent event) {
234        mSessionCb.sendRouteEvent(event);
235    }
236
237    /**
238     * Set the connection to use for the selected route and notify the app it is
239     * now connected.
240     *
241     * @param route The route the connection is to.
242     * @param request The request that was used to connect.
243     * @param connection The connection to the route.
244     * @return True if this connection is still valid, false if it is stale.
245     */
246    public boolean setRouteConnected(RouteInfo route, RouteOptions request,
247            RouteConnectionRecord connection) {
248        synchronized (mLock) {
249            if (mRoute == null || !TextUtils.equals(route.getId(), mRoute.getId())) {
250                Log.w(TAG, "setRouteConnected: connected route is stale");
251                // TODO figure out disconnection path
252                return false;
253            }
254            if (request != mRequest) {
255                Log.w(TAG, "setRouteConnected: connection request is stale");
256                // TODO figure out disconnection path
257                return false;
258            }
259            mConnection = connection;
260            mConnection.setListener(mConnectionListener);
261            mSessionCb.sendRouteConnected();
262        }
263        return true;
264    }
265
266    /**
267     * Check if this session has been set to active by the app.
268     *
269     * @return True if the session is active, false otherwise.
270     */
271    public boolean isActive() {
272        return mIsActive;
273    }
274
275    /**
276     * Check if the session is currently performing playback. This will also
277     * return true if the session was recently paused.
278     *
279     * @return True if the session is performing playback, false otherwise.
280     */
281    public boolean isPlaybackActive() {
282        int state = mPlaybackState == null ? 0 : mPlaybackState.getState();
283        if (isActiveState(state)) {
284            return true;
285        }
286        if (state == mPlaybackState.PLAYSTATE_PAUSED) {
287            long inactiveTime = SystemClock.uptimeMillis() - mLastActiveTime;
288            if (inactiveTime < ACTIVE_BUFFER) {
289                return true;
290            }
291        }
292        return false;
293    }
294
295    public boolean isTransportControlEnabled() {
296        return hasFlag(Session.FLAG_HANDLES_TRANSPORT_CONTROLS);
297    }
298
299    @Override
300    public void binderDied() {
301        mService.sessionDied(this);
302    }
303
304    public void dump(PrintWriter pw, String prefix) {
305        pw.println(prefix + mTag + " " + this);
306
307        final String indent = prefix + "  ";
308        pw.println(indent + "pid=" + mPid);
309        pw.println(indent + "info=" + mSessionInfo.toString());
310        pw.println(indent + "published=" + mIsActive);
311        pw.println(indent + "flags=" + mFlags);
312        pw.println(indent + "rating type=" + mRatingType);
313        pw.println(indent + "controllers: " + mControllerCallbacks.size());
314        pw.println(indent + "state=" + (mPlaybackState == null ? null : mPlaybackState.toString()));
315        pw.println(indent + "metadata:" + getShortMetadataString());
316        pw.println(indent + "route requests {");
317        int size = mRequests.size();
318        for (int i = 0; i < size; i++) {
319            pw.println(indent + "  " + mRequests.get(i).toString());
320        }
321        pw.println(indent + "}");
322        pw.println(indent + "route=" + (mRoute == null ? null : mRoute.toString()));
323        pw.println(indent + "connection=" + (mConnection == null ? null : mConnection.toString()));
324        pw.println(indent + "params=" + (mRequest == null ? null : mRequest.toString()));
325    }
326
327    private boolean isActiveState(int state) {
328        for (int i = 0; i < ACTIVE_STATES.length; i++) {
329            if (ACTIVE_STATES[i] == state) {
330                return true;
331            }
332        }
333        return false;
334    }
335
336    private String getShortMetadataString() {
337        int fields = mMetadata == null ? 0 : mMetadata.size();
338        String title = mMetadata == null ? null : mMetadata
339                .getString(MediaMetadata.METADATA_KEY_TITLE);
340        return "size=" + fields + ", title=" + title;
341    }
342
343    private void onDestroy() {
344        mService.destroySession(this);
345    }
346
347    private void pushPlaybackStateUpdate() {
348        synchronized (mLock) {
349            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
350                ISessionControllerCallback cb = mControllerCallbacks.get(i);
351                try {
352                    cb.onPlaybackStateChanged(mPlaybackState);
353                } catch (RemoteException e) {
354                    Log.w(TAG, "Removing dead callback in pushPlaybackStateUpdate.", e);
355                    mControllerCallbacks.remove(i);
356                }
357            }
358        }
359    }
360
361    private void pushMetadataUpdate() {
362        synchronized (mLock) {
363            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
364                ISessionControllerCallback cb = mControllerCallbacks.get(i);
365                try {
366                    cb.onMetadataChanged(mMetadata);
367                } catch (RemoteException e) {
368                    Log.w(TAG, "Removing dead callback in pushMetadataUpdate.", e);
369                    mControllerCallbacks.remove(i);
370                }
371            }
372        }
373    }
374
375    private void pushRouteUpdate() {
376        synchronized (mLock) {
377            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
378                ISessionControllerCallback cb = mControllerCallbacks.get(i);
379                try {
380                    cb.onRouteChanged(mRoute);
381                } catch (RemoteException e) {
382                    Log.w(TAG, "Removing dead callback in pushRouteUpdate.", e);
383                    mControllerCallbacks.remove(i);
384                }
385            }
386        }
387    }
388
389    private void pushEvent(String event, Bundle data) {
390        synchronized (mLock) {
391            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
392                ISessionControllerCallback cb = mControllerCallbacks.get(i);
393                try {
394                    cb.onEvent(event, data);
395                } catch (RemoteException e) {
396                    Log.w(TAG, "Error with callback in pushEvent.", e);
397                }
398            }
399        }
400    }
401
402    private void pushRouteCommand(RouteCommand command, ResultReceiver cb) {
403        synchronized (mLock) {
404            if (mRoute == null || !TextUtils.equals(command.getRouteInfo(), mRoute.getId())) {
405                if (cb != null) {
406                    cb.send(RouteInterface.RESULT_ROUTE_IS_STALE, null);
407                    return;
408                }
409            }
410            if (mConnection != null) {
411                mConnection.sendCommand(command, cb);
412            } else if (cb != null) {
413                cb.send(RouteInterface.RESULT_NOT_CONNECTED, null);
414            }
415        }
416    }
417
418    private PlaybackState getStateWithUpdatedPosition() {
419        PlaybackState state = mPlaybackState;
420        long duration = -1;
421        if (mMetadata != null && mMetadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
422            duration = mMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
423        }
424        PlaybackState result = null;
425        if (state != null) {
426            if (state.getState() == PlaybackState.PLAYSTATE_PLAYING
427                    || state.getState() == PlaybackState.PLAYSTATE_FAST_FORWARDING
428                    || state.getState() == PlaybackState.PLAYSTATE_REWINDING) {
429                long updateTime = state.getLastPositionUpdateTime();
430                if (updateTime > 0) {
431                    long position = (long) (state.getRate()
432                            * (SystemClock.elapsedRealtime() - updateTime)) + state.getPosition();
433                    if (duration >= 0 && position > duration) {
434                        position = duration;
435                    } else if (position < 0) {
436                        position = 0;
437                    }
438                    result = new PlaybackState(state);
439                    result.setState(state.getState(), position, state.getRate());
440                }
441            }
442        }
443        return result == null ? state : result;
444    }
445
446    private final RouteConnectionRecord.Listener mConnectionListener
447            = new RouteConnectionRecord.Listener() {
448        @Override
449        public void onEvent(RouteEvent event) {
450            RouteEvent eventForSession = new RouteEvent(null, event.getIface(),
451                    event.getEvent(), event.getExtras());
452            mSessionCb.sendRouteEvent(eventForSession);
453        }
454
455        @Override
456        public void disconnect() {
457            // TODO
458        }
459    };
460
461    private final class SessionStub extends ISession.Stub {
462        @Override
463        public void destroy() {
464            onDestroy();
465        }
466
467        @Override
468        public void sendEvent(String event, Bundle data) {
469            mHandler.post(MessageHandler.MSG_SEND_EVENT, event, data);
470        }
471
472        @Override
473        public ISessionController getController() {
474            return mController;
475        }
476
477        @Override
478        public void setActive(boolean active) {
479            mIsActive = active;
480            mService.updateSession(MediaSessionRecord.this);
481            mHandler.post(MessageHandler.MSG_UPDATE_SESSION_STATE);
482        }
483
484        @Override
485        public void setFlags(int flags) {
486            if ((flags & Session.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
487                int pid = getCallingPid();
488                int uid = getCallingUid();
489                mService.enforcePhoneStatePermission(pid, uid);
490            }
491            mFlags = flags;
492            mHandler.post(MessageHandler.MSG_UPDATE_SESSION_STATE);
493        }
494
495        @Override
496        public void setMetadata(MediaMetadata metadata) {
497            mMetadata = metadata;
498            mHandler.post(MessageHandler.MSG_UPDATE_METADATA);
499        }
500
501        @Override
502        public void setPlaybackState(PlaybackState state) {
503            int oldState = mPlaybackState == null ? 0 : mPlaybackState.getState();
504            int newState = state == null ? 0 : state.getState();
505            if (isActiveState(oldState) && newState == PlaybackState.PLAYSTATE_PAUSED) {
506                mLastActiveTime = SystemClock.elapsedRealtime();
507            }
508            mPlaybackState = state;
509            mService.onSessionPlaystateChange(MediaSessionRecord.this, oldState, newState);
510            mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);
511        }
512
513        @Override
514        public void setRatingType(int type) {
515            mRatingType = type;
516        }
517
518        @Override
519        public void sendRouteCommand(RouteCommand command, ResultReceiver cb) {
520            mHandler.post(MessageHandler.MSG_SEND_COMMAND,
521                    new Pair<RouteCommand, ResultReceiver>(command, cb));
522        }
523
524        @Override
525        public boolean setRoute(RouteInfo route) throws RemoteException {
526            // TODO decide if allowed to set route and if the route exists
527            return false;
528        }
529
530        @Override
531        public void connectToRoute(RouteInfo route, RouteOptions request)
532                throws RemoteException {
533            if (mRoute == null || !TextUtils.equals(route.getId(), mRoute.getId())) {
534                throw new RemoteException("RouteInfo does not match current route");
535            }
536            mService.connectToRoute(MediaSessionRecord.this, route, request);
537            mRequest = request;
538        }
539
540        @Override
541        public void setRouteOptions(List<RouteOptions> options) throws RemoteException {
542            mRequests.clear();
543            for (int i = options.size() - 1; i >= 0; i--) {
544                RouteRequest request = new RouteRequest(mSessionInfo, options.get(i),
545                        false);
546                mRequests.add(request);
547            }
548        }
549    }
550
551    class SessionCb {
552        private final ISessionCallback mCb;
553
554        public SessionCb(ISessionCallback cb) {
555            mCb = cb;
556        }
557
558        public void sendMediaButton(KeyEvent keyEvent) {
559            Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
560            mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
561            try {
562                mCb.onMediaButton(mediaButtonIntent);
563            } catch (RemoteException e) {
564                Slog.e(TAG, "Remote failure in sendMediaRequest.", e);
565            }
566        }
567
568        public void sendCommand(String command, Bundle extras, ResultReceiver cb) {
569            try {
570                mCb.onCommand(command, extras, cb);
571            } catch (RemoteException e) {
572                Slog.e(TAG, "Remote failure in sendCommand.", e);
573            }
574        }
575
576        public void sendRouteChange(RouteInfo route) {
577            try {
578                mCb.onRequestRouteChange(route);
579            } catch (RemoteException e) {
580                Slog.e(TAG, "Remote failure in sendRouteChange.", e);
581            }
582        }
583
584        public void sendRouteStateChange(int state) {
585            try {
586                mCb.onRouteStateChange(state);
587            } catch (RemoteException e) {
588                Slog.e(TAG, "Remote failure in sendRouteStateChange.", e);
589            }
590        }
591
592        public void sendRouteEvent(RouteEvent event) {
593            try {
594                mCb.onRouteEvent(event);
595            } catch (RemoteException e) {
596                Slog.e(TAG, "Remote failure in sendRouteEvent.", e);
597            }
598        }
599
600        public void sendRouteConnected() {
601            try {
602                mCb.onRouteConnected(mRoute, mRequest);
603            } catch (RemoteException e) {
604                Slog.e(TAG, "Remote failure in sendRouteStateChange.", e);
605            }
606        }
607
608        public void play() {
609            try {
610                mCb.onPlay();
611            } catch (RemoteException e) {
612                Slog.e(TAG, "Remote failure in play.", e);
613            }
614        }
615
616        public void pause() {
617            try {
618                mCb.onPause();
619            } catch (RemoteException e) {
620                Slog.e(TAG, "Remote failure in pause.", e);
621            }
622        }
623
624        public void stop() {
625            try {
626                mCb.onStop();
627            } catch (RemoteException e) {
628                Slog.e(TAG, "Remote failure in stop.", e);
629            }
630        }
631
632        public void next() {
633            try {
634                mCb.onNext();
635            } catch (RemoteException e) {
636                Slog.e(TAG, "Remote failure in next.", e);
637            }
638        }
639
640        public void previous() {
641            try {
642                mCb.onPrevious();
643            } catch (RemoteException e) {
644                Slog.e(TAG, "Remote failure in previous.", e);
645            }
646        }
647
648        public void fastForward() {
649            try {
650                mCb.onFastForward();
651            } catch (RemoteException e) {
652                Slog.e(TAG, "Remote failure in fastForward.", e);
653            }
654        }
655
656        public void rewind() {
657            try {
658                mCb.onRewind();
659            } catch (RemoteException e) {
660                Slog.e(TAG, "Remote failure in rewind.", e);
661            }
662        }
663
664        public void seekTo(long pos) {
665            try {
666                mCb.onSeekTo(pos);
667            } catch (RemoteException e) {
668                Slog.e(TAG, "Remote failure in seekTo.", e);
669            }
670        }
671
672        public void rate(Rating rating) {
673            try {
674                mCb.onRate(rating);
675            } catch (RemoteException e) {
676                Slog.e(TAG, "Remote failure in rate.", e);
677            }
678        }
679    }
680
681    class ControllerStub extends ISessionController.Stub {
682        @Override
683        public void sendCommand(String command, Bundle extras, ResultReceiver cb)
684                throws RemoteException {
685            mSessionCb.sendCommand(command, extras, cb);
686        }
687
688        @Override
689        public void sendMediaButton(KeyEvent mediaButtonIntent) {
690            mSessionCb.sendMediaButton(mediaButtonIntent);
691        }
692
693        @Override
694        public void registerCallbackListener(ISessionControllerCallback cb) {
695            synchronized (mLock) {
696                if (!mControllerCallbacks.contains(cb)) {
697                    mControllerCallbacks.add(cb);
698                }
699            }
700        }
701
702        @Override
703        public void unregisterCallbackListener(ISessionControllerCallback cb)
704                throws RemoteException {
705            synchronized (mLock) {
706                mControllerCallbacks.remove(cb);
707            }
708        }
709
710        @Override
711        public void play() throws RemoteException {
712            mSessionCb.play();
713        }
714
715        @Override
716        public void pause() throws RemoteException {
717            mSessionCb.pause();
718        }
719
720        @Override
721        public void stop() throws RemoteException {
722            mSessionCb.stop();
723        }
724
725        @Override
726        public void next() throws RemoteException {
727            mSessionCb.next();
728        }
729
730        @Override
731        public void previous() throws RemoteException {
732            mSessionCb.previous();
733        }
734
735        @Override
736        public void fastForward() throws RemoteException {
737            mSessionCb.fastForward();
738        }
739
740        @Override
741        public void rewind() throws RemoteException {
742            mSessionCb.rewind();
743        }
744
745        @Override
746        public void seekTo(long pos) throws RemoteException {
747            mSessionCb.seekTo(pos);
748        }
749
750        @Override
751        public void rate(Rating rating) throws RemoteException {
752            mSessionCb.rate(rating);
753        }
754
755
756        @Override
757        public MediaMetadata getMetadata() {
758            return mMetadata;
759        }
760
761        @Override
762        public PlaybackState getPlaybackState() {
763            return getStateWithUpdatedPosition();
764        }
765
766        @Override
767        public int getRatingType() {
768            return mRatingType;
769        }
770
771        @Override
772        public boolean isTransportControlEnabled() {
773            return MediaSessionRecord.this.isTransportControlEnabled();
774        }
775
776        @Override
777        public void showRoutePicker() {
778            mService.showRoutePickerForSession(MediaSessionRecord.this);
779        }
780    }
781
782    private class MessageHandler extends Handler {
783        private static final int MSG_UPDATE_METADATA = 1;
784        private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
785        private static final int MSG_UPDATE_ROUTE = 3;
786        private static final int MSG_SEND_EVENT = 4;
787        private static final int MSG_UPDATE_ROUTE_FILTERS = 5;
788        private static final int MSG_SEND_COMMAND = 6;
789        private static final int MSG_UPDATE_SESSION_STATE = 7;
790
791        public MessageHandler(Looper looper) {
792            super(looper);
793        }
794        @Override
795        public void handleMessage(Message msg) {
796            switch (msg.what) {
797                case MSG_UPDATE_METADATA:
798                    pushMetadataUpdate();
799                    break;
800                case MSG_UPDATE_PLAYBACK_STATE:
801                    pushPlaybackStateUpdate();
802                    break;
803                case MSG_UPDATE_ROUTE:
804                    pushRouteUpdate();
805                    break;
806                case MSG_SEND_EVENT:
807                    pushEvent((String) msg.obj, msg.getData());
808                    break;
809                case MSG_SEND_COMMAND:
810                    Pair<RouteCommand, ResultReceiver> cmd =
811                            (Pair<RouteCommand, ResultReceiver>) msg.obj;
812                    pushRouteCommand(cmd.first, cmd.second);
813                    break;
814                case MSG_UPDATE_SESSION_STATE:
815                    // TODO add session state
816                    break;
817            }
818        }
819
820        public void post(int what) {
821            post(what, null);
822        }
823
824        public void post(int what, Object obj) {
825            obtainMessage(what, obj).sendToTarget();
826        }
827
828        public void post(int what, Object obj, Bundle data) {
829            Message msg = obtainMessage(what, obj);
830            msg.setData(data);
831            msg.sendToTarget();
832        }
833    }
834
835}
836