MediaSessionRecord.java revision f1372428f2df781c71c71caa2f6a4db6f847cf10
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.media;
18
19import android.content.Intent;
20import android.media.routeprovider.RouteRequest;
21import android.media.session.ISessionController;
22import android.media.session.ISessionControllerCallback;
23import android.media.session.ISession;
24import android.media.session.ISessionCallback;
25import android.media.session.SessionController;
26import android.media.session.MediaMetadata;
27import android.media.session.RouteCommand;
28import android.media.session.RouteInfo;
29import android.media.session.RouteOptions;
30import android.media.session.RouteEvent;
31import android.media.session.Session;
32import android.media.session.SessionInfo;
33import android.media.session.RouteInterface;
34import android.media.session.PlaybackState;
35import android.media.Rating;
36import android.os.Bundle;
37import android.os.Handler;
38import android.os.IBinder;
39import android.os.Looper;
40import android.os.Message;
41import android.os.RemoteException;
42import android.os.ResultReceiver;
43import android.os.SystemClock;
44import android.text.TextUtils;
45import android.util.Log;
46import android.util.Pair;
47import android.util.Slog;
48import android.view.KeyEvent;
49
50import java.util.ArrayList;
51import java.util.List;
52import java.util.UUID;
53
54/**
55 * This is the system implementation of a Session. Apps will interact with the
56 * MediaSession wrapper class instead.
57 */
58public class MediaSessionRecord implements IBinder.DeathRecipient {
59    private static final String TAG = "MediaSessionRecord";
60
61    private final MessageHandler mHandler;
62
63    private final int mPid;
64    private final SessionInfo mSessionInfo;
65    private final String mTag;
66    private final ControllerStub mController;
67    private final SessionStub mSession;
68    private final SessionCb mSessionCb;
69    private final MediaSessionService mService;
70
71    private final Object mLock = new Object();
72    private final ArrayList<ISessionControllerCallback> mControllerCallbacks =
73            new ArrayList<ISessionControllerCallback>();
74    private final ArrayList<RouteRequest> mRequests = new ArrayList<RouteRequest>();
75
76    private boolean mTransportPerformerEnabled = false;
77    private RouteInfo mRoute;
78    private RouteOptions mRequest;
79    private RouteConnectionRecord mConnection;
80    // TODO define a RouteState class with relevant info
81    private int mRouteState;
82
83    // TransportPerformer fields
84
85    private MediaMetadata mMetadata;
86    private PlaybackState mPlaybackState;
87    private int mRatingType;
88    // End TransportPerformer fields
89
90    private boolean mIsPublished = false;
91
92    public MediaSessionRecord(int pid, String packageName, ISessionCallback cb, String tag,
93            MediaSessionService service, Handler handler) {
94        mPid = pid;
95        mSessionInfo = new SessionInfo(UUID.randomUUID().toString(), packageName);
96        mTag = tag;
97        mController = new ControllerStub();
98        mSession = new SessionStub();
99        mSessionCb = new SessionCb(cb);
100        mService = service;
101        mHandler = new MessageHandler(handler.getLooper());
102    }
103
104    /**
105     * Get the binder for the {@link Session}.
106     *
107     * @return The session binder apps talk to.
108     */
109    public ISession getSessionBinder() {
110        return mSession;
111    }
112
113    /**
114     * Get the binder for the {@link SessionController}.
115     *
116     * @return The controller binder apps talk to.
117     */
118    public ISessionController getControllerBinder() {
119        return mController;
120    }
121
122    /**
123     * Get the set of route requests this session is interested in.
124     *
125     * @return The list of RouteRequests
126     */
127    public List<RouteRequest> getRouteRequests() {
128        return mRequests;
129    }
130
131    /**
132     * Get the route this session is currently on.
133     *
134     * @return The route the session is on.
135     */
136    public RouteInfo getRoute() {
137        return mRoute;
138    }
139
140    /**
141     * Get the info for this session.
142     *
143     * @return Info that identifies this session.
144     */
145    public SessionInfo getSessionInfo() {
146        return mSessionInfo;
147    }
148
149    /**
150     * Set the selected route. This does not connect to the route, just notifies
151     * the app that a new route has been selected.
152     *
153     * @param route The route that was selected.
154     */
155    public void selectRoute(RouteInfo route) {
156        synchronized (mLock) {
157            if (route != mRoute) {
158                if (mConnection != null) {
159                    mConnection.disconnect();
160                    mConnection = null;
161                }
162            }
163            mRoute = route;
164        }
165        mSessionCb.sendRouteChange(route);
166    }
167
168    /**
169     * Update the state of the route this session is using and notify the
170     * session.
171     *
172     * @param state The new state of the route.
173     */
174    public void setRouteState(int state) {
175        mSessionCb.sendRouteStateChange(state);
176    }
177
178    /**
179     * Send an event to this session from the route it is using.
180     *
181     * @param event The event to send.
182     */
183    public void sendRouteEvent(RouteEvent event) {
184        mSessionCb.sendRouteEvent(event);
185    }
186
187    /**
188     * Set the connection to use for the selected route and notify the app it is
189     * now connected.
190     *
191     * @param route The route the connection is to.
192     * @param request The request that was used to connect.
193     * @param connection The connection to the route.
194     * @return True if this connection is still valid, false if it is stale.
195     */
196    public boolean setRouteConnected(RouteInfo route, RouteOptions request,
197            RouteConnectionRecord connection) {
198        synchronized (mLock) {
199            if (mRoute == null || !TextUtils.equals(route.getId(), mRoute.getId())) {
200                Log.w(TAG, "setRouteConnected: connected route is stale");
201                // TODO figure out disconnection path
202                return false;
203            }
204            if (request != mRequest) {
205                Log.w(TAG, "setRouteConnected: connection request is stale");
206                // TODO figure out disconnection path
207                return false;
208            }
209            mConnection = connection;
210            mConnection.setListener(mConnectionListener);
211            mSessionCb.sendRouteConnected();
212        }
213        return true;
214    }
215
216    /**
217     * Check if this session has been published by the app yet.
218     *
219     * @return True if it has been published, false otherwise.
220     */
221    public boolean isPublished() {
222        return mIsPublished;
223    }
224
225    @Override
226    public void binderDied() {
227        mService.sessionDied(this);
228    }
229
230    private void onDestroy() {
231        mService.destroySession(this);
232    }
233
234    private void pushPlaybackStateUpdate() {
235        synchronized (mLock) {
236            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
237                ISessionControllerCallback cb = mControllerCallbacks.get(i);
238                try {
239                    cb.onPlaybackStateChanged(mPlaybackState);
240                } catch (RemoteException e) {
241                    Log.w(TAG, "Removing dead callback in pushPlaybackStateUpdate.", e);
242                    mControllerCallbacks.remove(i);
243                }
244            }
245        }
246    }
247
248    private void pushMetadataUpdate() {
249        synchronized (mLock) {
250            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
251                ISessionControllerCallback cb = mControllerCallbacks.get(i);
252                try {
253                    cb.onMetadataChanged(mMetadata);
254                } catch (RemoteException e) {
255                    Log.w(TAG, "Removing dead callback in pushMetadataUpdate.", e);
256                    mControllerCallbacks.remove(i);
257                }
258            }
259        }
260    }
261
262    private void pushRouteUpdate() {
263        synchronized (mLock) {
264            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
265                ISessionControllerCallback cb = mControllerCallbacks.get(i);
266                try {
267                    cb.onRouteChanged(mRoute);
268                } catch (RemoteException e) {
269                    Log.w(TAG, "Removing dead callback in pushRouteUpdate.", e);
270                    mControllerCallbacks.remove(i);
271                }
272            }
273        }
274    }
275
276    private void pushEvent(String event, Bundle data) {
277        synchronized (mLock) {
278            for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
279                ISessionControllerCallback cb = mControllerCallbacks.get(i);
280                try {
281                    cb.onEvent(event, data);
282                } catch (RemoteException e) {
283                    Log.w(TAG, "Error with callback in pushEvent.", e);
284                }
285            }
286        }
287    }
288
289    private void pushRouteCommand(RouteCommand command, ResultReceiver cb) {
290        synchronized (mLock) {
291            if (mRoute == null || !TextUtils.equals(command.getRouteInfo(), mRoute.getId())) {
292                if (cb != null) {
293                    cb.send(RouteInterface.RESULT_ROUTE_IS_STALE, null);
294                    return;
295                }
296            }
297            if (mConnection != null) {
298                mConnection.sendCommand(command, cb);
299            } else if (cb != null) {
300                cb.send(RouteInterface.RESULT_NOT_CONNECTED, null);
301            }
302        }
303    }
304
305    private PlaybackState getStateWithUpdatedPosition() {
306        PlaybackState state = mPlaybackState;
307        long duration = -1;
308        if (mMetadata != null && mMetadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
309            duration = mMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
310        }
311        PlaybackState result = null;
312        if (state != null) {
313            if (state.getState() == PlaybackState.PLAYSTATE_PLAYING
314                    || state.getState() == PlaybackState.PLAYSTATE_FAST_FORWARDING
315                    || state.getState() == PlaybackState.PLAYSTATE_REWINDING) {
316                long updateTime = state.getLastPositionUpdateTime();
317                if (updateTime > 0) {
318                    long position = (long) (state.getRate()
319                            * (SystemClock.elapsedRealtime() - updateTime)) + state.getPosition();
320                    if (duration >= 0 && position > duration) {
321                        position = duration;
322                    } else if (position < 0) {
323                        position = 0;
324                    }
325                    result = new PlaybackState(state);
326                    result.setState(state.getState(), position, state.getRate());
327                }
328            }
329        }
330        return result == null ? state : result;
331    }
332
333    private final RouteConnectionRecord.Listener mConnectionListener
334            = new RouteConnectionRecord.Listener() {
335        @Override
336        public void onEvent(RouteEvent event) {
337            RouteEvent eventForSession = new RouteEvent(null, event.getIface(),
338                    event.getEvent(), event.getExtras());
339            mSessionCb.sendRouteEvent(eventForSession);
340        }
341
342        @Override
343        public void disconnect() {
344            // TODO
345        }
346    };
347
348    private final class SessionStub extends ISession.Stub {
349        @Override
350        public void destroy() {
351            onDestroy();
352        }
353
354        @Override
355        public void sendEvent(String event, Bundle data) {
356            mHandler.post(MessageHandler.MSG_SEND_EVENT, event, data);
357        }
358
359        @Override
360        public ISessionController getController() {
361            return mController;
362        }
363
364        @Override
365        public void publish() {
366            mIsPublished = true; // TODO push update to service
367        }
368        @Override
369        public void setTransportPerformerEnabled() {
370            mTransportPerformerEnabled = true;
371        }
372
373        @Override
374        public void setMetadata(MediaMetadata metadata) {
375            mMetadata = metadata;
376            mHandler.post(MessageHandler.MSG_UPDATE_METADATA);
377        }
378
379        @Override
380        public void setPlaybackState(PlaybackState state) {
381            mPlaybackState = state;
382            mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE);
383        }
384
385        @Override
386        public void setRatingType(int type) {
387            mRatingType = type;
388        }
389
390        @Override
391        public void sendRouteCommand(RouteCommand command, ResultReceiver cb) {
392            mHandler.post(MessageHandler.MSG_SEND_COMMAND,
393                    new Pair<RouteCommand, ResultReceiver>(command, cb));
394        }
395
396        @Override
397        public boolean setRoute(RouteInfo route) throws RemoteException {
398            // TODO decide if allowed to set route and if the route exists
399            return false;
400        }
401
402        @Override
403        public void connectToRoute(RouteInfo route, RouteOptions request)
404                throws RemoteException {
405            if (mRoute == null || !TextUtils.equals(route.getId(), mRoute.getId())) {
406                throw new RemoteException("RouteInfo does not match current route");
407            }
408            mService.connectToRoute(MediaSessionRecord.this, route, request);
409            mRequest = request;
410        }
411
412        @Override
413        public void setRouteOptions(List<RouteOptions> options) throws RemoteException {
414            mRequests.clear();
415            for (int i = options.size() - 1; i >= 0; i--) {
416                RouteRequest request = new RouteRequest(mSessionInfo, options.get(i),
417                        false);
418                mRequests.add(request);
419            }
420        }
421    }
422
423    class SessionCb {
424        private final ISessionCallback mCb;
425
426        public SessionCb(ISessionCallback cb) {
427            mCb = cb;
428        }
429
430        public void sendMediaButton(KeyEvent keyEvent) {
431            Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
432            mediaButtonIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent);
433            try {
434                mCb.onMediaButton(mediaButtonIntent);
435            } catch (RemoteException e) {
436                Slog.e(TAG, "Remote failure in sendMediaRequest.", e);
437            }
438        }
439
440        public void sendCommand(String command, Bundle extras, ResultReceiver cb) {
441            try {
442                mCb.onCommand(command, extras, cb);
443            } catch (RemoteException e) {
444                Slog.e(TAG, "Remote failure in sendCommand.", e);
445            }
446        }
447
448        public void sendRouteChange(RouteInfo route) {
449            try {
450                mCb.onRequestRouteChange(route);
451            } catch (RemoteException e) {
452                Slog.e(TAG, "Remote failure in sendRouteChange.", e);
453            }
454        }
455
456        public void sendRouteStateChange(int state) {
457            try {
458                mCb.onRouteStateChange(state);
459            } catch (RemoteException e) {
460                Slog.e(TAG, "Remote failure in sendRouteStateChange.", e);
461            }
462        }
463
464        public void sendRouteEvent(RouteEvent event) {
465            try {
466                mCb.onRouteEvent(event);
467            } catch (RemoteException e) {
468                Slog.e(TAG, "Remote failure in sendRouteEvent.", e);
469            }
470        }
471
472        public void sendRouteConnected() {
473            try {
474                mCb.onRouteConnected(mRoute, mRequest);
475            } catch (RemoteException e) {
476                Slog.e(TAG, "Remote failure in sendRouteStateChange.", e);
477            }
478        }
479
480        public void play() {
481            try {
482                mCb.onPlay();
483            } catch (RemoteException e) {
484                Slog.e(TAG, "Remote failure in play.", e);
485            }
486        }
487
488        public void pause() {
489            try {
490                mCb.onPause();
491            } catch (RemoteException e) {
492                Slog.e(TAG, "Remote failure in pause.", e);
493            }
494        }
495
496        public void stop() {
497            try {
498                mCb.onStop();
499            } catch (RemoteException e) {
500                Slog.e(TAG, "Remote failure in stop.", e);
501            }
502        }
503
504        public void next() {
505            try {
506                mCb.onNext();
507            } catch (RemoteException e) {
508                Slog.e(TAG, "Remote failure in next.", e);
509            }
510        }
511
512        public void previous() {
513            try {
514                mCb.onPrevious();
515            } catch (RemoteException e) {
516                Slog.e(TAG, "Remote failure in previous.", e);
517            }
518        }
519
520        public void fastForward() {
521            try {
522                mCb.onFastForward();
523            } catch (RemoteException e) {
524                Slog.e(TAG, "Remote failure in fastForward.", e);
525            }
526        }
527
528        public void rewind() {
529            try {
530                mCb.onRewind();
531            } catch (RemoteException e) {
532                Slog.e(TAG, "Remote failure in rewind.", e);
533            }
534        }
535
536        public void seekTo(long pos) {
537            try {
538                mCb.onSeekTo(pos);
539            } catch (RemoteException e) {
540                Slog.e(TAG, "Remote failure in seekTo.", e);
541            }
542        }
543
544        public void rate(Rating rating) {
545            try {
546                mCb.onRate(rating);
547            } catch (RemoteException e) {
548                Slog.e(TAG, "Remote failure in rate.", e);
549            }
550        }
551    }
552
553    class ControllerStub extends ISessionController.Stub {
554        @Override
555        public void sendCommand(String command, Bundle extras, ResultReceiver cb)
556                throws RemoteException {
557            mSessionCb.sendCommand(command, extras, cb);
558        }
559
560        @Override
561        public void sendMediaButton(KeyEvent mediaButtonIntent) {
562            mSessionCb.sendMediaButton(mediaButtonIntent);
563        }
564
565        @Override
566        public void registerCallbackListener(ISessionControllerCallback cb) {
567            synchronized (mLock) {
568                if (!mControllerCallbacks.contains(cb)) {
569                    mControllerCallbacks.add(cb);
570                }
571            }
572        }
573
574        @Override
575        public void unregisterCallbackListener(ISessionControllerCallback cb)
576                throws RemoteException {
577            synchronized (mLock) {
578                mControllerCallbacks.remove(cb);
579            }
580        }
581
582        @Override
583        public void play() throws RemoteException {
584            mSessionCb.play();
585        }
586
587        @Override
588        public void pause() throws RemoteException {
589            mSessionCb.pause();
590        }
591
592        @Override
593        public void stop() throws RemoteException {
594            mSessionCb.stop();
595        }
596
597        @Override
598        public void next() throws RemoteException {
599            mSessionCb.next();
600        }
601
602        @Override
603        public void previous() throws RemoteException {
604            mSessionCb.previous();
605        }
606
607        @Override
608        public void fastForward() throws RemoteException {
609            mSessionCb.fastForward();
610        }
611
612        @Override
613        public void rewind() throws RemoteException {
614            mSessionCb.rewind();
615        }
616
617        @Override
618        public void seekTo(long pos) throws RemoteException {
619            mSessionCb.seekTo(pos);
620        }
621
622        @Override
623        public void rate(Rating rating) throws RemoteException {
624            mSessionCb.rate(rating);
625        }
626
627
628        @Override
629        public MediaMetadata getMetadata() {
630            return mMetadata;
631        }
632
633        @Override
634        public PlaybackState getPlaybackState() {
635            return getStateWithUpdatedPosition();
636        }
637
638        @Override
639        public int getRatingType() {
640            return mRatingType;
641        }
642
643        @Override
644        public boolean isTransportControlEnabled() {
645            return mTransportPerformerEnabled;
646        }
647
648        @Override
649        public void showRoutePicker() {
650            mService.showRoutePickerForSession(MediaSessionRecord.this);
651        }
652    }
653
654    private class MessageHandler extends Handler {
655        private static final int MSG_UPDATE_METADATA = 1;
656        private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
657        private static final int MSG_UPDATE_ROUTE = 3;
658        private static final int MSG_SEND_EVENT = 4;
659        private static final int MSG_UPDATE_ROUTE_FILTERS = 5;
660        private static final int MSG_SEND_COMMAND = 6;
661
662        public MessageHandler(Looper looper) {
663            super(looper);
664        }
665        @Override
666        public void handleMessage(Message msg) {
667            switch (msg.what) {
668                case MSG_UPDATE_METADATA:
669                    pushMetadataUpdate();
670                    break;
671                case MSG_UPDATE_PLAYBACK_STATE:
672                    pushPlaybackStateUpdate();
673                    break;
674                case MSG_UPDATE_ROUTE:
675                    pushRouteUpdate();
676                    break;
677                case MSG_SEND_EVENT:
678                    pushEvent((String) msg.obj, msg.getData());
679                    break;
680                case MSG_SEND_COMMAND:
681                    Pair<RouteCommand, ResultReceiver> cmd =
682                            (Pair<RouteCommand, ResultReceiver>) msg.obj;
683                    pushRouteCommand(cmd.first, cmd.second);
684                    break;
685            }
686        }
687
688        public void post(int what) {
689            post(what, null);
690        }
691
692        public void post(int what, Object obj) {
693            obtainMessage(what, obj).sendToTarget();
694        }
695
696        public void post(int what, Object obj, Bundle data) {
697            Message msg = obtainMessage(what, obj);
698            msg.setData(data);
699            msg.sendToTarget();
700        }
701    }
702
703}
704