MediaSessionRecord.java revision 5a4e01114361b7dad4fc5465b4da00fd444fc098
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.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.media.routeprovider.RouteRequest;
23import android.media.session.ISessionController;
24import android.media.session.ISessionControllerCallback;
25import android.media.session.ISession;
26import android.media.session.ISessionCallback;
27import android.media.session.MediaController;
28import android.media.session.RouteCommand;
29import android.media.session.RouteInfo;
30import android.media.session.RouteOptions;
31import android.media.session.RouteEvent;
32import android.media.session.MediaSession;
33import android.media.session.MediaSessionInfo;
34import android.media.session.RouteInterface;
35import android.media.session.PlaybackState;
36import android.media.session.ParcelableVolumeInfo;
37import android.media.AudioManager;
38import android.media.MediaMetadata;
39import android.media.Rating;
40import android.media.VolumeProvider;
41import android.os.Binder;
42import android.os.Bundle;
43import android.os.DeadObjectException;
44import android.os.Handler;
45import android.os.IBinder;
46import android.os.Looper;
47import android.os.Message;
48import android.os.RemoteException;
49import android.os.ResultReceiver;
50import android.os.SystemClock;
51import android.text.TextUtils;
52import android.util.Log;
53import android.util.Pair;
54import android.util.Slog;
55import android.view.KeyEvent;
56
57import java.io.PrintWriter;
58import java.util.ArrayList;
59import java.util.List;
60import java.util.UUID;
61
62/**
63 * This is the system implementation of a Session. Apps will interact with the
64 * MediaSession wrapper class instead.
65 */
66public class MediaSessionRecord implements IBinder.DeathRecipient {
67    private static final String TAG = "MediaSessionRecord";
68    private static final boolean DEBUG = false;
69
70    /**
71     * The length of time a session will still be considered active after
72     * pausing in ms.
73     */
74    private static final int ACTIVE_BUFFER = 30000;
75
76    /**
77     * The amount of time we'll send an assumed volume after the last volume
78     * command before reverting to the last reported volume.
79     */
80    private static final int OPTIMISTIC_VOLUME_TIMEOUT = 1000;
81
82    private final MessageHandler mHandler;
83
84    private final int mOwnerPid;
85    private final int mOwnerUid;
86    private final int mUserId;
87    private final MediaSessionInfo mSessionInfo;
88    private final String mTag;
89    private final ControllerStub mController;
90    private final SessionStub mSession;
91    private final SessionCb mSessionCb;
92    private final MediaSessionService mService;
93
94    private final Object mLock = new Object();
95    private final ArrayList<ISessionControllerCallback> mControllerCallbacks =
96            new ArrayList<ISessionControllerCallback>();
97    private final ArrayList<RouteRequest> mRequests = new ArrayList<RouteRequest>();
98
99    private RouteInfo mRoute;
100    private RouteOptions mRequest;
101    private RouteConnectionRecord mConnection;
102    // TODO define a RouteState class with relevant info
103    private int mRouteState;
104    private long mFlags;
105    private ComponentName mMediaButtonReceiver;
106
107    // TransportPerformer fields
108
109    private MediaMetadata mMetadata;
110    private PlaybackState mPlaybackState;
111    private int mRatingType;
112    private long mLastActiveTime;
113    // End TransportPerformer fields
114
115    // Volume handling fields
116    private AudioManager mAudioManager;
117    private int mVolumeType = MediaSession.PLAYBACK_TYPE_LOCAL;
118    private int mAudioStream = AudioManager.STREAM_MUSIC;
119    private int mVolumeControlType = VolumeProvider.VOLUME_CONTROL_ABSOLUTE;
120    private int mMaxVolume = 0;
121    private int mCurrentVolume = 0;
122    private int mOptimisticVolume = -1;
123    // End volume handling fields
124
125    private boolean mIsActive = false;
126    private boolean mDestroyed = false;
127
128    public MediaSessionRecord(int ownerPid, int ownerUid, int userId, String ownerPackageName,
129            ISessionCallback cb, String tag, MediaSessionService service, Handler handler) {
130        mOwnerPid = ownerPid;
131        mOwnerUid = ownerUid;
132        mUserId = userId;
133        mSessionInfo = new MediaSessionInfo(UUID.randomUUID().toString(), ownerPackageName,
134                ownerPid);
135        mTag = tag;
136        mController = new ControllerStub();
137        mSession = new SessionStub();
138        mSessionCb = new SessionCb(cb);
139        mService = service;
140        mHandler = new MessageHandler(handler.getLooper());
141        mAudioManager = (AudioManager) service.getContext().getSystemService(Context.AUDIO_SERVICE);
142    }
143
144    /**
145     * Get the binder for the {@link MediaSession}.
146     *
147     * @return The session binder apps talk to.
148     */
149    public ISession getSessionBinder() {
150        return mSession;
151    }
152
153    /**
154     * Get the binder for the {@link MediaController}.
155     *
156     * @return The controller binder apps talk to.
157     */
158    public ISessionController getControllerBinder() {
159        return mController;
160    }
161
162    /**
163     * Get the set of route requests this session is interested in.
164     *
165     * @return The list of RouteRequests
166     */
167    public List<RouteRequest> getRouteRequests() {
168        return mRequests;
169    }
170
171    /**
172     * Get the route this session is currently on.
173     *
174     * @return The route the session is on.
175     */
176    public RouteInfo getRoute() {
177        return mRoute;
178    }
179
180    /**
181     * Get the info for this session.
182     *
183     * @return Info that identifies this session.
184     */
185    public MediaSessionInfo getSessionInfo() {
186        return mSessionInfo;
187    }
188
189    public ComponentName getMediaButtonReceiver() {
190        return mMediaButtonReceiver;
191    }
192
193    /**
194     * Get this session's flags.
195     *
196     * @return The flags for this session.
197     */
198    public long getFlags() {
199        return mFlags;
200    }
201
202    /**
203     * Check if this session has the specified flag.
204     *
205     * @param flag The flag to check.
206     * @return True if this session has that flag set, false otherwise.
207     */
208    public boolean hasFlag(int flag) {
209        return (mFlags & flag) != 0;
210    }
211
212    /**
213     * Get the user id this session was created for.
214     *
215     * @return The user id for this session.
216     */
217    public int getUserId() {
218        return mUserId;
219    }
220
221    /**
222     * Check if this session has system priorty and should receive media buttons
223     * before any other sessions.
224     *
225     * @return True if this is a system priority session, false otherwise
226     */
227    public boolean isSystemPriority() {
228        return (mFlags & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0;
229    }
230
231    /**
232     * Set the selected route. This does not connect to the route, just notifies
233     * the app that a new route has been selected.
234     *
235     * @param route The route that was selected.
236     */
237    public void selectRoute(RouteInfo route) {
238        synchronized (mLock) {
239            if (route != mRoute) {
240                disconnect(MediaSession.DISCONNECT_REASON_ROUTE_CHANGED);
241            }
242            mRoute = route;
243        }
244        mSessionCb.sendRouteChange(route);
245    }
246
247    /**
248     * Update the state of the route this session is using and notify the
249     * session.
250     *
251     * @param state The new state of the route.
252     */
253    public void setRouteState(int state) {
254        mSessionCb.sendRouteStateChange(state);
255    }
256
257    /**
258     * Send an event to this session from the route it is using.
259     *
260     * @param event The event to send.
261     */
262    public void sendRouteEvent(RouteEvent event) {
263        mSessionCb.sendRouteEvent(event);
264    }
265
266    /**
267     * Send a volume adjustment to the session owner.
268     *
269     * @param delta The amount to adjust the volume by.
270     */
271    public void adjustVolumeBy(int delta, int flags) {
272        if (isPlaybackActive(false)) {
273            flags &= ~AudioManager.FLAG_PLAY_SOUND;
274        }
275        if (mVolumeType == MediaSession.PLAYBACK_TYPE_LOCAL) {
276            if (delta == 0) {
277                mAudioManager.adjustStreamVolume(mAudioStream, delta, flags);
278            } else {
279                int direction = 0;
280                int steps = delta;
281                if (delta > 0) {
282                    direction = 1;
283                } else if (delta < 0) {
284                    direction = -1;
285                    steps = -delta;
286                }
287                for (int i = 0; i < steps; i++) {
288                    mAudioManager.adjustStreamVolume(mAudioStream, direction, flags);
289                }
290            }
291        } else {
292            if (mVolumeControlType == VolumeProvider.VOLUME_CONTROL_FIXED) {
293                // Nothing to do, the volume cannot be changed
294                return;
295            }
296            mSessionCb.adjustVolumeBy(delta);
297
298            int volumeBefore = (mOptimisticVolume < 0 ? mCurrentVolume : mOptimisticVolume);
299            mOptimisticVolume = volumeBefore + delta;
300            mOptimisticVolume = Math.max(0, Math.min(mOptimisticVolume, mMaxVolume));
301            mHandler.removeCallbacks(mClearOptimisticVolumeRunnable);
302            mHandler.postDelayed(mClearOptimisticVolumeRunnable, OPTIMISTIC_VOLUME_TIMEOUT);
303            if (volumeBefore != mOptimisticVolume) {
304                pushVolumeUpdate();
305            }
306
307            if (DEBUG) {
308                Log.d(TAG, "Adjusted optimistic volume to " + mOptimisticVolume + " max is "
309                        + mMaxVolume);
310            }
311        }
312    }
313
314    public void setVolumeTo(int value, int flags) {
315        if (mVolumeType == MediaSession.PLAYBACK_TYPE_LOCAL) {
316            mAudioManager.setStreamVolume(mAudioStream, value, flags);
317        } else {
318            if (mVolumeControlType != VolumeProvider.VOLUME_CONTROL_ABSOLUTE) {
319                // Nothing to do. The volume can't be set directly.
320                return;
321            }
322            value = Math.max(0, Math.min(value, mMaxVolume));
323            mSessionCb.setVolumeTo(value);
324
325            int volumeBefore = (mOptimisticVolume < 0 ? mCurrentVolume : mOptimisticVolume);
326            mOptimisticVolume = Math.max(0, Math.min(value, mMaxVolume));
327            mHandler.removeCallbacks(mClearOptimisticVolumeRunnable);
328            mHandler.postDelayed(mClearOptimisticVolumeRunnable, OPTIMISTIC_VOLUME_TIMEOUT);
329            if (volumeBefore != mOptimisticVolume) {
330                pushVolumeUpdate();
331            }
332
333            if (DEBUG) {
334                Log.d(TAG, "Set optimistic volume to " + mOptimisticVolume + " max is "
335                        + mMaxVolume);
336            }
337        }
338    }
339
340    /**
341     * Set the connection to use for the selected route and notify the app it is
342     * now connected.
343     *
344     * @param route The route the connection is to.
345     * @param request The request that was used to connect.
346     * @param connection The connection to the route.
347     * @return True if this connection is still valid, false if it is stale.
348     */
349    public boolean setRouteConnected(RouteInfo route, RouteOptions request,
350            RouteConnectionRecord connection) {
351        synchronized (mLock) {
352            if (mDestroyed) {
353                Log.i(TAG, "setRouteConnected: session has been destroyed");
354                connection.disconnect();
355                return false;
356            }
357            if (mRoute == null || !TextUtils.equals(route.getId(), mRoute.getId())) {
358                Log.w(TAG, "setRouteConnected: connected route is stale");
359                connection.disconnect();
360                return false;
361            }
362            if (request != mRequest) {
363                Log.w(TAG, "setRouteConnected: connection request is stale");
364                connection.disconnect();
365                return false;
366            }
367            mConnection = connection;
368            mConnection.setListener(mConnectionListener);
369            mSessionCb.sendRouteConnected();
370        }
371        return true;
372    }
373
374    /**
375     * Check if this session has been set to active by the app.
376     *
377     * @return True if the session is active, false otherwise.
378     */
379    public boolean isActive() {
380        return mIsActive && !mDestroyed;
381    }
382
383    /**
384     * Check if the session is currently performing playback. This will also
385     * return true if the session was recently paused.
386     *
387     * @param includeRecentlyActive True if playback that was recently paused
388     *            should count, false if it shouldn't.
389     * @return True if the session is performing playback, false otherwise.
390     */
391    public boolean isPlaybackActive(boolean includeRecentlyActive) {
392        int state = mPlaybackState == null ? 0 : mPlaybackState.getState();
393        if (MediaSession.isActiveState(state)) {
394            return true;
395        }
396        if (includeRecentlyActive && state == mPlaybackState.STATE_PAUSED) {
397            long inactiveTime = SystemClock.uptimeMillis() - mLastActiveTime;
398            if (inactiveTime < ACTIVE_BUFFER) {
399                return true;
400            }
401        }
402        return false;
403    }
404
405    /**
406     * Get the type of playback, either local or remote.
407     *
408     * @return The current type of playback.
409     */
410    public int getPlaybackType() {
411        return mVolumeType;
412    }
413
414    /**
415     * Get the local audio stream being used. Only valid if playback type is
416     * local.
417     *
418     * @return The audio stream the session is using.
419     */
420    public int getAudioStream() {
421        return mAudioStream;
422    }
423
424    /**
425     * Get the type of volume control. Only valid if playback type is remote.
426     *
427     * @return The volume control type being used.
428     */
429    public int getVolumeControl() {
430        return mVolumeControlType;
431    }
432
433    /**
434     * Get the max volume that can be set. Only valid if playback type is
435     * remote.
436     *
437     * @return The max volume that can be set.
438     */
439    public int getMaxVolume() {
440        return mMaxVolume;
441    }
442
443    /**
444     * Get the current volume for this session. Only valid if playback type is
445     * remote.
446     *
447     * @return The current volume of the remote playback.
448     */
449    public int getCurrentVolume() {
450        return mCurrentVolume;
451    }
452
453    /**
454     * Get the volume we'd like it to be set to. This is only valid for a short
455     * while after a call to adjust or set volume.
456     *
457     * @return The current optimistic volume or -1.
458     */
459    public int getOptimisticVolume() {
460        return mOptimisticVolume;
461    }
462
463    /**
464     * @return True if this session is currently connected to a route.
465     */
466    public boolean isConnected() {
467        return mConnection != null;
468    }
469
470    public void disconnect(int reason) {
471        synchronized (mLock) {
472            if (!mDestroyed) {
473                disconnectLocked(reason);
474            }
475        }
476    }
477
478    private void disconnectLocked(int reason) {
479        if (mConnection != null) {
480            mConnection.setListener(null);
481            mConnection.disconnect();
482            mConnection = null;
483            pushDisconnected(reason);
484        }
485    }
486
487    public boolean isTransportControlEnabled() {
488        return hasFlag(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
489    }
490
491    @Override
492    public void binderDied() {
493        mService.sessionDied(this);
494    }
495
496    /**
497     * Finish cleaning up this session, including disconnecting if connected and
498     * removing the death observer from the callback binder.
499     */
500    public void onDestroy() {
501        synchronized (mLock) {
502            if (mDestroyed) {
503                return;
504            }
505            if (isConnected()) {
506                disconnectLocked(MediaSession.DISCONNECT_REASON_SESSION_DESTROYED);
507            }
508            mRoute = null;
509            mRequest = null;
510            mDestroyed = true;
511        }
512    }
513
514    public ISessionCallback getCallback() {
515        return mSessionCb.mCb;
516    }
517
518    public void sendMediaButton(KeyEvent ke, int sequenceId, ResultReceiver cb) {
519        mSessionCb.sendMediaButton(ke, sequenceId, cb);
520    }
521
522    public void dump(PrintWriter pw, String prefix) {
523        pw.println(prefix + mTag + " " + this);
524
525        final String indent = prefix + "  ";
526        pw.println(indent + "ownerPid=" + mOwnerPid + ", ownerUid=" + mOwnerUid
527                + ", userId=" + mUserId);
528        pw.println(indent + "info=" + mSessionInfo.toString());
529        pw.println(indent + "active=" + mIsActive);
530        pw.println(indent + "flags=" + mFlags);
531        pw.println(indent + "rating type=" + mRatingType);
532        pw.println(indent + "controllers: " + mControllerCallbacks.size());
533        pw.println(indent + "state=" + (mPlaybackState == null ? null : mPlaybackState.toString()));
534        pw.println(indent + "metadata:" + getShortMetadataString());
535        pw.println(indent + "route requests {");
536        int size = mRequests.size();
537        for (int i = 0; i < size; i++) {
538            pw.println(indent + "  " + mRequests.get(i).toString());
539        }
540        pw.println(indent + "}");
541        pw.println(indent + "route=" + (mRoute == null ? null : mRoute.toString()));
542        pw.println(indent + "connection=" + (mConnection == null ? null : mConnection.toString()));
543        pw.println(indent + "params=" + (mRequest == null ? null : mRequest.toString()));
544    }
545
546    private String getShortMetadataString() {
547        int fields = mMetadata == null ? 0 : mMetadata.size();
548        String title = mMetadata == null ? null : mMetadata
549                .getString(MediaMetadata.METADATA_KEY_TITLE);
550        return "size=" + fields + ", title=" + title;
551    }
552
553    private void pushDisconnected(int reason) {
554        synchronized (mLock) {
555            mSessionCb.sendRouteDisconnected(reason);
556        }
557    }
558
559    private void pushPlaybackStateUpdate() {
560        synchronized (mLock) {
561            if (mDestroyed) {
562                return;
563            }
564            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
565                ISessionControllerCallback cb = mControllerCallbacks.get(i);
566                try {
567                    cb.onPlaybackStateChanged(mPlaybackState);
568                } catch (DeadObjectException e) {
569                    mControllerCallbacks.remove(i);
570                    Log.w(TAG, "Removed dead callback in pushPlaybackStateUpdate.", e);
571                } catch (RemoteException e) {
572                    Log.w(TAG, "unexpected exception in pushPlaybackStateUpdate.", e);
573                }
574            }
575        }
576    }
577
578    private void pushMetadataUpdate() {
579        synchronized (mLock) {
580            if (mDestroyed) {
581                return;
582            }
583            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
584                ISessionControllerCallback cb = mControllerCallbacks.get(i);
585                try {
586                    cb.onMetadataChanged(mMetadata);
587                } catch (DeadObjectException e) {
588                    Log.w(TAG, "Removing dead callback in pushMetadataUpdate. ", e);
589                    mControllerCallbacks.remove(i);
590                } catch (RemoteException e) {
591                    Log.w(TAG, "unexpected exception in pushMetadataUpdate. ", e);
592                }
593            }
594        }
595    }
596
597    private void pushVolumeUpdate() {
598        synchronized (mLock) {
599            if (mDestroyed) {
600                return;
601            }
602            ParcelableVolumeInfo info = mController.getVolumeAttributes();
603            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
604                ISessionControllerCallback cb = mControllerCallbacks.get(i);
605                try {
606                    cb.onVolumeInfoChanged(info);
607                } catch (DeadObjectException e) {
608                    Log.w(TAG, "Removing dead callback in pushVolumeUpdate. ", e);
609                } catch (RemoteException e) {
610                    Log.w(TAG, "Unexpected exception in pushVolumeUpdate. ", e);
611                }
612            }
613        }
614    }
615
616    private void pushRouteUpdate() {
617        synchronized (mLock) {
618            if (mDestroyed) {
619                return;
620            }
621            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
622                ISessionControllerCallback cb = mControllerCallbacks.get(i);
623                try {
624                    cb.onRouteChanged(mRoute);
625                } catch (DeadObjectException e) {
626                    Log.w(TAG, "Removing dead callback in pushRouteUpdate.", e);
627                    mControllerCallbacks.remove(i);
628                } catch (RemoteException e) {
629                    Log.w(TAG, "unexpected exception in pushRouteUpdate.", e);
630                }
631            }
632        }
633    }
634
635    private void pushEvent(String event, Bundle data) {
636        synchronized (mLock) {
637            if (mDestroyed) {
638                return;
639            }
640            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
641                ISessionControllerCallback cb = mControllerCallbacks.get(i);
642                try {
643                    cb.onEvent(event, data);
644                } catch (DeadObjectException e) {
645                    Log.w(TAG, "Removing dead callback in pushEvent.", e);
646                    mControllerCallbacks.remove(i);
647                } catch (RemoteException e) {
648                    Log.w(TAG, "unexpected exception in pushEvent.", e);
649                }
650            }
651        }
652    }
653
654    private void pushRouteCommand(RouteCommand command, ResultReceiver cb) {
655        synchronized (mLock) {
656            if (mDestroyed) {
657                return;
658            }
659            if (mRoute == null || !TextUtils.equals(command.getRouteInfo(), mRoute.getId())) {
660                if (cb != null) {
661                    cb.send(RouteInterface.RESULT_ROUTE_IS_STALE, null);
662                    return;
663                }
664            }
665            if (mConnection != null) {
666                mConnection.sendCommand(command, cb);
667            } else if (cb != null) {
668                cb.send(RouteInterface.RESULT_NOT_CONNECTED, null);
669            }
670        }
671    }
672
673    private PlaybackState getStateWithUpdatedPosition() {
674        PlaybackState state = mPlaybackState;
675        long duration = -1;
676        if (mMetadata != null && mMetadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
677            duration = mMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
678        }
679        PlaybackState result = null;
680        if (state != null) {
681            if (state.getState() == PlaybackState.STATE_PLAYING
682                    || state.getState() == PlaybackState.STATE_FAST_FORWARDING
683                    || state.getState() == PlaybackState.STATE_REWINDING) {
684                long updateTime = state.getLastPositionUpdateTime();
685                long currentTime = SystemClock.elapsedRealtime();
686                if (updateTime > 0) {
687                    long position = (long) (state.getPlaybackSpeed()
688                            * (currentTime - updateTime)) + state.getPosition();
689                    if (duration >= 0 && position > duration) {
690                        position = duration;
691                    } else if (position < 0) {
692                        position = 0;
693                    }
694                    PlaybackState.Builder builder = new PlaybackState.Builder(state);
695                    builder.setState(state.getState(), position, state.getPlaybackSpeed(),
696                            currentTime);
697                    result = builder.build();
698                }
699            }
700        }
701        return result == null ? state : result;
702    }
703
704    private int getControllerCbIndexForCb(ISessionControllerCallback cb) {
705        IBinder binder = cb.asBinder();
706        for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
707            if (binder.equals(mControllerCallbacks.get(i).asBinder())) {
708                return i;
709            }
710        }
711        return -1;
712    }
713
714    private final RouteConnectionRecord.Listener mConnectionListener
715            = new RouteConnectionRecord.Listener() {
716        @Override
717        public void onEvent(RouteEvent event) {
718            RouteEvent eventForSession = new RouteEvent(null, event.getIface(),
719                    event.getEvent(), event.getExtras());
720            mSessionCb.sendRouteEvent(eventForSession);
721        }
722
723        @Override
724        public void disconnect() {
725            MediaSessionRecord.this.disconnect(MediaSession.DISCONNECT_REASON_PROVIDER_DISCONNECTED);
726        }
727    };
728
729    private final Runnable mClearOptimisticVolumeRunnable = new Runnable() {
730        @Override
731        public void run() {
732            boolean needUpdate = (mOptimisticVolume != mCurrentVolume);
733            mOptimisticVolume = -1;
734            if (needUpdate) {
735                pushVolumeUpdate();
736            }
737        }
738    };
739
740    private final class SessionStub extends ISession.Stub {
741        @Override
742        public void destroy() {
743            mService.destroySession(MediaSessionRecord.this);
744        }
745
746        @Override
747        public void sendEvent(String event, Bundle data) {
748            mHandler.post(MessageHandler.MSG_SEND_EVENT, event, data);
749        }
750
751        @Override
752        public ISessionController getController() {
753            return mController;
754        }
755
756        @Override
757        public void setActive(boolean active) {
758            mIsActive = active;
759            mService.updateSession(MediaSessionRecord.this);
760            mHandler.post(MessageHandler.MSG_UPDATE_SESSION_STATE);
761        }
762
763        @Override
764        public void setFlags(int flags) {
765            if ((flags & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
766                int pid = getCallingPid();
767                int uid = getCallingUid();
768                mService.enforcePhoneStatePermission(pid, uid);
769            }
770            mFlags = flags;
771            mHandler.post(MessageHandler.MSG_UPDATE_SESSION_STATE);
772        }
773
774        @Override
775        public void setMediaButtonReceiver(ComponentName mbr) {
776            mMediaButtonReceiver = mbr;
777        }
778
779        @Override
780        public void setMetadata(MediaMetadata metadata) {
781            mMetadata = metadata;
782            mHandler.post(MessageHandler.MSG_UPDATE_METADATA);
783        }
784
785        @Override
786        public void setPlaybackState(PlaybackState state) {
787            int oldState = mPlaybackState == null ? 0 : mPlaybackState.getState();
788            int newState = state == null ? 0 : state.getState();
789            if (MediaSession.isActiveState(oldState) && newState == PlaybackState.STATE_PAUSED) {
790                mLastActiveTime = SystemClock.elapsedRealtime();
791            }
792            mPlaybackState = state;
793            mService.onSessionPlaystateChange(MediaSessionRecord.this, oldState, newState);
794            mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);
795        }
796
797        @Override
798        public void setRatingType(int type) {
799            mRatingType = type;
800        }
801
802        @Override
803        public void sendRouteCommand(RouteCommand command, ResultReceiver cb) {
804            mHandler.post(MessageHandler.MSG_SEND_COMMAND,
805                    new Pair<RouteCommand, ResultReceiver>(command, cb));
806        }
807
808        @Override
809        public boolean setRoute(RouteInfo route) throws RemoteException {
810            // TODO decide if allowed to set route and if the route exists
811            return false;
812        }
813
814        @Override
815        public void connectToRoute(RouteInfo route, RouteOptions request)
816                throws RemoteException {
817            if (mRoute == null || !TextUtils.equals(route.getId(), mRoute.getId())) {
818                throw new RemoteException("RouteInfo does not match current route");
819            }
820            mService.connectToRoute(MediaSessionRecord.this, route, request);
821            mRequest = request;
822        }
823
824        @Override
825        public void disconnectFromRoute(RouteInfo route) {
826            if (route != null && mRoute != null
827                    && TextUtils.equals(route.getId(), mRoute.getId())) {
828                disconnect(MediaSession.DISCONNECT_REASON_SESSION_DISCONNECTED);
829            }
830        }
831
832        @Override
833        public void setRouteOptions(List<RouteOptions> options) throws RemoteException {
834            mRequests.clear();
835            for (int i = options.size() - 1; i >= 0; i--) {
836                RouteRequest request = new RouteRequest(mSessionInfo, options.get(i),
837                        false);
838                mRequests.add(request);
839            }
840        }
841
842        @Override
843        public void setCurrentVolume(int volume) {
844            mCurrentVolume = volume;
845            mHandler.post(MessageHandler.MSG_UPDATE_VOLUME);
846        }
847
848        @Override
849        public void configureVolumeHandling(int type, int arg1, int arg2) throws RemoteException {
850            boolean typeChanged = type != mVolumeType;
851            switch(type) {
852                case MediaSession.PLAYBACK_TYPE_LOCAL:
853                    mVolumeType = type;
854                    int audioStream = arg1;
855                    if (isValidStream(audioStream)) {
856                        mAudioStream = audioStream;
857                    } else {
858                        Log.e(TAG, "Cannot set stream to " + audioStream + ". Using music stream");
859                        mAudioStream = AudioManager.STREAM_MUSIC;
860                    }
861                    break;
862                case MediaSession.PLAYBACK_TYPE_REMOTE:
863                    mVolumeType = type;
864                    mVolumeControlType = arg1;
865                    mMaxVolume = arg2;
866                    break;
867                default:
868                    throw new IllegalArgumentException("Volume handling type " + type
869                            + " not recognized.");
870            }
871            if (typeChanged) {
872                mService.onSessionPlaybackTypeChanged(MediaSessionRecord.this);
873            }
874        }
875
876        private boolean isValidStream(int stream) {
877            return stream >= AudioManager.STREAM_VOICE_CALL
878                    && stream <= AudioManager.STREAM_NOTIFICATION;
879        }
880    }
881
882    class SessionCb {
883        private final ISessionCallback mCb;
884
885        public SessionCb(ISessionCallback cb) {
886            mCb = cb;
887        }
888
889        public boolean sendMediaButton(KeyEvent keyEvent, int sequenceId, ResultReceiver cb) {
890            Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
891            mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
892            try {
893                mCb.onMediaButton(mediaButtonIntent, sequenceId, cb);
894                return true;
895            } catch (RemoteException e) {
896                Slog.e(TAG, "Remote failure in sendMediaRequest.", e);
897            }
898            return false;
899        }
900
901        public void sendCommand(String command, Bundle extras, ResultReceiver cb) {
902            try {
903                mCb.onCommand(command, extras, cb);
904            } catch (RemoteException e) {
905                Slog.e(TAG, "Remote failure in sendCommand.", e);
906            }
907        }
908
909        public void sendRouteChange(RouteInfo route) {
910            try {
911                mCb.onRequestRouteChange(route);
912            } catch (RemoteException e) {
913                Slog.e(TAG, "Remote failure in sendRouteChange.", e);
914            }
915        }
916
917        public void sendRouteStateChange(int state) {
918            try {
919                mCb.onRouteStateChange(state);
920            } catch (RemoteException e) {
921                Slog.e(TAG, "Remote failure in sendRouteStateChange.", e);
922            }
923        }
924
925        public void sendRouteEvent(RouteEvent event) {
926            try {
927                mCb.onRouteEvent(event);
928            } catch (RemoteException e) {
929                Slog.e(TAG, "Remote failure in sendRouteEvent.", e);
930            }
931        }
932
933        public void sendRouteConnected() {
934            try {
935                mCb.onRouteConnected(mRoute, mRequest);
936            } catch (RemoteException e) {
937                Slog.e(TAG, "Remote failure in sendRouteStateChange.", e);
938            }
939        }
940
941        public void sendRouteDisconnected(int reason) {
942            try {
943                mCb.onRouteDisconnected(mRoute, reason);
944            } catch (RemoteException e) {
945                Slog.e(TAG, "Remote failure in sendRouteDisconnected");
946            }
947        }
948
949        public void play() {
950            try {
951                mCb.onPlay();
952            } catch (RemoteException e) {
953                Slog.e(TAG, "Remote failure in play.", e);
954            }
955        }
956
957        public void pause() {
958            try {
959                mCb.onPause();
960            } catch (RemoteException e) {
961                Slog.e(TAG, "Remote failure in pause.", e);
962            }
963        }
964
965        public void stop() {
966            try {
967                mCb.onStop();
968            } catch (RemoteException e) {
969                Slog.e(TAG, "Remote failure in stop.", e);
970            }
971        }
972
973        public void next() {
974            try {
975                mCb.onNext();
976            } catch (RemoteException e) {
977                Slog.e(TAG, "Remote failure in next.", e);
978            }
979        }
980
981        public void previous() {
982            try {
983                mCb.onPrevious();
984            } catch (RemoteException e) {
985                Slog.e(TAG, "Remote failure in previous.", e);
986            }
987        }
988
989        public void fastForward() {
990            try {
991                mCb.onFastForward();
992            } catch (RemoteException e) {
993                Slog.e(TAG, "Remote failure in fastForward.", e);
994            }
995        }
996
997        public void rewind() {
998            try {
999                mCb.onRewind();
1000            } catch (RemoteException e) {
1001                Slog.e(TAG, "Remote failure in rewind.", e);
1002            }
1003        }
1004
1005        public void seekTo(long pos) {
1006            try {
1007                mCb.onSeekTo(pos);
1008            } catch (RemoteException e) {
1009                Slog.e(TAG, "Remote failure in seekTo.", e);
1010            }
1011        }
1012
1013        public void rate(Rating rating) {
1014            try {
1015                mCb.onRate(rating);
1016            } catch (RemoteException e) {
1017                Slog.e(TAG, "Remote failure in rate.", e);
1018            }
1019        }
1020
1021        public void adjustVolumeBy(int delta) {
1022            try {
1023                mCb.onAdjustVolumeBy(delta);
1024            } catch (RemoteException e) {
1025                Slog.e(TAG, "Remote failure in adjustVolumeBy.", e);
1026            }
1027        }
1028
1029        public void setVolumeTo(int value) {
1030            try {
1031                mCb.onSetVolumeTo(value);
1032            } catch (RemoteException e) {
1033                Slog.e(TAG, "Remote failure in adjustVolumeBy.", e);
1034            }
1035        }
1036    }
1037
1038    class ControllerStub extends ISessionController.Stub {
1039        @Override
1040        public void sendCommand(String command, Bundle extras, ResultReceiver cb)
1041                throws RemoteException {
1042            mSessionCb.sendCommand(command, extras, cb);
1043        }
1044
1045        @Override
1046        public boolean sendMediaButton(KeyEvent mediaButtonIntent) {
1047            return mSessionCb.sendMediaButton(mediaButtonIntent, 0, null);
1048        }
1049
1050        @Override
1051        public void registerCallbackListener(ISessionControllerCallback cb) {
1052            synchronized (mLock) {
1053                if (getControllerCbIndexForCb(cb) < 0) {
1054                    mControllerCallbacks.add(cb);
1055                    if (DEBUG) {
1056                        Log.d(TAG, "registering controller callback " + cb);
1057                    }
1058                }
1059            }
1060        }
1061
1062        @Override
1063        public void unregisterCallbackListener(ISessionControllerCallback cb)
1064                throws RemoteException {
1065            synchronized (mLock) {
1066                int index = getControllerCbIndexForCb(cb);
1067                if (index != -1) {
1068                    mControllerCallbacks.remove(index);
1069                }
1070                if (DEBUG) {
1071                    Log.d(TAG, "unregistering callback " + cb + ". index=" + index);
1072                }
1073            }
1074        }
1075
1076        @Override
1077        public MediaSessionInfo getSessionInfo() {
1078            return mSessionInfo;
1079        }
1080
1081        @Override
1082        public long getFlags() {
1083            return mFlags;
1084        }
1085
1086        @Override
1087        public ParcelableVolumeInfo getVolumeAttributes() {
1088            synchronized (mLock) {
1089                int type;
1090                int max;
1091                int current;
1092                if (mVolumeType == MediaSession.PLAYBACK_TYPE_REMOTE) {
1093                    type = mVolumeControlType;
1094                    max = mMaxVolume;
1095                    current = mOptimisticVolume != -1 ? mOptimisticVolume
1096                            : mCurrentVolume;
1097                } else {
1098                    type = VolumeProvider.VOLUME_CONTROL_ABSOLUTE;
1099                    max = mAudioManager.getStreamMaxVolume(mAudioStream);
1100                    current = mAudioManager.getStreamVolume(mAudioStream);
1101                }
1102                return new ParcelableVolumeInfo(mVolumeType, mAudioStream, type, max, current);
1103            }
1104        }
1105
1106        @Override
1107        public void adjustVolumeBy(int delta, int flags) {
1108            final long token = Binder.clearCallingIdentity();
1109            try {
1110                MediaSessionRecord.this.adjustVolumeBy(delta, flags);
1111            } finally {
1112                Binder.restoreCallingIdentity(token);
1113            }
1114        }
1115
1116        @Override
1117        public void setVolumeTo(int value, int flags) {
1118            final long token = Binder.clearCallingIdentity();
1119            try {
1120                MediaSessionRecord.this.setVolumeTo(value, flags);
1121            } finally {
1122                Binder.restoreCallingIdentity(token);
1123            }
1124        }
1125
1126        @Override
1127        public void play() throws RemoteException {
1128            mSessionCb.play();
1129        }
1130
1131        @Override
1132        public void pause() throws RemoteException {
1133            mSessionCb.pause();
1134        }
1135
1136        @Override
1137        public void stop() throws RemoteException {
1138            mSessionCb.stop();
1139        }
1140
1141        @Override
1142        public void next() throws RemoteException {
1143            mSessionCb.next();
1144        }
1145
1146        @Override
1147        public void previous() throws RemoteException {
1148            mSessionCb.previous();
1149        }
1150
1151        @Override
1152        public void fastForward() throws RemoteException {
1153            mSessionCb.fastForward();
1154        }
1155
1156        @Override
1157        public void rewind() throws RemoteException {
1158            mSessionCb.rewind();
1159        }
1160
1161        @Override
1162        public void seekTo(long pos) throws RemoteException {
1163            mSessionCb.seekTo(pos);
1164        }
1165
1166        @Override
1167        public void rate(Rating rating) throws RemoteException {
1168            mSessionCb.rate(rating);
1169        }
1170
1171
1172        @Override
1173        public MediaMetadata getMetadata() {
1174            return mMetadata;
1175        }
1176
1177        @Override
1178        public PlaybackState getPlaybackState() {
1179            return getStateWithUpdatedPosition();
1180        }
1181
1182        @Override
1183        public int getRatingType() {
1184            return mRatingType;
1185        }
1186
1187        @Override
1188        public boolean isTransportControlEnabled() {
1189            return MediaSessionRecord.this.isTransportControlEnabled();
1190        }
1191
1192        @Override
1193        public void showRoutePicker() {
1194            mService.showRoutePickerForSession(MediaSessionRecord.this);
1195        }
1196    }
1197
1198    private class MessageHandler extends Handler {
1199        private static final int MSG_UPDATE_METADATA = 1;
1200        private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
1201        private static final int MSG_UPDATE_ROUTE = 3;
1202        private static final int MSG_SEND_EVENT = 4;
1203        private static final int MSG_UPDATE_ROUTE_FILTERS = 5;
1204        private static final int MSG_SEND_COMMAND = 6;
1205        private static final int MSG_UPDATE_SESSION_STATE = 7;
1206        private static final int MSG_UPDATE_VOLUME = 8;
1207
1208        public MessageHandler(Looper looper) {
1209            super(looper);
1210        }
1211        @Override
1212        public void handleMessage(Message msg) {
1213            switch (msg.what) {
1214                case MSG_UPDATE_METADATA:
1215                    pushMetadataUpdate();
1216                    break;
1217                case MSG_UPDATE_PLAYBACK_STATE:
1218                    pushPlaybackStateUpdate();
1219                    break;
1220                case MSG_UPDATE_ROUTE:
1221                    pushRouteUpdate();
1222                    break;
1223                case MSG_SEND_EVENT:
1224                    pushEvent((String) msg.obj, msg.getData());
1225                    break;
1226                case MSG_SEND_COMMAND:
1227                    Pair<RouteCommand, ResultReceiver> cmd =
1228                            (Pair<RouteCommand, ResultReceiver>) msg.obj;
1229                    pushRouteCommand(cmd.first, cmd.second);
1230                    break;
1231                case MSG_UPDATE_SESSION_STATE:
1232                    // TODO add session state
1233                    break;
1234                case MSG_UPDATE_VOLUME:
1235                    pushVolumeUpdate();
1236                    break;
1237            }
1238        }
1239
1240        public void post(int what) {
1241            post(what, null);
1242        }
1243
1244        public void post(int what, Object obj) {
1245            obtainMessage(what, obj).sendToTarget();
1246        }
1247
1248        public void post(int what, Object obj, Bundle data) {
1249            Message msg = obtainMessage(what, obj);
1250            msg.setData(data);
1251            msg.sendToTarget();
1252        }
1253    }
1254
1255}
1256