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