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