MediaSession.java revision 07c7077c54717dbbf2c401ea32d00fa6df6d77c6
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 android.media.session;
18
19import android.content.Intent;
20import android.media.Rating;
21import android.media.session.ISessionController;
22import android.media.session.ISession;
23import android.media.session.ISessionCallback;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.Looper;
27import android.os.Message;
28import android.os.RemoteException;
29import android.os.ResultReceiver;
30import android.text.TextUtils;
31import android.util.ArrayMap;
32import android.util.Log;
33
34import java.lang.ref.WeakReference;
35import java.util.ArrayList;
36import java.util.List;
37
38/**
39 * Allows interaction with media controllers, media routes, volume keys, media
40 * buttons, and transport controls.
41 * <p>
42 * A MediaSession should be created when an app wants to publish media playback
43 * information or negotiate with a media route. In general an app only needs one
44 * session for all playback, though multiple sessions can be created for sending
45 * media to multiple routes or to provide finer grain controls of media.
46 * <p>
47 * A MediaSession is created by calling
48 * {@link SessionManager#createSession(String)}. Once a session is created
49 * apps that have the MEDIA_CONTENT_CONTROL permission can interact with the
50 * session through {@link SessionManager#getActiveSessions()}. The owner of
51 * the session may also use {@link #getSessionToken()} to allow apps without
52 * this permission to create a {@link SessionController} to interact with this
53 * session.
54 * <p>
55 * To receive commands, media keys, and other events a Callback must be set with
56 * {@link #addCallback(Callback)}.
57 * <p>
58 * When an app is finished performing playback it must call {@link #release()}
59 * to clean up the session and notify any controllers.
60 * <p>
61 * MediaSession objects are thread safe
62 */
63public final class Session {
64    private static final String TAG = "Session";
65
66    private static final int MSG_MEDIA_BUTTON = 1;
67    private static final int MSG_COMMAND = 2;
68    private static final int MSG_ROUTE_CHANGE = 3;
69    private static final int MSG_ROUTE_CONNECTED = 4;
70
71    private static final String KEY_COMMAND = "command";
72    private static final String KEY_EXTRAS = "extras";
73    private static final String KEY_CALLBACK = "callback";
74
75    private final Object mLock = new Object();
76
77    private final SessionToken mSessionToken;
78    private final ISession mBinder;
79    private final CallbackStub mCbStub;
80
81    private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>();
82    // TODO route interfaces
83    private final ArrayMap<String, RouteInterface.EventListener> mInterfaceListeners
84            = new ArrayMap<String, RouteInterface.EventListener>();
85
86    private TransportPerformer mPerformer;
87    private Route mRoute;
88
89    private boolean mPublished = false;;
90
91    /**
92     * @hide
93     */
94    public Session(ISession binder, CallbackStub cbStub) {
95        mBinder = binder;
96        mCbStub = cbStub;
97        ISessionController controllerBinder = null;
98        try {
99            controllerBinder = mBinder.getController();
100        } catch (RemoteException e) {
101            throw new RuntimeException("Dead object in MediaSessionController constructor: ", e);
102        }
103        mSessionToken = new SessionToken(controllerBinder);
104    }
105
106    /**
107     * Set the callback to receive updates on.
108     *
109     * @param callback The callback object
110     */
111    public void addCallback(Callback callback) {
112        addCallback(callback, null);
113    }
114
115    /**
116     * Add a callback to receive updates for the MediaSession. This includes
117     * events like route updates, media buttons, and focus changes.
118     *
119     * @param callback The callback to receive updates on.
120     * @param handler The handler that events should be posted on.
121     */
122    public void addCallback(Callback callback, Handler handler) {
123        if (callback == null) {
124            throw new IllegalArgumentException("Callback cannot be null");
125        }
126        synchronized (mLock) {
127            if (getHandlerForCallbackLocked(callback) != null) {
128                Log.w(TAG, "Callback is already added, ignoring");
129                return;
130            }
131            if (handler == null) {
132                handler = new Handler();
133            }
134            MessageHandler msgHandler = new MessageHandler(handler.getLooper(), callback);
135            mCallbacks.add(msgHandler);
136        }
137    }
138
139    /**
140     * Remove a callback. It will no longer receive updates.
141     *
142     * @param callback The callback to remove.
143     */
144    public void removeCallback(Callback callback) {
145        synchronized (mLock) {
146            removeCallbackLocked(callback);
147        }
148    }
149
150    /**
151     * Start using a TransportPerformer with this media session. This must be
152     * called before calling publish and cannot be called more than once.
153     * Calling this will allow MediaControllers to retrieve a
154     * TransportController.
155     *
156     * @see TransportController
157     * @return The TransportPerformer created for this session
158     */
159    public TransportPerformer setTransportPerformerEnabled() {
160        if (mPerformer != null) {
161            throw new IllegalStateException("setTransportPerformer can only be called once.");
162        }
163        if (mPublished) {
164            throw new IllegalStateException("setTransportPerformer cannot be called after publish");
165        }
166
167        mPerformer = new TransportPerformer(mBinder);
168        try {
169            mBinder.setTransportPerformerEnabled();
170        } catch (RemoteException e) {
171            Log.wtf(TAG, "Failure in setTransportPerformerEnabled.", e);
172        }
173        return mPerformer;
174    }
175
176    /**
177     * Retrieves the TransportPerformer used by this session. If called before
178     * {@link #setTransportPerformerEnabled} null will be returned.
179     *
180     * @return The TransportPerformer associated with this session or null
181     */
182    public TransportPerformer getTransportPerformer() {
183        return mPerformer;
184    }
185
186    /**
187     * Call after you have finished setting up the session. This will make it
188     * available to listeners and begin pushing updates to MediaControllers.
189     * This can only be called once.
190     */
191    public void publish() {
192        if (mPublished) {
193            throw new RuntimeException("publish() may only be called once.");
194        }
195        try {
196            mBinder.publish();
197        } catch (RemoteException e) {
198            Log.wtf(TAG, "Failure in publish.", e);
199        }
200        mPublished = true;
201    }
202
203    /**
204     * Send a proprietary event to all MediaControllers listening to this
205     * Session. It's up to the Controller/Session owner to determine the meaning
206     * of any events.
207     *
208     * @param event The name of the event to send
209     * @param extras Any extras included with the event
210     */
211    public void sendEvent(String event, Bundle extras) {
212        if (TextUtils.isEmpty(event)) {
213            throw new IllegalArgumentException("event cannot be null or empty");
214        }
215        try {
216            mBinder.sendEvent(event, extras);
217        } catch (RemoteException e) {
218            Log.wtf(TAG, "Error sending event", e);
219        }
220    }
221
222    /**
223     * This must be called when an app has finished performing playback. If
224     * playback is expected to start again shortly the session can be left open,
225     * but it must be released if your activity or service is being destroyed.
226     */
227    public void release() {
228        try {
229            mBinder.destroy();
230        } catch (RemoteException e) {
231            Log.wtf(TAG, "Error releasing session: ", e);
232        }
233    }
234
235    /**
236     * Retrieve a token object that can be used by apps to create a
237     * {@link SessionController} for interacting with this session. The owner of
238     * the session is responsible for deciding how to distribute these tokens.
239     *
240     * @return A token that can be used to create a MediaController for this
241     *         session
242     */
243    public SessionToken getSessionToken() {
244        return mSessionToken;
245    }
246
247    /**
248     * Connect to the current route using the specified request.
249     * <p>
250     * Connection updates will be sent to the callback's
251     * {@link Callback#onRouteConnected(Route)} and
252     * {@link Callback#onRouteDisconnected(Route, int)} methods. If the
253     * connection fails {@link Callback#onRouteDisconnected(Route, int)}
254     * will be called.
255     * <p>
256     * If you already have a connection to this route it will be disconnected
257     * before the new connection is established. TODO add an easy way to compare
258     * MediaRouteOptions.
259     *
260     * @param route The route the app is trying to connect to.
261     * @param request The connection request to use.
262     */
263    public void connect(RouteInfo route, RouteOptions request) {
264        if (route == null) {
265            throw new IllegalArgumentException("Must specify the route");
266        }
267        if (request == null) {
268            throw new IllegalArgumentException("Must specify the connection request");
269        }
270        try {
271            mBinder.connectToRoute(route, request);
272        } catch (RemoteException e) {
273            Log.wtf(TAG, "Error starting connection to route", e);
274        }
275    }
276
277    /**
278     * Disconnect from the current route. After calling you will be switched
279     * back to the default route.
280     *
281     * @param route The route to disconnect from.
282     */
283    public void disconnect(RouteInfo route) {
284        // TODO
285    }
286
287    /**
288     * Set the list of route options your app is interested in connecting to. It
289     * will be used for picking valid routes.
290     *
291     * @param options The set of route options your app may use to connect.
292     */
293    public void setRouteOptions(List<RouteOptions> options) {
294        try {
295            mBinder.setRouteOptions(options);
296        } catch (RemoteException e) {
297            Log.wtf(TAG, "Error setting route options.", e);
298        }
299    }
300
301    /**
302     * @hide
303     * TODO allow multiple listeners for the same interface, allow removal
304     */
305    public void addInterfaceListener(String iface,
306            RouteInterface.EventListener listener) {
307        mInterfaceListeners.put(iface, listener);
308    }
309
310    /**
311     * @hide
312     */
313    public boolean sendRouteCommand(RouteCommand command, ResultReceiver cb) {
314        try {
315            mBinder.sendRouteCommand(command, cb);
316        } catch (RemoteException e) {
317            Log.wtf(TAG, "Error sending command to route.", e);
318            return false;
319        }
320        return true;
321    }
322
323    private MessageHandler getHandlerForCallbackLocked(Callback cb) {
324        if (cb == null) {
325            throw new IllegalArgumentException("Callback cannot be null");
326        }
327        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
328            MessageHandler handler = mCallbacks.get(i);
329            if (cb == handler.mCallback) {
330                return handler;
331            }
332        }
333        return null;
334    }
335
336    private boolean removeCallbackLocked(Callback cb) {
337        if (cb == null) {
338            throw new IllegalArgumentException("Callback cannot be null");
339        }
340        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
341            MessageHandler handler = mCallbacks.get(i);
342            if (cb == handler.mCallback) {
343                mCallbacks.remove(i);
344                return true;
345            }
346        }
347        return false;
348    }
349
350    private void postCommand(String command, Bundle extras, ResultReceiver resultCb) {
351        Command cmd = new Command(command, extras, resultCb);
352        synchronized (mLock) {
353            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
354                mCallbacks.get(i).post(MSG_COMMAND, cmd);
355            }
356        }
357    }
358
359    private void postMediaButton(Intent mediaButtonIntent) {
360        synchronized (mLock) {
361            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
362                mCallbacks.get(i).post(MSG_MEDIA_BUTTON, mediaButtonIntent);
363            }
364        }
365    }
366
367    private void postRequestRouteChange(RouteInfo route) {
368        synchronized (mLock) {
369            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
370                mCallbacks.get(i).post(MSG_ROUTE_CHANGE, route);
371            }
372        }
373    }
374
375    private void postRouteConnected(RouteInfo route, RouteOptions options) {
376        synchronized (mLock) {
377            mRoute = new Route(route, options, this);
378            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
379                mCallbacks.get(i).post(MSG_ROUTE_CONNECTED, mRoute);
380            }
381        }
382    }
383
384    /**
385     * Receives commands or updates from controllers and routes. An app can
386     * specify what commands and buttons it supports by setting them on the
387     * MediaSession (TODO).
388     */
389    public abstract static class Callback {
390
391        public Callback() {
392        }
393
394        /**
395         * Called when a media button is pressed and this session has the
396         * highest priority or a controller sends a media button event to the
397         * session. TODO determine if using Intents identical to the ones
398         * RemoteControlClient receives is useful
399         * <p>
400         * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a
401         * KeyEvent in {@link Intent#EXTRA_KEY_EVENT}
402         *
403         * @param mediaButtonIntent an intent containing the KeyEvent as an
404         *            extra
405         */
406        public void onMediaButton(Intent mediaButtonIntent) {
407        }
408
409        /**
410         * Called when a controller has sent a custom command to this session.
411         * The owner of the session may handle custom commands but is not
412         * required to.
413         *
414         * @param command
415         * @param extras optional
416         */
417        public void onCommand(String command, Bundle extras, ResultReceiver cb) {
418        }
419
420        /**
421         * Called when the user has selected a different route to connect to.
422         * The app is responsible for connecting to the new route and migrating
423         * ongoing playback if necessary.
424         *
425         * @param route
426         */
427        public void onRequestRouteChange(RouteInfo route) {
428        }
429
430        /**
431         * Called when a route has successfully connected. Calls to the route
432         * are now valid.
433         *
434         * @param route The route that was connected
435         */
436        public void onRouteConnected(Route route) {
437        }
438
439        /**
440         * Called when a route was disconnected. Further calls to the route will
441         * fail. If available a reason for being disconnected will be provided.
442         * <p>
443         * Valid reasons are:
444         * <ul>
445         * </ul>
446         *
447         * @param route The route that disconnected
448         * @param reason The reason for the disconnect
449         */
450        public void onRouteDisconnected(Route route, int reason) {
451        }
452    }
453
454    /**
455     * @hide
456     */
457    public static class CallbackStub extends ISessionCallback.Stub {
458        private WeakReference<Session> mMediaSession;
459
460        public void setMediaSession(Session session) {
461            mMediaSession = new WeakReference<Session>(session);
462        }
463
464        @Override
465        public void onCommand(String command, Bundle extras, ResultReceiver cb)
466                throws RemoteException {
467            Session session = mMediaSession.get();
468            if (session != null) {
469                session.postCommand(command, extras, cb);
470            }
471        }
472
473        @Override
474        public void onMediaButton(Intent mediaButtonIntent) throws RemoteException {
475            Session session = mMediaSession.get();
476            if (session != null) {
477                session.postMediaButton(mediaButtonIntent);
478            }
479        }
480
481        @Override
482        public void onRequestRouteChange(RouteInfo route) throws RemoteException {
483            Session session = mMediaSession.get();
484            if (session != null) {
485                session.postRequestRouteChange(route);
486            }
487        }
488
489        @Override
490        public void onRouteConnected(RouteInfo route, RouteOptions options) {
491            Session session = mMediaSession.get();
492            if (session != null) {
493                session.postRouteConnected(route, options);
494            }
495        }
496
497        @Override
498        public void onPlay() throws RemoteException {
499            Session session = mMediaSession.get();
500            if (session != null) {
501                TransportPerformer tp = session.getTransportPerformer();
502                if (tp != null) {
503                    tp.onPlay();
504                }
505            }
506        }
507
508        @Override
509        public void onPause() throws RemoteException {
510            Session session = mMediaSession.get();
511            if (session != null) {
512                TransportPerformer tp = session.getTransportPerformer();
513                if (tp != null) {
514                    tp.onPause();
515                }
516            }
517        }
518
519        @Override
520        public void onStop() throws RemoteException {
521            Session session = mMediaSession.get();
522            if (session != null) {
523                TransportPerformer tp = session.getTransportPerformer();
524                if (tp != null) {
525                    tp.onStop();
526                }
527            }
528        }
529
530        @Override
531        public void onNext() throws RemoteException {
532            Session session = mMediaSession.get();
533            if (session != null) {
534                TransportPerformer tp = session.getTransportPerformer();
535                if (tp != null) {
536                    tp.onNext();
537                }
538            }
539        }
540
541        @Override
542        public void onPrevious() throws RemoteException {
543            Session session = mMediaSession.get();
544            if (session != null) {
545                TransportPerformer tp = session.getTransportPerformer();
546                if (tp != null) {
547                    tp.onPrevious();
548                }
549            }
550        }
551
552        @Override
553        public void onFastForward() throws RemoteException {
554            Session session = mMediaSession.get();
555            if (session != null) {
556                TransportPerformer tp = session.getTransportPerformer();
557                if (tp != null) {
558                    tp.onFastForward();
559                }
560            }
561        }
562
563        @Override
564        public void onRewind() throws RemoteException {
565            Session session = mMediaSession.get();
566            if (session != null) {
567                TransportPerformer tp = session.getTransportPerformer();
568                if (tp != null) {
569                    tp.onRewind();
570                }
571            }
572        }
573
574        @Override
575        public void onSeekTo(long pos) throws RemoteException {
576            Session session = mMediaSession.get();
577            if (session != null) {
578                TransportPerformer tp = session.getTransportPerformer();
579                if (tp != null) {
580                    tp.onSeekTo(pos);
581                }
582            }
583        }
584
585        @Override
586        public void onRate(Rating rating) throws RemoteException {
587            Session session = mMediaSession.get();
588            if (session != null) {
589                TransportPerformer tp = session.getTransportPerformer();
590                if (tp != null) {
591                    tp.onRate(rating);
592                }
593            }
594        }
595
596        @Override
597        public void onRouteEvent(RouteEvent event) throws RemoteException {
598            Session session = mMediaSession.get();
599            if (session != null) {
600                RouteInterface.EventListener iface
601                        = session.mInterfaceListeners.get(event.getIface());
602                Log.d(TAG, "Received route event on iface " + event.getIface() + ". Listener is "
603                        + iface);
604                if (iface != null) {
605                    iface.onEvent(event.getEvent(), event.getExtras());
606                }
607            }
608        }
609
610        @Override
611        public void onRouteStateChange(int state) throws RemoteException {
612            // TODO
613
614        }
615
616    }
617
618    private class MessageHandler extends Handler {
619        private Session.Callback mCallback;
620
621        public MessageHandler(Looper looper, Session.Callback callback) {
622            super(looper, null, true);
623            mCallback = callback;
624        }
625
626        @Override
627        public void handleMessage(Message msg) {
628            synchronized (mLock) {
629                if (mCallback == null) {
630                    return;
631                }
632                switch (msg.what) {
633                    case MSG_MEDIA_BUTTON:
634                        mCallback.onMediaButton((Intent) msg.obj);
635                        break;
636                    case MSG_COMMAND:
637                        Command cmd = (Command) msg.obj;
638                        mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
639                        break;
640                    case MSG_ROUTE_CHANGE:
641                        mCallback.onRequestRouteChange((RouteInfo) msg.obj);
642                        break;
643                    case MSG_ROUTE_CONNECTED:
644                        mCallback.onRouteConnected((Route) msg.obj);
645                        break;
646                }
647            }
648        }
649
650        public void post(int what, Object obj) {
651            obtainMessage(what, obj).sendToTarget();
652        }
653    }
654
655    private static final class Command {
656        public final String command;
657        public final Bundle extras;
658        public final ResultReceiver stub;
659
660        public Command(String command, Bundle extras, ResultReceiver stub) {
661            this.command = command;
662            this.extras = extras;
663            this.stub = stub;
664        }
665    }
666}
667