MediaSessionRecord.java revision 3c45c29109d23981d8b707c809b3b43ce2e20fc3
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                if (updateTime > 0) {
686                    long position = (long) (state.getPlaybackRate()
687                            * (SystemClock.elapsedRealtime() - updateTime)) + state.getPosition();
688                    if (duration >= 0 && position > duration) {
689                        position = duration;
690                    } else if (position < 0) {
691                        position = 0;
692                    }
693                    result = new PlaybackState(state);
694                    result.setState(state.getState(), position, state.getPlaybackRate());
695                }
696            }
697        }
698        return result == null ? state : result;
699    }
700
701    private int getControllerCbIndexForCb(ISessionControllerCallback cb) {
702        IBinder binder = cb.asBinder();
703        for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
704            if (binder.equals(mControllerCallbacks.get(i).asBinder())) {
705                return i;
706            }
707        }
708        return -1;
709    }
710
711    private final RouteConnectionRecord.Listener mConnectionListener
712            = new RouteConnectionRecord.Listener() {
713        @Override
714        public void onEvent(RouteEvent event) {
715            RouteEvent eventForSession = new RouteEvent(null, event.getIface(),
716                    event.getEvent(), event.getExtras());
717            mSessionCb.sendRouteEvent(eventForSession);
718        }
719
720        @Override
721        public void disconnect() {
722            MediaSessionRecord.this.disconnect(MediaSession.DISCONNECT_REASON_PROVIDER_DISCONNECTED);
723        }
724    };
725
726    private final Runnable mClearOptimisticVolumeRunnable = new Runnable() {
727        @Override
728        public void run() {
729            boolean needUpdate = (mOptimisticVolume != mCurrentVolume);
730            mOptimisticVolume = -1;
731            if (needUpdate) {
732                pushVolumeUpdate();
733            }
734        }
735    };
736
737    private final class SessionStub extends ISession.Stub {
738        @Override
739        public void destroy() {
740            mService.destroySession(MediaSessionRecord.this);
741        }
742
743        @Override
744        public void sendEvent(String event, Bundle data) {
745            mHandler.post(MessageHandler.MSG_SEND_EVENT, event, data);
746        }
747
748        @Override
749        public ISessionController getController() {
750            return mController;
751        }
752
753        @Override
754        public void setActive(boolean active) {
755            mIsActive = active;
756            mService.updateSession(MediaSessionRecord.this);
757            mHandler.post(MessageHandler.MSG_UPDATE_SESSION_STATE);
758        }
759
760        @Override
761        public void setFlags(int flags) {
762            if ((flags & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) {
763                int pid = getCallingPid();
764                int uid = getCallingUid();
765                mService.enforcePhoneStatePermission(pid, uid);
766            }
767            mFlags = flags;
768            mHandler.post(MessageHandler.MSG_UPDATE_SESSION_STATE);
769        }
770
771        @Override
772        public void setMediaButtonReceiver(ComponentName mbr) {
773            mMediaButtonReceiver = mbr;
774        }
775
776        @Override
777        public void setMetadata(MediaMetadata metadata) {
778            mMetadata = metadata;
779            mHandler.post(MessageHandler.MSG_UPDATE_METADATA);
780        }
781
782        @Override
783        public void setPlaybackState(PlaybackState state) {
784            int oldState = mPlaybackState == null ? 0 : mPlaybackState.getState();
785            int newState = state == null ? 0 : state.getState();
786            if (MediaSession.isActiveState(oldState) && newState == PlaybackState.STATE_PAUSED) {
787                mLastActiveTime = SystemClock.elapsedRealtime();
788            }
789            mPlaybackState = state;
790            mService.onSessionPlaystateChange(MediaSessionRecord.this, oldState, newState);
791            mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);
792        }
793
794        @Override
795        public void setRatingType(int type) {
796            mRatingType = type;
797        }
798
799        @Override
800        public void sendRouteCommand(RouteCommand command, ResultReceiver cb) {
801            mHandler.post(MessageHandler.MSG_SEND_COMMAND,
802                    new Pair<RouteCommand, ResultReceiver>(command, cb));
803        }
804
805        @Override
806        public boolean setRoute(RouteInfo route) throws RemoteException {
807            // TODO decide if allowed to set route and if the route exists
808            return false;
809        }
810
811        @Override
812        public void connectToRoute(RouteInfo route, RouteOptions request)
813                throws RemoteException {
814            if (mRoute == null || !TextUtils.equals(route.getId(), mRoute.getId())) {
815                throw new RemoteException("RouteInfo does not match current route");
816            }
817            mService.connectToRoute(MediaSessionRecord.this, route, request);
818            mRequest = request;
819        }
820
821        @Override
822        public void disconnectFromRoute(RouteInfo route) {
823            if (route != null && mRoute != null
824                    && TextUtils.equals(route.getId(), mRoute.getId())) {
825                disconnect(MediaSession.DISCONNECT_REASON_SESSION_DISCONNECTED);
826            }
827        }
828
829        @Override
830        public void setRouteOptions(List<RouteOptions> options) throws RemoteException {
831            mRequests.clear();
832            for (int i = options.size() - 1; i >= 0; i--) {
833                RouteRequest request = new RouteRequest(mSessionInfo, options.get(i),
834                        false);
835                mRequests.add(request);
836            }
837        }
838
839        @Override
840        public void setCurrentVolume(int volume) {
841            mCurrentVolume = volume;
842            mHandler.post(MessageHandler.MSG_UPDATE_VOLUME);
843        }
844
845        @Override
846        public void configureVolumeHandling(int type, int arg1, int arg2) throws RemoteException {
847            boolean typeChanged = type != mVolumeType;
848            switch(type) {
849                case MediaSession.PLAYBACK_TYPE_LOCAL:
850                    mVolumeType = type;
851                    int audioStream = arg1;
852                    if (isValidStream(audioStream)) {
853                        mAudioStream = audioStream;
854                    } else {
855                        Log.e(TAG, "Cannot set stream to " + audioStream + ". Using music stream");
856                        mAudioStream = AudioManager.STREAM_MUSIC;
857                    }
858                    break;
859                case MediaSession.PLAYBACK_TYPE_REMOTE:
860                    mVolumeType = type;
861                    mVolumeControlType = arg1;
862                    mMaxVolume = arg2;
863                    break;
864                default:
865                    throw new IllegalArgumentException("Volume handling type " + type
866                            + " not recognized.");
867            }
868            if (typeChanged) {
869                mService.onSessionPlaybackTypeChanged(MediaSessionRecord.this);
870            }
871        }
872
873        private boolean isValidStream(int stream) {
874            return stream >= AudioManager.STREAM_VOICE_CALL
875                    && stream <= AudioManager.STREAM_NOTIFICATION;
876        }
877    }
878
879    class SessionCb {
880        private final ISessionCallback mCb;
881
882        public SessionCb(ISessionCallback cb) {
883            mCb = cb;
884        }
885
886        public boolean sendMediaButton(KeyEvent keyEvent, int sequenceId, ResultReceiver cb) {
887            Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
888            mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
889            try {
890                mCb.onMediaButton(mediaButtonIntent, sequenceId, cb);
891                return true;
892            } catch (RemoteException e) {
893                Slog.e(TAG, "Remote failure in sendMediaRequest.", e);
894            }
895            return false;
896        }
897
898        public void sendCommand(String command, Bundle extras, ResultReceiver cb) {
899            try {
900                mCb.onCommand(command, extras, cb);
901            } catch (RemoteException e) {
902                Slog.e(TAG, "Remote failure in sendCommand.", e);
903            }
904        }
905
906        public void sendRouteChange(RouteInfo route) {
907            try {
908                mCb.onRequestRouteChange(route);
909            } catch (RemoteException e) {
910                Slog.e(TAG, "Remote failure in sendRouteChange.", e);
911            }
912        }
913
914        public void sendRouteStateChange(int state) {
915            try {
916                mCb.onRouteStateChange(state);
917            } catch (RemoteException e) {
918                Slog.e(TAG, "Remote failure in sendRouteStateChange.", e);
919            }
920        }
921
922        public void sendRouteEvent(RouteEvent event) {
923            try {
924                mCb.onRouteEvent(event);
925            } catch (RemoteException e) {
926                Slog.e(TAG, "Remote failure in sendRouteEvent.", e);
927            }
928        }
929
930        public void sendRouteConnected() {
931            try {
932                mCb.onRouteConnected(mRoute, mRequest);
933            } catch (RemoteException e) {
934                Slog.e(TAG, "Remote failure in sendRouteStateChange.", e);
935            }
936        }
937
938        public void sendRouteDisconnected(int reason) {
939            try {
940                mCb.onRouteDisconnected(mRoute, reason);
941            } catch (RemoteException e) {
942                Slog.e(TAG, "Remote failure in sendRouteDisconnected");
943            }
944        }
945
946        public void play() {
947            try {
948                mCb.onPlay();
949            } catch (RemoteException e) {
950                Slog.e(TAG, "Remote failure in play.", e);
951            }
952        }
953
954        public void pause() {
955            try {
956                mCb.onPause();
957            } catch (RemoteException e) {
958                Slog.e(TAG, "Remote failure in pause.", e);
959            }
960        }
961
962        public void stop() {
963            try {
964                mCb.onStop();
965            } catch (RemoteException e) {
966                Slog.e(TAG, "Remote failure in stop.", e);
967            }
968        }
969
970        public void next() {
971            try {
972                mCb.onNext();
973            } catch (RemoteException e) {
974                Slog.e(TAG, "Remote failure in next.", e);
975            }
976        }
977
978        public void previous() {
979            try {
980                mCb.onPrevious();
981            } catch (RemoteException e) {
982                Slog.e(TAG, "Remote failure in previous.", e);
983            }
984        }
985
986        public void fastForward() {
987            try {
988                mCb.onFastForward();
989            } catch (RemoteException e) {
990                Slog.e(TAG, "Remote failure in fastForward.", e);
991            }
992        }
993
994        public void rewind() {
995            try {
996                mCb.onRewind();
997            } catch (RemoteException e) {
998                Slog.e(TAG, "Remote failure in rewind.", e);
999            }
1000        }
1001
1002        public void seekTo(long pos) {
1003            try {
1004                mCb.onSeekTo(pos);
1005            } catch (RemoteException e) {
1006                Slog.e(TAG, "Remote failure in seekTo.", e);
1007            }
1008        }
1009
1010        public void rate(Rating rating) {
1011            try {
1012                mCb.onRate(rating);
1013            } catch (RemoteException e) {
1014                Slog.e(TAG, "Remote failure in rate.", e);
1015            }
1016        }
1017
1018        public void adjustVolumeBy(int delta) {
1019            try {
1020                mCb.onAdjustVolumeBy(delta);
1021            } catch (RemoteException e) {
1022                Slog.e(TAG, "Remote failure in adjustVolumeBy.", e);
1023            }
1024        }
1025
1026        public void setVolumeTo(int value) {
1027            try {
1028                mCb.onSetVolumeTo(value);
1029            } catch (RemoteException e) {
1030                Slog.e(TAG, "Remote failure in adjustVolumeBy.", e);
1031            }
1032        }
1033    }
1034
1035    class ControllerStub extends ISessionController.Stub {
1036        @Override
1037        public void sendCommand(String command, Bundle extras, ResultReceiver cb)
1038                throws RemoteException {
1039            mSessionCb.sendCommand(command, extras, cb);
1040        }
1041
1042        @Override
1043        public boolean sendMediaButton(KeyEvent mediaButtonIntent) {
1044            return mSessionCb.sendMediaButton(mediaButtonIntent, 0, null);
1045        }
1046
1047        @Override
1048        public void registerCallbackListener(ISessionControllerCallback cb) {
1049            synchronized (mLock) {
1050                if (getControllerCbIndexForCb(cb) < 0) {
1051                    mControllerCallbacks.add(cb);
1052                    if (DEBUG) {
1053                        Log.d(TAG, "registering controller callback " + cb);
1054                    }
1055                }
1056            }
1057        }
1058
1059        @Override
1060        public void unregisterCallbackListener(ISessionControllerCallback cb)
1061                throws RemoteException {
1062            synchronized (mLock) {
1063                int index = getControllerCbIndexForCb(cb);
1064                if (index != -1) {
1065                    mControllerCallbacks.remove(index);
1066                }
1067                if (DEBUG) {
1068                    Log.d(TAG, "unregistering callback " + cb + ". index=" + index);
1069                }
1070            }
1071        }
1072
1073        @Override
1074        public MediaSessionInfo getSessionInfo() {
1075            return mSessionInfo;
1076        }
1077
1078        @Override
1079        public long getFlags() {
1080            return mFlags;
1081        }
1082
1083        @Override
1084        public ParcelableVolumeInfo getVolumeAttributes() {
1085            synchronized (mLock) {
1086                int type;
1087                int max;
1088                int current;
1089                if (mVolumeType == MediaSession.PLAYBACK_TYPE_REMOTE) {
1090                    type = mVolumeControlType;
1091                    max = mMaxVolume;
1092                    current = mOptimisticVolume != -1 ? mOptimisticVolume
1093                            : mCurrentVolume;
1094                } else {
1095                    type = VolumeProvider.VOLUME_CONTROL_ABSOLUTE;
1096                    max = mAudioManager.getStreamMaxVolume(mAudioStream);
1097                    current = mAudioManager.getStreamVolume(mAudioStream);
1098                }
1099                return new ParcelableVolumeInfo(mVolumeType, mAudioStream, type, max, current);
1100            }
1101        }
1102
1103        @Override
1104        public void adjustVolumeBy(int delta, int flags) {
1105            final long token = Binder.clearCallingIdentity();
1106            try {
1107                MediaSessionRecord.this.adjustVolumeBy(delta, flags);
1108            } finally {
1109                Binder.restoreCallingIdentity(token);
1110            }
1111        }
1112
1113        @Override
1114        public void setVolumeTo(int value, int flags) {
1115            final long token = Binder.clearCallingIdentity();
1116            try {
1117                MediaSessionRecord.this.setVolumeTo(value, flags);
1118            } finally {
1119                Binder.restoreCallingIdentity(token);
1120            }
1121        }
1122
1123        @Override
1124        public void play() throws RemoteException {
1125            mSessionCb.play();
1126        }
1127
1128        @Override
1129        public void pause() throws RemoteException {
1130            mSessionCb.pause();
1131        }
1132
1133        @Override
1134        public void stop() throws RemoteException {
1135            mSessionCb.stop();
1136        }
1137
1138        @Override
1139        public void next() throws RemoteException {
1140            mSessionCb.next();
1141        }
1142
1143        @Override
1144        public void previous() throws RemoteException {
1145            mSessionCb.previous();
1146        }
1147
1148        @Override
1149        public void fastForward() throws RemoteException {
1150            mSessionCb.fastForward();
1151        }
1152
1153        @Override
1154        public void rewind() throws RemoteException {
1155            mSessionCb.rewind();
1156        }
1157
1158        @Override
1159        public void seekTo(long pos) throws RemoteException {
1160            mSessionCb.seekTo(pos);
1161        }
1162
1163        @Override
1164        public void rate(Rating rating) throws RemoteException {
1165            mSessionCb.rate(rating);
1166        }
1167
1168
1169        @Override
1170        public MediaMetadata getMetadata() {
1171            return mMetadata;
1172        }
1173
1174        @Override
1175        public PlaybackState getPlaybackState() {
1176            return getStateWithUpdatedPosition();
1177        }
1178
1179        @Override
1180        public int getRatingType() {
1181            return mRatingType;
1182        }
1183
1184        @Override
1185        public boolean isTransportControlEnabled() {
1186            return MediaSessionRecord.this.isTransportControlEnabled();
1187        }
1188
1189        @Override
1190        public void showRoutePicker() {
1191            mService.showRoutePickerForSession(MediaSessionRecord.this);
1192        }
1193    }
1194
1195    private class MessageHandler extends Handler {
1196        private static final int MSG_UPDATE_METADATA = 1;
1197        private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
1198        private static final int MSG_UPDATE_ROUTE = 3;
1199        private static final int MSG_SEND_EVENT = 4;
1200        private static final int MSG_UPDATE_ROUTE_FILTERS = 5;
1201        private static final int MSG_SEND_COMMAND = 6;
1202        private static final int MSG_UPDATE_SESSION_STATE = 7;
1203        private static final int MSG_UPDATE_VOLUME = 8;
1204
1205        public MessageHandler(Looper looper) {
1206            super(looper);
1207        }
1208        @Override
1209        public void handleMessage(Message msg) {
1210            switch (msg.what) {
1211                case MSG_UPDATE_METADATA:
1212                    pushMetadataUpdate();
1213                    break;
1214                case MSG_UPDATE_PLAYBACK_STATE:
1215                    pushPlaybackStateUpdate();
1216                    break;
1217                case MSG_UPDATE_ROUTE:
1218                    pushRouteUpdate();
1219                    break;
1220                case MSG_SEND_EVENT:
1221                    pushEvent((String) msg.obj, msg.getData());
1222                    break;
1223                case MSG_SEND_COMMAND:
1224                    Pair<RouteCommand, ResultReceiver> cmd =
1225                            (Pair<RouteCommand, ResultReceiver>) msg.obj;
1226                    pushRouteCommand(cmd.first, cmd.second);
1227                    break;
1228                case MSG_UPDATE_SESSION_STATE:
1229                    // TODO add session state
1230                    break;
1231                case MSG_UPDATE_VOLUME:
1232                    pushVolumeUpdate();
1233                    break;
1234            }
1235        }
1236
1237        public void post(int what) {
1238            post(what, null);
1239        }
1240
1241        public void post(int what, Object obj) {
1242            obtainMessage(what, obj).sendToTarget();
1243        }
1244
1245        public void post(int what, Object obj, Bundle data) {
1246            Message msg = obtainMessage(what, obj);
1247            msg.setData(data);
1248            msg.sendToTarget();
1249        }
1250    }
1251
1252}
1253