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