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