MediaSession.java revision 3625bf72cb8bcf3c7f8f8cd8d708d7206824cc62
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.IntDef;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.app.Activity;
23import android.app.PendingIntent;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.ParceledListSlice;
27import android.media.AudioAttributes;
28import android.media.MediaDescription;
29import android.media.MediaMetadata;
30import android.media.Rating;
31import android.media.VolumeProvider;
32import android.net.Uri;
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.service.media.MediaBrowserService;
43import android.text.TextUtils;
44import android.util.Log;
45import android.view.KeyEvent;
46
47import java.lang.annotation.Retention;
48import java.lang.annotation.RetentionPolicy;
49import java.lang.ref.WeakReference;
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 #setCallback(Callback)} and {@link #setActive(boolean)
67 * setActive(true)} must be called.
68 * <p>
69 * When an app is finished performing playback it must call {@link #release()}
70 * to clean up the session and notify any controllers.
71 * <p>
72 * MediaSession objects are thread safe.
73 */
74public final class MediaSession {
75    private static final String TAG = "MediaSession";
76
77    /**
78     * Set this flag on the session to indicate that it can handle media button
79     * events.
80     */
81    public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0;
82
83    /**
84     * Set this flag on the session to indicate that it handles transport
85     * control commands through its {@link Callback}.
86     */
87    public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1;
88
89    /**
90     * System only flag for a session that needs to have priority over all other
91     * sessions. This flag ensures this session will receive media button events
92     * regardless of the current ordering in the system.
93     *
94     * @hide
95     */
96    public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16;
97
98    /** @hide */
99    @Retention(RetentionPolicy.SOURCE)
100    @IntDef(flag = true, value = {
101            FLAG_HANDLES_MEDIA_BUTTONS,
102            FLAG_HANDLES_TRANSPORT_CONTROLS,
103            FLAG_EXCLUSIVE_GLOBAL_PRIORITY })
104    public @interface SessionFlags { }
105
106    private final Object mLock = new Object();
107
108    private final MediaSession.Token mSessionToken;
109    private final MediaController mController;
110    private final ISession mBinder;
111    private final CallbackStub mCbStub;
112
113    private CallbackMessageHandler mCallback;
114    private VolumeProvider mVolumeProvider;
115    private PlaybackState mPlaybackState;
116
117    private boolean mActive = false;
118
119    /**
120     * Creates a new session. The session will automatically be registered with
121     * the system but will not be published until {@link #setActive(boolean)
122     * setActive(true)} is called. You must call {@link #release()} when
123     * finished with the session.
124     *
125     * @param context The context to use to create the session.
126     * @param tag A short name for debugging purposes.
127     */
128    public MediaSession(@NonNull Context context, @NonNull String tag) {
129        this(context, tag, UserHandle.myUserId());
130    }
131
132    /**
133     * Creates a new session as the specified user. To create a session as a
134     * user other than your own you must hold the
135     * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL}
136     * permission.
137     *
138     * @param context The context to use to create the session.
139     * @param tag A short name for debugging purposes.
140     * @param userId The user id to create the session as.
141     * @hide
142     */
143    public MediaSession(@NonNull Context context, @NonNull String tag, int userId) {
144        if (context == null) {
145            throw new IllegalArgumentException("context cannot be null.");
146        }
147        if (TextUtils.isEmpty(tag)) {
148            throw new IllegalArgumentException("tag cannot be null or empty");
149        }
150        mCbStub = new CallbackStub(this);
151        MediaSessionManager manager = (MediaSessionManager) context
152                .getSystemService(Context.MEDIA_SESSION_SERVICE);
153        try {
154            mBinder = manager.createSession(mCbStub, tag, userId);
155            mSessionToken = new Token(mBinder.getController());
156            mController = new MediaController(context, mSessionToken);
157        } catch (RemoteException e) {
158            throw new RuntimeException("Remote error creating session.", e);
159        }
160    }
161
162    /**
163     * Set the callback to receive updates for the MediaSession. This includes
164     * media button events and transport controls. The caller's thread will be
165     * used to post updates.
166     * <p>
167     * Set the callback to null to stop receiving updates.
168     *
169     * @param callback The callback object
170     */
171    public void setCallback(@Nullable Callback callback) {
172        setCallback(callback, null);
173    }
174
175    /**
176     * Set the callback to receive updates for the MediaSession. This includes
177     * media button events and transport controls.
178     * <p>
179     * Set the callback to null to stop receiving updates.
180     *
181     * @param callback The callback to receive updates on.
182     * @param handler The handler that events should be posted on.
183     */
184    public void setCallback(@Nullable Callback callback, @Nullable Handler handler) {
185        if (callback == null) {
186            mCallback = null;
187            return;
188        }
189        synchronized (mLock) {
190            if (mCallback != null && mCallback.mCallback == callback) {
191                Log.w(TAG, "Tried to set same callback, ignoring");
192                return;
193            }
194            if (handler == null) {
195                handler = new Handler();
196            }
197            CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
198                    callback);
199            mCallback = msgHandler;
200        }
201    }
202
203    /**
204     * Set an intent for launching UI for this Session. This can be used as a
205     * quick link to an ongoing media screen. The intent should be for an
206     * activity that may be started using {@link Activity#startActivity(Intent)}.
207     *
208     * @param pi The intent to launch to show UI for this Session.
209     */
210    public void setSessionActivity(@Nullable PendingIntent pi) {
211        try {
212            mBinder.setLaunchPendingIntent(pi);
213        } catch (RemoteException e) {
214            Log.wtf(TAG, "Failure in setLaunchPendingIntent.", e);
215        }
216    }
217
218    /**
219     * Set a pending intent for your media button receiver to allow restarting
220     * playback after the session has been stopped. If your app is started in
221     * this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via
222     * the pending intent.
223     *
224     * @param mbr The {@link PendingIntent} to send the media button event to.
225     */
226    public void setMediaButtonReceiver(@Nullable PendingIntent mbr) {
227        try {
228            mBinder.setMediaButtonReceiver(mbr);
229        } catch (RemoteException e) {
230            Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e);
231        }
232    }
233
234    /**
235     * Set any flags for the session.
236     *
237     * @param flags The flags to set for this session.
238     */
239    public void setFlags(@SessionFlags int flags) {
240        try {
241            mBinder.setFlags(flags);
242        } catch (RemoteException e) {
243            Log.wtf(TAG, "Failure in setFlags.", e);
244        }
245    }
246
247    /**
248     * Set the attributes for this session's audio. This will affect the
249     * system's volume handling for this session. If
250     * {@link #setPlaybackToRemote} was previously called it will stop receiving
251     * volume commands and the system will begin sending volume changes to the
252     * appropriate stream.
253     * <p>
254     * By default sessions use attributes for media.
255     *
256     * @param attributes The {@link AudioAttributes} for this session's audio.
257     */
258    public void setPlaybackToLocal(AudioAttributes attributes) {
259        if (attributes == null) {
260            throw new IllegalArgumentException("Attributes cannot be null for local playback.");
261        }
262        try {
263            mBinder.setPlaybackToLocal(attributes);
264        } catch (RemoteException e) {
265            Log.wtf(TAG, "Failure in setPlaybackToLocal.", e);
266        }
267    }
268
269    /**
270     * Configure this session to use remote volume handling. This must be called
271     * to receive volume button events, otherwise the system will adjust the
272     * appropriate stream volume for this session. If
273     * {@link #setPlaybackToLocal} was previously called the system will stop
274     * handling volume changes for this session and pass them to the volume
275     * provider instead.
276     *
277     * @param volumeProvider The provider that will handle volume changes. May
278     *            not be null.
279     */
280    public void setPlaybackToRemote(@NonNull VolumeProvider volumeProvider) {
281        if (volumeProvider == null) {
282            throw new IllegalArgumentException("volumeProvider may not be null!");
283        }
284        mVolumeProvider = volumeProvider;
285        volumeProvider.setCallback(new VolumeProvider.Callback() {
286            @Override
287            public void onVolumeChanged(VolumeProvider volumeProvider) {
288                notifyRemoteVolumeChanged(volumeProvider);
289            }
290        });
291
292        try {
293            mBinder.setPlaybackToRemote(volumeProvider.getVolumeControl(),
294                    volumeProvider.getMaxVolume());
295            mBinder.setCurrentVolume(volumeProvider.onGetCurrentVolume());
296        } catch (RemoteException e) {
297            Log.wtf(TAG, "Failure in setPlaybackToRemote.", e);
298        }
299    }
300
301    /**
302     * Set if this session is currently active and ready to receive commands. If
303     * set to false your session's controller may not be discoverable. You must
304     * set the session to active before it can start receiving media button
305     * events or transport commands.
306     *
307     * @param active Whether this session is active or not.
308     */
309    public void setActive(boolean active) {
310        if (mActive == active) {
311            return;
312        }
313        try {
314            mBinder.setActive(active);
315            mActive = active;
316        } catch (RemoteException e) {
317            Log.wtf(TAG, "Failure in setActive.", e);
318        }
319    }
320
321    /**
322     * Get the current active state of this session.
323     *
324     * @return True if the session is active, false otherwise.
325     */
326    public boolean isActive() {
327        return mActive;
328    }
329
330    /**
331     * Send a proprietary event to all MediaControllers listening to this
332     * Session. It's up to the Controller/Session owner to determine the meaning
333     * of any events.
334     *
335     * @param event The name of the event to send
336     * @param extras Any extras included with the event
337     */
338    public void sendSessionEvent(@NonNull String event, @Nullable Bundle extras) {
339        if (TextUtils.isEmpty(event)) {
340            throw new IllegalArgumentException("event cannot be null or empty");
341        }
342        try {
343            mBinder.sendEvent(event, extras);
344        } catch (RemoteException e) {
345            Log.wtf(TAG, "Error sending event", e);
346        }
347    }
348
349    /**
350     * This must be called when an app has finished performing playback. If
351     * playback is expected to start again shortly the session can be left open,
352     * but it must be released if your activity or service is being destroyed.
353     */
354    public void release() {
355        try {
356            mBinder.destroy();
357        } catch (RemoteException e) {
358            Log.wtf(TAG, "Error releasing session: ", e);
359        }
360    }
361
362    /**
363     * Retrieve a token object that can be used by apps to create a
364     * {@link MediaController} for interacting with this session. The owner of
365     * the session is responsible for deciding how to distribute these tokens.
366     *
367     * @return A token that can be used to create a MediaController for this
368     *         session
369     */
370    public @NonNull Token getSessionToken() {
371        return mSessionToken;
372    }
373
374    /**
375     * Get a controller for this session. This is a convenience method to avoid
376     * having to cache your own controller in process.
377     *
378     * @return A controller for this session.
379     */
380    public @NonNull MediaController getController() {
381        return mController;
382    }
383
384    /**
385     * Update the current playback state.
386     *
387     * @param state The current state of playback
388     */
389    public void setPlaybackState(@Nullable PlaybackState state) {
390        mPlaybackState = state;
391        try {
392            mBinder.setPlaybackState(state);
393        } catch (RemoteException e) {
394            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
395        }
396    }
397
398    /**
399     * Update the current metadata. New metadata can be created using
400     * {@link android.media.MediaMetadata.Builder}.
401     *
402     * @param metadata The new metadata
403     */
404    public void setMetadata(@Nullable MediaMetadata metadata) {
405        try {
406            mBinder.setMetadata(metadata);
407        } catch (RemoteException e) {
408            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
409        }
410    }
411
412    /**
413     * Update the list of items in the play queue. It is an ordered list and
414     * should contain the current item, and previous or upcoming items if they
415     * exist. Specify null if there is no current play queue.
416     * <p>
417     * The queue should be of reasonable size. If the play queue is unbounded
418     * within your app, it is better to send a reasonable amount in a sliding
419     * window instead.
420     *
421     * @param queue A list of items in the play queue.
422     */
423    public void setQueue(@Nullable List<QueueItem> queue) {
424        try {
425            mBinder.setQueue(new ParceledListSlice<QueueItem>(queue));
426        } catch (RemoteException e) {
427            Log.wtf("Dead object in setQueue.", e);
428        }
429    }
430
431    /**
432     * Set the title of the play queue. The UI should display this title along
433     * with the play queue itself.
434     * e.g. "Play Queue", "Now Playing", or an album name.
435     *
436     * @param title The title of the play queue.
437     */
438    public void setQueueTitle(@Nullable CharSequence title) {
439        try {
440            mBinder.setQueueTitle(title);
441        } catch (RemoteException e) {
442            Log.wtf("Dead object in setQueueTitle.", e);
443        }
444    }
445
446    /**
447     * Set some extras that can be associated with the {@link MediaSession}. No assumptions should
448     * be made as to how a {@link MediaController} will handle these extras.
449     * Keys should be fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts.
450     *
451     * @param extras The extras associated with the {@link MediaSession}.
452     */
453    public void setExtras(@Nullable Bundle extras) {
454        try {
455            mBinder.setExtras(extras);
456        } catch (RemoteException e) {
457            Log.wtf("Dead object in setExtras.", e);
458        }
459    }
460
461    /**
462     * Notify the system that the remote volume changed.
463     *
464     * @param provider The provider that is handling volume changes.
465     * @hide
466     */
467    public void notifyRemoteVolumeChanged(VolumeProvider provider) {
468        if (provider == null || provider != mVolumeProvider) {
469            Log.w(TAG, "Received update from stale volume provider");
470            return;
471        }
472        try {
473            mBinder.setCurrentVolume(provider.onGetCurrentVolume());
474        } catch (RemoteException e) {
475            Log.e(TAG, "Error in notifyVolumeChanged", e);
476        }
477    }
478
479    private void dispatchPlay() {
480        postToCallback(CallbackMessageHandler.MSG_PLAY);
481    }
482
483    private void dispatchPlayFromMediaId(String mediaId, Bundle extras) {
484        postToCallback(CallbackMessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras);
485    }
486
487    private void dispatchPlayFromSearch(String query, Bundle extras) {
488        postToCallback(CallbackMessageHandler.MSG_PLAY_SEARCH, query, extras);
489    }
490
491    private void dispatchSkipToItem(long id) {
492        postToCallback(CallbackMessageHandler.MSG_SKIP_TO_ITEM, id);
493    }
494
495    private void dispatchPause() {
496        postToCallback(CallbackMessageHandler.MSG_PAUSE);
497    }
498
499    private void dispatchStop() {
500        postToCallback(CallbackMessageHandler.MSG_STOP);
501    }
502
503    private void dispatchNext() {
504        postToCallback(CallbackMessageHandler.MSG_NEXT);
505    }
506
507    private void dispatchPrevious() {
508        postToCallback(CallbackMessageHandler.MSG_PREVIOUS);
509    }
510
511    private void dispatchFastForward() {
512        postToCallback(CallbackMessageHandler.MSG_FAST_FORWARD);
513    }
514
515    private void dispatchRewind() {
516        postToCallback(CallbackMessageHandler.MSG_REWIND);
517    }
518
519    private void dispatchSeekTo(long pos) {
520        postToCallback(CallbackMessageHandler.MSG_SEEK_TO, pos);
521    }
522
523    private void dispatchRate(Rating rating) {
524        postToCallback(CallbackMessageHandler.MSG_RATE, rating);
525    }
526
527    private void dispatchCustomAction(String action, Bundle args) {
528        postToCallback(CallbackMessageHandler.MSG_CUSTOM_ACTION, action, args);
529    }
530
531    private void dispatchMediaButton(Intent mediaButtonIntent) {
532        postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent);
533    }
534
535    private void postToCallback(int what) {
536        postToCallback(what, null);
537    }
538
539    private void postCommand(String command, Bundle args, ResultReceiver resultCb) {
540        Command cmd = new Command(command, args, resultCb);
541        postToCallback(CallbackMessageHandler.MSG_COMMAND, cmd);
542    }
543
544    private void postToCallback(int what, Object obj) {
545        postToCallback(what, obj, null);
546    }
547
548    private void postToCallback(int what, Object obj, Bundle extras) {
549        synchronized (mLock) {
550            if (mCallback != null) {
551                mCallback.post(what, obj, extras);
552            }
553        }
554    }
555
556    /**
557     * Return true if this is considered an active playback state.
558     *
559     * @hide
560     */
561    public static boolean isActiveState(int state) {
562        switch (state) {
563            case PlaybackState.STATE_FAST_FORWARDING:
564            case PlaybackState.STATE_REWINDING:
565            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
566            case PlaybackState.STATE_SKIPPING_TO_NEXT:
567            case PlaybackState.STATE_BUFFERING:
568            case PlaybackState.STATE_CONNECTING:
569            case PlaybackState.STATE_PLAYING:
570                return true;
571        }
572        return false;
573    }
574
575    /**
576     * Represents an ongoing session. This may be passed to apps by the session
577     * owner to allow them to create a {@link MediaController} to communicate with
578     * the session.
579     */
580    public static final class Token implements Parcelable {
581
582        private ISessionController mBinder;
583
584        /**
585         * @hide
586         */
587        public Token(ISessionController binder) {
588            mBinder = binder;
589        }
590
591        @Override
592        public int describeContents() {
593            return 0;
594        }
595
596        @Override
597        public void writeToParcel(Parcel dest, int flags) {
598            dest.writeStrongBinder(mBinder.asBinder());
599        }
600
601        @Override
602        public int hashCode() {
603            final int prime = 31;
604            int result = 1;
605            result = prime * result + ((mBinder == null) ? 0 : mBinder.asBinder().hashCode());
606            return result;
607        }
608
609        @Override
610        public boolean equals(Object obj) {
611            if (this == obj)
612                return true;
613            if (obj == null)
614                return false;
615            if (getClass() != obj.getClass())
616                return false;
617            Token other = (Token) obj;
618            if (mBinder == null) {
619                if (other.mBinder != null)
620                    return false;
621            } else if (!mBinder.asBinder().equals(other.mBinder.asBinder()))
622                return false;
623            return true;
624        }
625
626        ISessionController getBinder() {
627            return mBinder;
628        }
629
630        public static final Parcelable.Creator<Token> CREATOR
631                = new Parcelable.Creator<Token>() {
632            @Override
633            public Token createFromParcel(Parcel in) {
634                return new Token(ISessionController.Stub.asInterface(in.readStrongBinder()));
635            }
636
637            @Override
638            public Token[] newArray(int size) {
639                return new Token[size];
640            }
641        };
642    }
643
644    /**
645     * Receives media buttons, transport controls, and commands from controllers
646     * and the system. A callback may be set using {@link #setCallback}.
647     */
648    public abstract static class Callback {
649        private MediaSession mSession;
650
651        public Callback() {
652        }
653
654        /**
655         * Called when a controller has sent a command to this session.
656         * The owner of the session may handle custom commands but is not
657         * required to.
658         *
659         * @param command The command name.
660         * @param args Optional parameters for the command, may be null.
661         * @param cb A result receiver to which a result may be sent by the command, may be null.
662         */
663        public void onCommand(@NonNull String command, @Nullable Bundle args,
664                @Nullable ResultReceiver cb) {
665        }
666
667        /**
668         * Called when a media button is pressed and this session has the
669         * highest priority or a controller sends a media button event to the
670         * session. The default behavior will call the relevant method if the
671         * action for it was set.
672         * <p>
673         * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a
674         * KeyEvent in {@link Intent#EXTRA_KEY_EVENT}
675         *
676         * @param mediaButtonIntent an intent containing the KeyEvent as an
677         *            extra
678         */
679        public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
680            if (mSession != null
681                    && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
682                KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
683                if (ke != null && ke.getAction() == KeyEvent.ACTION_DOWN) {
684                    PlaybackState state = mSession.mPlaybackState;
685                    long validActions = state == null ? 0 : state.getActions();
686                    switch (ke.getKeyCode()) {
687                        case KeyEvent.KEYCODE_MEDIA_PLAY:
688                            if ((validActions & PlaybackState.ACTION_PLAY) != 0) {
689                                onPlay();
690                            }
691                            break;
692                        case KeyEvent.KEYCODE_MEDIA_PAUSE:
693                            if ((validActions & PlaybackState.ACTION_PAUSE) != 0) {
694                                onPause();
695                            }
696                            break;
697                        case KeyEvent.KEYCODE_MEDIA_NEXT:
698                            if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
699                                onSkipToNext();
700                            }
701                            break;
702                        case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
703                            if ((validActions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
704                                onSkipToPrevious();
705                            }
706                            break;
707                        case KeyEvent.KEYCODE_MEDIA_STOP:
708                            if ((validActions & PlaybackState.ACTION_STOP) != 0) {
709                                onStop();
710                            }
711                            break;
712                        case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
713                            if ((validActions & PlaybackState.ACTION_FAST_FORWARD) != 0) {
714                                onFastForward();
715                            }
716                            break;
717                        case KeyEvent.KEYCODE_MEDIA_REWIND:
718                            if ((validActions & PlaybackState.ACTION_REWIND) != 0) {
719                                onRewind();
720                            }
721                            break;
722                        case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
723                        case KeyEvent.KEYCODE_HEADSETHOOK:
724                            boolean isPlaying = state == null ? false
725                                    : state.getState() == PlaybackState.STATE_PLAYING;
726                            boolean canPlay = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
727                                    | PlaybackState.ACTION_PLAY)) != 0;
728                            boolean canPause = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
729                                    | PlaybackState.ACTION_PAUSE)) != 0;
730                            if (isPlaying && canPause) {
731                                onPause();
732                            } else if (!isPlaying && canPlay) {
733                                onPlay();
734                            }
735                            break;
736                    }
737                }
738            }
739            return false;
740        }
741
742        /**
743         * Override to handle requests to begin playback.
744         */
745        public void onPlay() {
746        }
747
748        /**
749         * Override to handle requests to play a specific mediaId that was
750         * provided by your app's {@link MediaBrowserService}.
751         */
752        public void onPlayFromMediaId(String mediaId, Bundle extras) {
753        }
754
755        /**
756         * Override to handle requests to begin playback from a search query.
757         */
758        public void onPlayFromSearch(String query, Bundle extras) {
759        }
760
761        /**
762         * Override to handle requests to play an item with a given id from the
763         * play queue.
764         */
765        public void onSkipToQueueItem(long id) {
766        }
767
768        /**
769         * Override to handle requests to pause playback.
770         */
771        public void onPause() {
772        }
773
774        /**
775         * Override to handle requests to skip to the next media item.
776         */
777        public void onSkipToNext() {
778        }
779
780        /**
781         * Override to handle requests to skip to the previous media item.
782         */
783        public void onSkipToPrevious() {
784        }
785
786        /**
787         * Override to handle requests to fast forward.
788         */
789        public void onFastForward() {
790        }
791
792        /**
793         * Override to handle requests to rewind.
794         */
795        public void onRewind() {
796        }
797
798        /**
799         * Override to handle requests to stop playback.
800         */
801        public void onStop() {
802        }
803
804        /**
805         * Override to handle requests to seek to a specific position in ms.
806         *
807         * @param pos New position to move to, in milliseconds.
808         */
809        public void onSeekTo(long pos) {
810        }
811
812        /**
813         * Override to handle the item being rated.
814         *
815         * @param rating
816         */
817        public void onSetRating(@NonNull Rating rating) {
818        }
819
820        /**
821         * Called when a {@link MediaController} wants a {@link PlaybackState.CustomAction} to be
822         * performed.
823         *
824         * @param action The action that was originally sent in the
825         *               {@link PlaybackState.CustomAction}.
826         * @param extras Optional extras specified by the {@link MediaController}.
827         */
828        public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
829        }
830
831        private void setSession(MediaSession session) {
832            mSession = session;
833        }
834    }
835
836    /**
837     * @hide
838     */
839    public static class CallbackStub extends ISessionCallback.Stub {
840        private WeakReference<MediaSession> mMediaSession;
841
842        public CallbackStub(MediaSession session) {
843            mMediaSession = new WeakReference<MediaSession>(session);
844        }
845
846        @Override
847        public void onCommand(String command, Bundle args, ResultReceiver cb) {
848            MediaSession session = mMediaSession.get();
849            if (session != null) {
850                session.postCommand(command, args, cb);
851            }
852        }
853
854        @Override
855        public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber,
856                ResultReceiver cb) {
857            MediaSession session = mMediaSession.get();
858            try {
859                if (session != null) {
860                    session.dispatchMediaButton(mediaButtonIntent);
861                }
862            } finally {
863                if (cb != null) {
864                    cb.send(sequenceNumber, null);
865                }
866            }
867        }
868
869        @Override
870        public void onPlay() {
871            MediaSession session = mMediaSession.get();
872            if (session != null) {
873                session.dispatchPlay();
874            }
875        }
876
877        @Override
878        public void onPlayFromMediaId(String mediaId, Bundle extras) {
879            MediaSession session = mMediaSession.get();
880            if (session != null) {
881                session.dispatchPlayFromMediaId(mediaId, extras);
882            }
883        }
884
885        @Override
886        public void onPlayFromSearch(String query, Bundle extras) {
887            MediaSession session = mMediaSession.get();
888            if (session != null) {
889                session.dispatchPlayFromSearch(query, extras);
890            }
891        }
892
893        @Override
894        public void onSkipToTrack(long id) {
895            MediaSession session = mMediaSession.get();
896            if (session != null) {
897                session.dispatchSkipToItem(id);
898            }
899        }
900
901        @Override
902        public void onPause() {
903            MediaSession session = mMediaSession.get();
904            if (session != null) {
905                session.dispatchPause();
906            }
907        }
908
909        @Override
910        public void onStop() {
911            MediaSession session = mMediaSession.get();
912            if (session != null) {
913                session.dispatchStop();
914            }
915        }
916
917        @Override
918        public void onNext() {
919            MediaSession session = mMediaSession.get();
920            if (session != null) {
921                session.dispatchNext();
922            }
923        }
924
925        @Override
926        public void onPrevious() {
927            MediaSession session = mMediaSession.get();
928            if (session != null) {
929                session.dispatchPrevious();
930            }
931        }
932
933        @Override
934        public void onFastForward() {
935            MediaSession session = mMediaSession.get();
936            if (session != null) {
937                session.dispatchFastForward();
938            }
939        }
940
941        @Override
942        public void onRewind() {
943            MediaSession session = mMediaSession.get();
944            if (session != null) {
945                session.dispatchRewind();
946            }
947        }
948
949        @Override
950        public void onSeekTo(long pos) {
951            MediaSession session = mMediaSession.get();
952            if (session != null) {
953                session.dispatchSeekTo(pos);
954            }
955        }
956
957        @Override
958        public void onRate(Rating rating) {
959            MediaSession session = mMediaSession.get();
960            if (session != null) {
961                session.dispatchRate(rating);
962            }
963        }
964
965        @Override
966        public void onCustomAction(String action, Bundle args) {
967            MediaSession session = mMediaSession.get();
968            if (session != null) {
969                session.dispatchCustomAction(action, args);
970            }
971        }
972
973        @Override
974        public void onAdjustVolume(int direction) {
975            MediaSession session = mMediaSession.get();
976            if (session != null) {
977                if (session.mVolumeProvider != null) {
978                    session.mVolumeProvider.onAdjustVolume(direction);
979                }
980            }
981        }
982
983        @Override
984        public void onSetVolumeTo(int value) {
985            MediaSession session = mMediaSession.get();
986            if (session != null) {
987                if (session.mVolumeProvider != null) {
988                    session.mVolumeProvider.onSetVolumeTo(value);
989                }
990            }
991        }
992
993    }
994
995    /**
996     * A single item that is part of the play queue. It contains a description
997     * of the item and its id in the queue.
998     */
999    public static final class QueueItem implements Parcelable {
1000        /**
1001         * This id is reserved. No items can be explicitly asigned this id.
1002         */
1003        public static final int UNKNOWN_ID = -1;
1004
1005        private final MediaDescription mDescription;
1006        private final long mId;
1007
1008        /**
1009         * Create a new {@link MediaSession.QueueItem}.
1010         *
1011         * @param description The {@link MediaDescription} for this item.
1012         * @param id An identifier for this item. It must be unique within the
1013         *            play queue and cannot be {@link #UNKNOWN_ID}.
1014         */
1015        public QueueItem(MediaDescription description, long id) {
1016            if (description == null) {
1017                throw new IllegalArgumentException("Description cannot be null.");
1018            }
1019            if (id == UNKNOWN_ID) {
1020                throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID");
1021            }
1022            mDescription = description;
1023            mId = id;
1024        }
1025
1026        private QueueItem(Parcel in) {
1027            mDescription = MediaDescription.CREATOR.createFromParcel(in);
1028            mId = in.readLong();
1029        }
1030
1031        /**
1032         * Get the description for this item.
1033         */
1034        public MediaDescription getDescription() {
1035            return mDescription;
1036        }
1037
1038        /**
1039         * Get the queue id for this item.
1040         */
1041        public long getQueueId() {
1042            return mId;
1043        }
1044
1045        @Override
1046        public void writeToParcel(Parcel dest, int flags) {
1047            mDescription.writeToParcel(dest, flags);
1048            dest.writeLong(mId);
1049        }
1050
1051        @Override
1052        public int describeContents() {
1053            return 0;
1054        }
1055
1056        public static final Creator<MediaSession.QueueItem> CREATOR = new Creator<MediaSession.QueueItem>() {
1057
1058            @Override
1059            public MediaSession.QueueItem createFromParcel(Parcel p) {
1060                return new MediaSession.QueueItem(p);
1061            }
1062
1063            @Override
1064            public MediaSession.QueueItem[] newArray(int size) {
1065                return new MediaSession.QueueItem[size];
1066            }
1067        };
1068
1069        @Override
1070        public String toString() {
1071            return "MediaSession.QueueItem {" +
1072                    "Description=" + mDescription +
1073                    ", Id=" + mId + " }";
1074        }
1075    }
1076
1077    private static final class Command {
1078        public final String command;
1079        public final Bundle extras;
1080        public final ResultReceiver stub;
1081
1082        public Command(String command, Bundle extras, ResultReceiver stub) {
1083            this.command = command;
1084            this.extras = extras;
1085            this.stub = stub;
1086        }
1087    }
1088
1089    private class CallbackMessageHandler extends Handler {
1090
1091        private static final int MSG_PLAY = 1;
1092        private static final int MSG_PLAY_MEDIA_ID = 2;
1093        private static final int MSG_PLAY_SEARCH = 3;
1094        private static final int MSG_SKIP_TO_ITEM = 4;
1095        private static final int MSG_PAUSE = 5;
1096        private static final int MSG_STOP = 6;
1097        private static final int MSG_NEXT = 7;
1098        private static final int MSG_PREVIOUS = 8;
1099        private static final int MSG_FAST_FORWARD = 9;
1100        private static final int MSG_REWIND = 10;
1101        private static final int MSG_SEEK_TO = 11;
1102        private static final int MSG_RATE = 12;
1103        private static final int MSG_CUSTOM_ACTION = 13;
1104        private static final int MSG_MEDIA_BUTTON = 14;
1105        private static final int MSG_COMMAND = 15;
1106
1107        private MediaSession.Callback mCallback;
1108
1109        public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) {
1110            super(looper, null, true);
1111            mCallback = callback;
1112        }
1113
1114        public void post(int what, Object obj, Bundle bundle) {
1115            Message msg = obtainMessage(what, obj);
1116            msg.setData(bundle);
1117            msg.sendToTarget();
1118        }
1119
1120        public void post(int what, Object obj) {
1121            obtainMessage(what, obj).sendToTarget();
1122        }
1123
1124        public void post(int what) {
1125            post(what, null);
1126        }
1127
1128        public void post(int what, Object obj, int arg1) {
1129            obtainMessage(what, arg1, 0, obj).sendToTarget();
1130        }
1131
1132        @Override
1133        public void handleMessage(Message msg) {
1134            switch (msg.what) {
1135                case MSG_PLAY:
1136                    mCallback.onPlay();
1137                    break;
1138                case MSG_PLAY_MEDIA_ID:
1139                    mCallback.onPlayFromMediaId((String) msg.obj, msg.getData());
1140                    break;
1141                case MSG_PLAY_SEARCH:
1142                    mCallback.onPlayFromSearch((String) msg.obj, msg.getData());
1143                    break;
1144                case MSG_SKIP_TO_ITEM:
1145                    mCallback.onSkipToQueueItem((Long) msg.obj);
1146                case MSG_PAUSE:
1147                    mCallback.onPause();
1148                    break;
1149                case MSG_STOP:
1150                    mCallback.onStop();
1151                    break;
1152                case MSG_NEXT:
1153                    mCallback.onSkipToNext();
1154                    break;
1155                case MSG_PREVIOUS:
1156                    mCallback.onSkipToPrevious();
1157                    break;
1158                case MSG_FAST_FORWARD:
1159                    mCallback.onFastForward();
1160                    break;
1161                case MSG_REWIND:
1162                    mCallback.onRewind();
1163                    break;
1164                case MSG_SEEK_TO:
1165                    mCallback.onSeekTo((Long) msg.obj);
1166                    break;
1167                case MSG_RATE:
1168                    mCallback.onSetRating((Rating) msg.obj);
1169                    break;
1170                case MSG_CUSTOM_ACTION:
1171                    mCallback.onCustomAction((String) msg.obj, msg.getData());
1172                    break;
1173                case MSG_MEDIA_BUTTON:
1174                    mCallback.onMediaButtonEvent((Intent) msg.obj);
1175                    break;
1176                case MSG_COMMAND:
1177                    Command cmd = (Command) msg.obj;
1178                    mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
1179                    break;
1180            }
1181        }
1182    }
1183}
1184