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