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