MediaSession.java revision 19c9518f6a817d53d5234de0020313cab6950b2f
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.annotation.NonNull;
20import android.annotation.Nullable;
21import android.app.PendingIntent;
22import android.content.ComponentName;
23import android.content.Intent;
24import android.media.AudioManager;
25import android.media.MediaMetadata;
26import android.media.Rating;
27import android.media.VolumeProvider;
28import android.media.session.ISessionController;
29import android.media.session.ISession;
30import android.media.session.ISessionCallback;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.Looper;
34import android.os.Message;
35import android.os.RemoteException;
36import android.os.ResultReceiver;
37import android.text.TextUtils;
38import android.util.ArrayMap;
39import android.util.Log;
40
41import java.lang.ref.WeakReference;
42import java.util.ArrayList;
43import java.util.List;
44
45/**
46 * Allows interaction with media controllers, volume keys, media buttons, and
47 * transport controls.
48 * <p>
49 * A MediaSession should be created when an app wants to publish media playback
50 * information or handle media keys. In general an app only needs one session
51 * for all playback, though multiple sessions can be created to provide finer
52 * grain controls of media.
53 * <p>
54 * Once a session is created the owner of the session may pass its
55 * {@link #getSessionToken() session token} to other processes to allow them to
56 * create a {@link MediaController} to interact with the session.
57 * <p>
58 * To receive commands, media keys, and other events a {@link Callback} must be
59 * set with {@link #addCallback(Callback)}. To receive transport control
60 * commands a {@link TransportControlsCallback} must be set with
61 * {@link #addTransportControlsCallback}.
62 * <p>
63 * When an app is finished performing playback it must call {@link #release()}
64 * to clean up the session and notify any controllers.
65 * <p>
66 * MediaSession objects are thread safe.
67 */
68public final class MediaSession {
69    private static final String TAG = "MediaSession";
70
71    /**
72     * Set this flag on the session to indicate that it can handle media button
73     * events.
74     */
75    public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0;
76
77    /**
78     * Set this flag on the session to indicate that it handles transport
79     * control commands through a {@link TransportControlsCallback}.
80     * The callback can be retrieved by calling {@link #addTransportControlsCallback}.
81     */
82    public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1;
83
84    /**
85     * System only flag for a session that needs to have priority over all other
86     * sessions. This flag ensures this session will receive media button events
87     * regardless of the current ordering in the system.
88     *
89     * @hide
90     */
91    public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16;
92
93    /**
94     * Indicates the session was disconnected because the user that the session
95     * belonged to is stopping.
96     *
97     * @hide
98     */
99    public static final int DISCONNECT_REASON_USER_STOPPING = 1;
100
101    /**
102     * Indicates the session was disconnected because the provider disconnected
103     * the route.
104     * @hide
105     */
106    public static final int DISCONNECT_REASON_PROVIDER_DISCONNECTED = 2;
107
108    /**
109     * Indicates the session was disconnected because the route has changed.
110     * @hide
111     */
112    public static final int DISCONNECT_REASON_ROUTE_CHANGED = 3;
113
114    /**
115     * Indicates the session was disconnected because the session owner
116     * requested it disconnect.
117     * @hide
118     */
119    public static final int DISCONNECT_REASON_SESSION_DISCONNECTED = 4;
120
121    /**
122     * Indicates the session was disconnected because it was destroyed.
123     * @hide
124     */
125    public static final int DISCONNECT_REASON_SESSION_DESTROYED = 5;
126
127    /**
128     * The session uses local playback.
129     */
130    public static final int PLAYBACK_TYPE_LOCAL = 1;
131
132    /**
133     * The session uses remote playback.
134     */
135    public static final int PLAYBACK_TYPE_REMOTE = 2;
136
137    private final Object mLock = new Object();
138
139    private final MediaSessionToken mSessionToken;
140    private final ISession mBinder;
141    private final CallbackStub mCbStub;
142
143    private final ArrayList<CallbackMessageHandler> mCallbacks
144            = new ArrayList<CallbackMessageHandler>();
145    private final ArrayList<TransportMessageHandler> mTransportCallbacks
146            = new ArrayList<TransportMessageHandler>();
147    // TODO route interfaces
148    private final ArrayMap<String, RouteInterface.EventListener> mInterfaceListeners
149            = new ArrayMap<String, RouteInterface.EventListener>();
150
151    private Route mRoute;
152    private VolumeProvider mVolumeProvider;
153
154    private boolean mActive = false;
155
156    /**
157     * @hide
158     */
159    public MediaSession(ISession binder, CallbackStub cbStub) {
160        mBinder = binder;
161        mCbStub = cbStub;
162        ISessionController controllerBinder = null;
163        try {
164            controllerBinder = mBinder.getController();
165        } catch (RemoteException e) {
166            throw new RuntimeException("Dead object in MediaSessionController constructor: ", e);
167        }
168        mSessionToken = new MediaSessionToken(controllerBinder);
169    }
170
171    /**
172     * Add a callback to receive updates on for the MediaSession. This includes
173     * media button and volume events. The caller's thread will be used to post
174     * events.
175     *
176     * @param callback The callback object
177     */
178    public void addCallback(@NonNull Callback callback) {
179        addCallback(callback, null);
180    }
181
182    /**
183     * Add a callback to receive updates for the MediaSession. This includes
184     * media button and volume events.
185     *
186     * @param callback The callback to receive updates on.
187     * @param handler The handler that events should be posted on.
188     */
189    public void addCallback(@NonNull Callback callback, @Nullable Handler handler) {
190        if (callback == null) {
191            throw new IllegalArgumentException("Callback cannot be null");
192        }
193        synchronized (mLock) {
194            if (getHandlerForCallbackLocked(callback) != null) {
195                Log.w(TAG, "Callback is already added, ignoring");
196                return;
197            }
198            if (handler == null) {
199                handler = new Handler();
200            }
201            CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
202                    callback);
203            mCallbacks.add(msgHandler);
204        }
205    }
206
207    /**
208     * Remove a callback. It will no longer receive updates.
209     *
210     * @param callback The callback to remove.
211     */
212    public void removeCallback(@NonNull Callback callback) {
213        synchronized (mLock) {
214            removeCallbackLocked(callback);
215        }
216    }
217
218    /**
219     * Set an intent for launching UI for this Session. This can be used as a
220     * quick link to an ongoing media screen.
221     *
222     * @param pi The intent to launch to show UI for this Session.
223     */
224    public void setLaunchPendingIntent(@Nullable PendingIntent pi) {
225        // TODO
226    }
227
228    /**
229     * Set a media button event receiver component to use to restart playback
230     * after an app has been stopped.
231     *
232     * @param mbr The receiver component to send the media button event to.
233     * @hide
234     */
235    public void setMediaButtonReceiver(@Nullable ComponentName mbr) {
236        try {
237            mBinder.setMediaButtonReceiver(mbr);
238        } catch (RemoteException e) {
239            Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e);
240        }
241    }
242
243    /**
244     * Set any flags for the session.
245     *
246     * @param flags The flags to set for this session.
247     */
248    public void setFlags(int flags) {
249        try {
250            mBinder.setFlags(flags);
251        } catch (RemoteException e) {
252            Log.wtf(TAG, "Failure in setFlags.", e);
253        }
254    }
255
256    /**
257     * Set the stream this session is playing on. This will affect the system's
258     * volume handling for this session. If {@link #setPlaybackToRemote} was
259     * previously called it will stop receiving volume commands and the system
260     * will begin sending volume changes to the appropriate stream.
261     * <p>
262     * By default sessions are on {@link AudioManager#STREAM_MUSIC}.
263     *
264     * @param stream The {@link AudioManager} stream this session is playing on.
265     */
266    public void setPlaybackToLocal(int stream) {
267        try {
268            mBinder.configureVolumeHandling(PLAYBACK_TYPE_LOCAL, stream, 0);
269        } catch (RemoteException e) {
270            Log.wtf(TAG, "Failure in setPlaybackToLocal.", e);
271        }
272    }
273
274    /**
275     * Configure this session to use remote volume handling. This must be called
276     * to receive volume button events, otherwise the system will adjust the
277     * current stream volume for this session. If {@link #setPlaybackToLocal}
278     * was previously called that stream will stop receiving volume changes for
279     * this session.
280     *
281     * @param volumeProvider The provider that will handle volume changes. May
282     *            not be null.
283     */
284    public void setPlaybackToRemote(@NonNull VolumeProvider volumeProvider) {
285        if (volumeProvider == null) {
286            throw new IllegalArgumentException("volumeProvider may not be null!");
287        }
288        mVolumeProvider = volumeProvider;
289        volumeProvider.setCallback(new VolumeProvider.Callback() {
290            @Override
291            public void onVolumeChanged(VolumeProvider volumeProvider) {
292                notifyRemoteVolumeChanged(volumeProvider);
293            }
294        });
295
296        try {
297            mBinder.configureVolumeHandling(PLAYBACK_TYPE_REMOTE, volumeProvider.getVolumeControl(),
298                    volumeProvider.getMaxVolume());
299        } catch (RemoteException e) {
300            Log.wtf(TAG, "Failure in setPlaybackToRemote.", e);
301        }
302    }
303
304    /**
305     * Set if this session is currently active and ready to receive commands. If
306     * set to false your session's controller may not be discoverable. You must
307     * set the session to active before it can start receiving media button
308     * events or transport commands.
309     *
310     * @param active Whether this session is active or not.
311     */
312    public void setActive(boolean active) {
313        if (mActive == active) {
314            return;
315        }
316        try {
317            mBinder.setActive(active);
318            mActive = active;
319        } catch (RemoteException e) {
320            Log.wtf(TAG, "Failure in setActive.", e);
321        }
322    }
323
324    /**
325     * Get the current active state of this session.
326     *
327     * @return True if the session is active, false otherwise.
328     */
329    public boolean isActive() {
330        return mActive;
331    }
332
333    /**
334     * Send a proprietary event to all MediaControllers listening to this
335     * Session. It's up to the Controller/Session owner to determine the meaning
336     * of any events.
337     *
338     * @param event The name of the event to send
339     * @param extras Any extras included with the event
340     */
341    public void sendSessionEvent(@NonNull String event, @Nullable Bundle extras) {
342        if (TextUtils.isEmpty(event)) {
343            throw new IllegalArgumentException("event cannot be null or empty");
344        }
345        try {
346            mBinder.sendEvent(event, extras);
347        } catch (RemoteException e) {
348            Log.wtf(TAG, "Error sending event", e);
349        }
350    }
351
352    /**
353     * This must be called when an app has finished performing playback. If
354     * playback is expected to start again shortly the session can be left open,
355     * but it must be released if your activity or service is being destroyed.
356     */
357    public void release() {
358        try {
359            mBinder.destroy();
360        } catch (RemoteException e) {
361            Log.wtf(TAG, "Error releasing session: ", e);
362        }
363    }
364
365    /**
366     * Retrieve a token object that can be used by apps to create a
367     * {@link MediaController} for interacting with this session. The owner of
368     * the session is responsible for deciding how to distribute these tokens.
369     *
370     * @return A token that can be used to create a MediaController for this
371     *         session
372     */
373    public @NonNull MediaSessionToken getSessionToken() {
374        return mSessionToken;
375    }
376
377    /**
378     * Connect to the current route using the specified request.
379     * <p>
380     * Connection updates will be sent to the callback's
381     * {@link Callback#onRouteConnected(Route)} and
382     * {@link Callback#onRouteDisconnected(Route, int)} methods. If the
383     * connection fails {@link Callback#onRouteDisconnected(Route, int)} will be
384     * called.
385     * <p>
386     * If you already have a connection to this route it will be disconnected
387     * before the new connection is established. TODO add an easy way to compare
388     * MediaRouteOptions.
389     *
390     * @param route The route the app is trying to connect to.
391     * @param request The connection request to use.
392     * @hide
393     */
394    public void connect(RouteInfo route, RouteOptions request) {
395        if (route == null) {
396            throw new IllegalArgumentException("Must specify the route");
397        }
398        if (request == null) {
399            throw new IllegalArgumentException("Must specify the connection request");
400        }
401        try {
402            mBinder.connectToRoute(route, request);
403        } catch (RemoteException e) {
404            Log.wtf(TAG, "Error starting connection to route", e);
405        }
406    }
407
408    /**
409     * Disconnect from the current route. After calling you will be switched
410     * back to the default route.
411     *
412     * @hide
413     */
414    public void disconnect() {
415        if (mRoute != null) {
416            try {
417                mBinder.disconnectFromRoute(mRoute.getRouteInfo());
418            } catch (RemoteException e) {
419                Log.wtf(TAG, "Error disconnecting from route");
420            }
421        }
422    }
423
424    /**
425     * Set the list of route options your app is interested in connecting to. It
426     * will be used for picking valid routes.
427     *
428     * @param options The set of route options your app may use to connect.
429     * @hide
430     */
431    public void setRouteOptions(List<RouteOptions> options) {
432        try {
433            mBinder.setRouteOptions(options);
434        } catch (RemoteException e) {
435            Log.wtf(TAG, "Error setting route options.", e);
436        }
437    }
438
439    /**
440     * @hide
441     * TODO allow multiple listeners for the same interface, allow removal
442     */
443    public void addInterfaceListener(String iface,
444            RouteInterface.EventListener listener) {
445        mInterfaceListeners.put(iface, listener);
446    }
447
448    /**
449     * @hide
450     */
451    public boolean sendRouteCommand(RouteCommand command, ResultReceiver cb) {
452        try {
453            mBinder.sendRouteCommand(command, cb);
454        } catch (RemoteException e) {
455            Log.wtf(TAG, "Error sending command to route.", e);
456            return false;
457        }
458        return true;
459    }
460
461    /**
462     * Add a callback to receive transport controls on, such as play, rewind, or
463     * fast forward.
464     *
465     * @param callback The callback object
466     */
467    public void addTransportControlsCallback(@NonNull TransportControlsCallback callback) {
468        addTransportControlsCallback(callback, null);
469    }
470
471    /**
472     * Add a callback to receive transport controls on, such as play, rewind, or
473     * fast forward. The updates will be posted to the specified handler. If no
474     * handler is provided they will be posted to the caller's thread.
475     *
476     * @param callback The callback to receive updates on
477     * @param handler The handler to post the updates on
478     */
479    public void addTransportControlsCallback(@NonNull TransportControlsCallback callback,
480            @Nullable Handler handler) {
481        if (callback == null) {
482            throw new IllegalArgumentException("Callback cannot be null");
483        }
484        synchronized (mLock) {
485            if (getTransportControlsHandlerForCallbackLocked(callback) != null) {
486                Log.w(TAG, "Callback is already added, ignoring");
487                return;
488            }
489            if (handler == null) {
490                handler = new Handler();
491            }
492            TransportMessageHandler msgHandler = new TransportMessageHandler(handler.getLooper(),
493                    callback);
494            mTransportCallbacks.add(msgHandler);
495        }
496    }
497
498    /**
499     * Stop receiving transport controls on the specified callback. If an update
500     * has already been posted you may still receive it after this call returns.
501     *
502     * @param callback The callback to stop receiving updates on
503     */
504    public void removeTransportControlsCallback(@NonNull TransportControlsCallback callback) {
505        if (callback == null) {
506            throw new IllegalArgumentException("Callback cannot be null");
507        }
508        synchronized (mLock) {
509            removeTransportControlsCallbackLocked(callback);
510        }
511    }
512
513    /**
514     * Update the current playback state.
515     *
516     * @param state The current state of playback
517     */
518    public void setPlaybackState(@Nullable PlaybackState state) {
519        try {
520            mBinder.setPlaybackState(state);
521        } catch (RemoteException e) {
522            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
523        }
524    }
525
526    /**
527     * Update the current metadata. New metadata can be created using
528     * {@link android.media.MediaMetadata.Builder}.
529     *
530     * @param metadata The new metadata
531     */
532    public void setMetadata(@Nullable MediaMetadata metadata) {
533        try {
534            mBinder.setMetadata(metadata);
535        } catch (RemoteException e) {
536            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
537        }
538    }
539
540    /**
541     * Notify the system that the remote volume changed.
542     *
543     * @param provider The provider that is handling volume changes.
544     * @hide
545     */
546    public void notifyRemoteVolumeChanged(VolumeProvider provider) {
547        if (provider == null || provider != mVolumeProvider) {
548            Log.w(TAG, "Received update from stale volume provider");
549            return;
550        }
551        try {
552            mBinder.setCurrentVolume(provider.onGetCurrentVolume());
553        } catch (RemoteException e) {
554            Log.e(TAG, "Error in notifyVolumeChanged", e);
555        }
556    }
557
558    private void dispatchPlay() {
559        postToTransportCallbacks(TransportMessageHandler.MSG_PLAY);
560    }
561
562    private void dispatchPause() {
563        postToTransportCallbacks(TransportMessageHandler.MSG_PAUSE);
564    }
565
566    private void dispatchStop() {
567        postToTransportCallbacks(TransportMessageHandler.MSG_STOP);
568    }
569
570    private void dispatchNext() {
571        postToTransportCallbacks(TransportMessageHandler.MSG_NEXT);
572    }
573
574    private void dispatchPrevious() {
575        postToTransportCallbacks(TransportMessageHandler.MSG_PREVIOUS);
576    }
577
578    private void dispatchFastForward() {
579        postToTransportCallbacks(TransportMessageHandler.MSG_FAST_FORWARD);
580    }
581
582    private void dispatchRewind() {
583        postToTransportCallbacks(TransportMessageHandler.MSG_REWIND);
584    }
585
586    private void dispatchSeekTo(long pos) {
587        postToTransportCallbacks(TransportMessageHandler.MSG_SEEK_TO, pos);
588    }
589
590    private void dispatchRate(Rating rating) {
591        postToTransportCallbacks(TransportMessageHandler.MSG_RATE, rating);
592    }
593
594    private TransportMessageHandler getTransportControlsHandlerForCallbackLocked(
595            TransportControlsCallback callback) {
596        for (int i = mTransportCallbacks.size() - 1; i >= 0; i--) {
597            TransportMessageHandler handler = mTransportCallbacks.get(i);
598            if (callback == handler.mCallback) {
599                return handler;
600            }
601        }
602        return null;
603    }
604
605    private boolean removeTransportControlsCallbackLocked(TransportControlsCallback callback) {
606        for (int i = mTransportCallbacks.size() - 1; i >= 0; i--) {
607            if (callback == mTransportCallbacks.get(i).mCallback) {
608                mTransportCallbacks.remove(i);
609                return true;
610            }
611        }
612        return false;
613    }
614
615    private void postToTransportCallbacks(int what, Object obj) {
616        synchronized (mLock) {
617            for (int i = mTransportCallbacks.size() - 1; i >= 0; i--) {
618                mTransportCallbacks.get(i).post(what, obj);
619            }
620        }
621    }
622
623    private void postToTransportCallbacks(int what) {
624        postToTransportCallbacks(what, null);
625    }
626
627    private CallbackMessageHandler getHandlerForCallbackLocked(Callback cb) {
628        if (cb == null) {
629            throw new IllegalArgumentException("Callback cannot be null");
630        }
631        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
632            CallbackMessageHandler handler = mCallbacks.get(i);
633            if (cb == handler.mCallback) {
634                return handler;
635            }
636        }
637        return null;
638    }
639
640    private boolean removeCallbackLocked(Callback cb) {
641        if (cb == null) {
642            throw new IllegalArgumentException("Callback cannot be null");
643        }
644        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
645            CallbackMessageHandler handler = mCallbacks.get(i);
646            if (cb == handler.mCallback) {
647                mCallbacks.remove(i);
648                return true;
649            }
650        }
651        return false;
652    }
653
654    private void postCommand(String command, Bundle extras, ResultReceiver resultCb) {
655        Command cmd = new Command(command, extras, resultCb);
656        synchronized (mLock) {
657            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
658                mCallbacks.get(i).post(CallbackMessageHandler.MSG_COMMAND, cmd);
659            }
660        }
661    }
662
663    private void postMediaButton(Intent mediaButtonIntent) {
664        synchronized (mLock) {
665            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
666                mCallbacks.get(i).post(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent);
667            }
668        }
669    }
670
671    private void postRequestRouteChange(RouteInfo route) {
672        synchronized (mLock) {
673            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
674                mCallbacks.get(i).post(CallbackMessageHandler.MSG_ROUTE_CHANGE, route);
675            }
676        }
677    }
678
679    private void postRouteConnected(RouteInfo route, RouteOptions options) {
680        synchronized (mLock) {
681            mRoute = new Route(route, options, this);
682            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
683                mCallbacks.get(i).post(CallbackMessageHandler.MSG_ROUTE_CONNECTED, mRoute);
684            }
685        }
686    }
687
688    private void postRouteDisconnected(RouteInfo route, int reason) {
689        synchronized (mLock) {
690            if (mRoute != null && TextUtils.equals(mRoute.getRouteInfo().getId(), route.getId())) {
691                for (int i = mCallbacks.size() - 1; i >= 0; i--) {
692                    mCallbacks.get(i).post(CallbackMessageHandler.MSG_ROUTE_DISCONNECTED, mRoute,
693                            reason);
694                }
695            }
696        }
697    }
698
699    /**
700     * Receives generic commands or updates from controllers and the system.
701     * Callbacks may be registered using {@link #addCallback}.
702     */
703    public abstract static class Callback {
704
705        public Callback() {
706        }
707
708        /**
709         * Called when a media button is pressed and this session has the
710         * highest priority or a controller sends a media button event to the
711         * session. TODO determine if using Intents identical to the ones
712         * RemoteControlClient receives is useful
713         * <p>
714         * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a
715         * KeyEvent in {@link Intent#EXTRA_KEY_EVENT}
716         *
717         * @param mediaButtonIntent an intent containing the KeyEvent as an
718         *            extra
719         */
720        public void onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
721        }
722
723        /**
724         * Called when a controller has sent a custom command to this session.
725         * The owner of the session may handle custom commands but is not
726         * required to.
727         *
728         * @param command The command name.
729         * @param extras Optional parameters for the command, may be null.
730         * @param cb A result receiver to which a result may be sent by the command, may be null.
731         */
732        public void onControlCommand(@NonNull String command, @Nullable Bundle extras,
733                @Nullable ResultReceiver cb) {
734        }
735
736        /**
737         * Called when the user has selected a different route to connect to.
738         * The app is responsible for connecting to the new route and migrating
739         * ongoing playback if necessary.
740         *
741         * @param route
742         * @hide
743         */
744        public void onRequestRouteChange(RouteInfo route) {
745        }
746
747        /**
748         * Called when a route has successfully connected. Calls to the route
749         * are now valid.
750         *
751         * @param route The route that was connected
752         * @hide
753         */
754        public void onRouteConnected(Route route) {
755        }
756
757        /**
758         * Called when a route was disconnected. Further calls to the route will
759         * fail. If available a reason for being disconnected will be provided.
760         * <p>
761         * Valid reasons are:
762         * <ul>
763         * <li>{@link #DISCONNECT_REASON_USER_STOPPING}</li>
764         * <li>{@link #DISCONNECT_REASON_PROVIDER_DISCONNECTED}</li>
765         * <li>{@link #DISCONNECT_REASON_ROUTE_CHANGED}</li>
766         * <li>{@link #DISCONNECT_REASON_SESSION_DISCONNECTED}</li>
767         * <li>{@link #DISCONNECT_REASON_SESSION_DESTROYED}</li>
768         * </ul>
769         *
770         * @param route The route that disconnected
771         * @param reason The reason for the disconnect
772         * @hide
773         */
774        public void onRouteDisconnected(Route route, int reason) {
775        }
776    }
777
778    /**
779     * Receives transport control commands. Callbacks may be registered using
780     * {@link #addTransportControlsCallback}.
781     */
782    public static abstract class TransportControlsCallback {
783
784        /**
785         * Override to handle requests to begin playback.
786         */
787        public void onPlay() {
788        }
789
790        /**
791         * Override to handle requests to pause playback.
792         */
793        public void onPause() {
794        }
795
796        /**
797         * Override to handle requests to skip to the next media item.
798         */
799        public void onSkipToNext() {
800        }
801
802        /**
803         * Override to handle requests to skip to the previous media item.
804         */
805        public void onSkipToPrevious() {
806        }
807
808        /**
809         * Override to handle requests to fast forward.
810         */
811        public void onFastForward() {
812        }
813
814        /**
815         * Override to handle requests to rewind.
816         */
817        public void onRewind() {
818        }
819
820        /**
821         * Override to handle requests to stop playback.
822         */
823        public void onStop() {
824        }
825
826        /**
827         * Override to handle requests to seek to a specific position in ms.
828         *
829         * @param pos New position to move to, in milliseconds.
830         */
831        public void onSeekTo(long pos) {
832        }
833
834        /**
835         * Override to handle the item being rated.
836         *
837         * @param rating
838         */
839        public void onSetRating(@NonNull Rating rating) {
840        }
841
842        /**
843         * Report that audio focus has changed on the app. This only happens if
844         * you have indicated you have started playing with
845         * {@link #setPlaybackState}.
846         *
847         * @param focusChange The type of focus change, TBD.
848         * @hide
849         */
850        public void onRouteFocusChange(int focusChange) {
851        }
852    }
853
854    /**
855     * @hide
856     */
857    public static class CallbackStub extends ISessionCallback.Stub {
858        private WeakReference<MediaSession> mMediaSession;
859
860        public void setMediaSession(MediaSession session) {
861            mMediaSession = new WeakReference<MediaSession>(session);
862        }
863
864        @Override
865        public void onCommand(String command, Bundle extras, ResultReceiver cb)
866                throws RemoteException {
867            MediaSession session = mMediaSession.get();
868            if (session != null) {
869                session.postCommand(command, extras, cb);
870            }
871        }
872
873        @Override
874        public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber, ResultReceiver cb)
875                throws RemoteException {
876            MediaSession session = mMediaSession.get();
877            try {
878                if (session != null) {
879                    session.postMediaButton(mediaButtonIntent);
880                }
881            } finally {
882                if (cb != null) {
883                    cb.send(sequenceNumber, null);
884                }
885            }
886        }
887
888        @Override
889        public void onRequestRouteChange(RouteInfo route) throws RemoteException {
890            MediaSession session = mMediaSession.get();
891            if (session != null) {
892                session.postRequestRouteChange(route);
893            }
894        }
895
896        @Override
897        public void onRouteConnected(RouteInfo route, RouteOptions options) {
898            MediaSession session = mMediaSession.get();
899            if (session != null) {
900                session.postRouteConnected(route, options);
901            }
902        }
903
904        @Override
905        public void onRouteDisconnected(RouteInfo route, int reason) {
906            MediaSession session = mMediaSession.get();
907            if (session != null) {
908                session.postRouteDisconnected(route, reason);
909            }
910        }
911
912        @Override
913        public void onPlay() throws RemoteException {
914            MediaSession session = mMediaSession.get();
915            if (session != null) {
916                session.dispatchPlay();
917            }
918        }
919
920        @Override
921        public void onPause() throws RemoteException {
922            MediaSession session = mMediaSession.get();
923            if (session != null) {
924                session.dispatchPause();
925            }
926        }
927
928        @Override
929        public void onStop() throws RemoteException {
930            MediaSession session = mMediaSession.get();
931            if (session != null) {
932                session.dispatchStop();
933            }
934        }
935
936        @Override
937        public void onNext() throws RemoteException {
938            MediaSession session = mMediaSession.get();
939            if (session != null) {
940                session.dispatchNext();
941            }
942        }
943
944        @Override
945        public void onPrevious() throws RemoteException {
946            MediaSession session = mMediaSession.get();
947            if (session != null) {
948                session.dispatchPrevious();
949            }
950        }
951
952        @Override
953        public void onFastForward() throws RemoteException {
954            MediaSession session = mMediaSession.get();
955            if (session != null) {
956                session.dispatchFastForward();
957            }
958        }
959
960        @Override
961        public void onRewind() throws RemoteException {
962            MediaSession session = mMediaSession.get();
963            if (session != null) {
964                session.dispatchRewind();
965            }
966        }
967
968        @Override
969        public void onSeekTo(long pos) throws RemoteException {
970            MediaSession session = mMediaSession.get();
971            if (session != null) {
972                session.dispatchSeekTo(pos);
973            }
974        }
975
976        @Override
977        public void onRate(Rating rating) throws RemoteException {
978            MediaSession session = mMediaSession.get();
979            if (session != null) {
980                session.dispatchRate(rating);
981            }
982        }
983
984        @Override
985        public void onRouteEvent(RouteEvent event) throws RemoteException {
986            MediaSession session = mMediaSession.get();
987            if (session != null) {
988                RouteInterface.EventListener iface
989                        = session.mInterfaceListeners.get(event.getIface());
990                Log.d(TAG, "Received route event on iface " + event.getIface() + ". Listener is "
991                        + iface);
992                if (iface != null) {
993                    iface.onEvent(event.getEvent(), event.getExtras());
994                }
995            }
996        }
997
998        @Override
999        public void onRouteStateChange(int state) throws RemoteException {
1000            // TODO
1001        }
1002
1003        @Override
1004        public void onAdjustVolumeBy(int delta) throws RemoteException {
1005            MediaSession session = mMediaSession.get();
1006            if (session != null) {
1007                if (session.mVolumeProvider != null) {
1008                    session.mVolumeProvider.onAdjustVolumeBy(delta);
1009                }
1010            }
1011        }
1012
1013        @Override
1014        public void onSetVolumeTo(int value) throws RemoteException {
1015            MediaSession session = mMediaSession.get();
1016            if (session != null) {
1017                if (session.mVolumeProvider != null) {
1018                    session.mVolumeProvider.onSetVolumeTo(value);
1019                }
1020            }
1021        }
1022
1023    }
1024
1025    private class CallbackMessageHandler extends Handler {
1026        private static final int MSG_MEDIA_BUTTON = 1;
1027        private static final int MSG_COMMAND = 2;
1028        private static final int MSG_ROUTE_CHANGE = 3;
1029        private static final int MSG_ROUTE_CONNECTED = 4;
1030        private static final int MSG_ROUTE_DISCONNECTED = 5;
1031
1032        private MediaSession.Callback mCallback;
1033
1034        public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) {
1035            super(looper, null, true);
1036            mCallback = callback;
1037        }
1038
1039        @Override
1040        public void handleMessage(Message msg) {
1041            synchronized (mLock) {
1042                if (mCallback == null) {
1043                    return;
1044                }
1045                switch (msg.what) {
1046                    case MSG_MEDIA_BUTTON:
1047                        mCallback.onMediaButtonEvent((Intent) msg.obj);
1048                        break;
1049                    case MSG_COMMAND:
1050                        Command cmd = (Command) msg.obj;
1051                        mCallback.onControlCommand(cmd.command, cmd.extras, cmd.stub);
1052                        break;
1053                    case MSG_ROUTE_CHANGE:
1054                        mCallback.onRequestRouteChange((RouteInfo) msg.obj);
1055                        break;
1056                    case MSG_ROUTE_CONNECTED:
1057                        mCallback.onRouteConnected((Route) msg.obj);
1058                        break;
1059                    case MSG_ROUTE_DISCONNECTED:
1060                        mCallback.onRouteDisconnected((Route) msg.obj, msg.arg1);
1061                        break;
1062                }
1063            }
1064        }
1065
1066        public void post(int what, Object obj) {
1067            obtainMessage(what, obj).sendToTarget();
1068        }
1069
1070        public void post(int what, Object obj, int arg1) {
1071            obtainMessage(what, arg1, 0, obj).sendToTarget();
1072        }
1073    }
1074
1075    private static final class Command {
1076        public final String command;
1077        public final Bundle extras;
1078        public final ResultReceiver stub;
1079
1080        public Command(String command, Bundle extras, ResultReceiver stub) {
1081            this.command = command;
1082            this.extras = extras;
1083            this.stub = stub;
1084        }
1085    }
1086
1087    private class TransportMessageHandler extends Handler {
1088        private static final int MSG_PLAY = 1;
1089        private static final int MSG_PAUSE = 2;
1090        private static final int MSG_STOP = 3;
1091        private static final int MSG_NEXT = 4;
1092        private static final int MSG_PREVIOUS = 5;
1093        private static final int MSG_FAST_FORWARD = 6;
1094        private static final int MSG_REWIND = 7;
1095        private static final int MSG_SEEK_TO = 8;
1096        private static final int MSG_RATE = 9;
1097
1098        private TransportControlsCallback mCallback;
1099
1100        public TransportMessageHandler(Looper looper, TransportControlsCallback cb) {
1101            super(looper);
1102            mCallback = cb;
1103        }
1104
1105        public void post(int what, Object obj) {
1106            obtainMessage(what, obj).sendToTarget();
1107        }
1108
1109        public void post(int what) {
1110            post(what, null);
1111        }
1112
1113        @Override
1114        public void handleMessage(Message msg) {
1115            switch (msg.what) {
1116                case MSG_PLAY:
1117                    mCallback.onPlay();
1118                    break;
1119                case MSG_PAUSE:
1120                    mCallback.onPause();
1121                    break;
1122                case MSG_STOP:
1123                    mCallback.onStop();
1124                    break;
1125                case MSG_NEXT:
1126                    mCallback.onSkipToNext();
1127                    break;
1128                case MSG_PREVIOUS:
1129                    mCallback.onSkipToPrevious();
1130                    break;
1131                case MSG_FAST_FORWARD:
1132                    mCallback.onFastForward();
1133                    break;
1134                case MSG_REWIND:
1135                    mCallback.onRewind();
1136                    break;
1137                case MSG_SEEK_TO:
1138                    mCallback.onSeekTo((Long) msg.obj);
1139                    break;
1140                case MSG_RATE:
1141                    mCallback.onSetRating((Rating) msg.obj);
1142                    break;
1143            }
1144        }
1145    }
1146}
1147