MediaSession.java revision 341dd2bda5fbe4f0cd6c0adfd46d67eb301fd229
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        synchronized (mLock) {
308            mVolumeProvider = volumeProvider;
309        }
310        volumeProvider.setCallback(new VolumeProvider.Callback() {
311            @Override
312            public void onVolumeChanged(VolumeProvider volumeProvider) {
313                notifyRemoteVolumeChanged(volumeProvider);
314            }
315        });
316
317        try {
318            mBinder.setPlaybackToRemote(volumeProvider.getVolumeControl(),
319                    volumeProvider.getMaxVolume());
320            mBinder.setCurrentVolume(volumeProvider.getCurrentVolume());
321        } catch (RemoteException e) {
322            Log.wtf(TAG, "Failure in setPlaybackToRemote.", e);
323        }
324    }
325
326    /**
327     * Set if this session is currently active and ready to receive commands. If
328     * set to false your session's controller may not be discoverable. You must
329     * set the session to active before it can start receiving media button
330     * events or transport commands.
331     *
332     * @param active Whether this session is active or not.
333     */
334    public void setActive(boolean active) {
335        if (mActive == active) {
336            return;
337        }
338        try {
339            mBinder.setActive(active);
340            mActive = active;
341        } catch (RemoteException e) {
342            Log.wtf(TAG, "Failure in setActive.", e);
343        }
344    }
345
346    /**
347     * Get the current active state of this session.
348     *
349     * @return True if the session is active, false otherwise.
350     */
351    public boolean isActive() {
352        return mActive;
353    }
354
355    /**
356     * Send a proprietary event to all MediaControllers listening to this
357     * Session. It's up to the Controller/Session owner to determine the meaning
358     * of any events.
359     *
360     * @param event The name of the event to send
361     * @param extras Any extras included with the event
362     */
363    public void sendSessionEvent(@NonNull String event, @Nullable Bundle extras) {
364        if (TextUtils.isEmpty(event)) {
365            throw new IllegalArgumentException("event cannot be null or empty");
366        }
367        try {
368            mBinder.sendEvent(event, extras);
369        } catch (RemoteException e) {
370            Log.wtf(TAG, "Error sending event", e);
371        }
372    }
373
374    /**
375     * This must be called when an app has finished performing playback. If
376     * playback is expected to start again shortly the session can be left open,
377     * but it must be released if your activity or service is being destroyed.
378     */
379    public void release() {
380        try {
381            mBinder.destroy();
382        } catch (RemoteException e) {
383            Log.wtf(TAG, "Error releasing session: ", e);
384        }
385    }
386
387    /**
388     * Retrieve a token object that can be used by apps to create a
389     * {@link MediaController} for interacting with this session. The owner of
390     * the session is responsible for deciding how to distribute these tokens.
391     *
392     * @return A token that can be used to create a MediaController for this
393     *         session
394     */
395    public @NonNull Token getSessionToken() {
396        return mSessionToken;
397    }
398
399    /**
400     * Get a controller for this session. This is a convenience method to avoid
401     * having to cache your own controller in process.
402     *
403     * @return A controller for this session.
404     */
405    public @NonNull MediaController getController() {
406        return mController;
407    }
408
409    /**
410     * Update the current playback state.
411     *
412     * @param state The current state of playback
413     */
414    public void setPlaybackState(@Nullable PlaybackState state) {
415        mPlaybackState = state;
416        try {
417            mBinder.setPlaybackState(state);
418        } catch (RemoteException e) {
419            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
420        }
421    }
422
423    /**
424     * Update the current metadata. New metadata can be created using
425     * {@link android.media.MediaMetadata.Builder}.
426     *
427     * @param metadata The new metadata
428     */
429    public void setMetadata(@Nullable MediaMetadata metadata) {
430        try {
431            mBinder.setMetadata(metadata);
432        } catch (RemoteException e) {
433            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
434        }
435    }
436
437    /**
438     * Update the list of items in the play queue. It is an ordered list and
439     * should contain the current item, and previous or upcoming items if they
440     * exist. Specify null if there is no current play queue.
441     * <p>
442     * The queue should be of reasonable size. If the play queue is unbounded
443     * within your app, it is better to send a reasonable amount in a sliding
444     * window instead.
445     *
446     * @param queue A list of items in the play queue.
447     */
448    public void setQueue(@Nullable List<QueueItem> queue) {
449        try {
450            mBinder.setQueue(queue == null ? null : new ParceledListSlice<QueueItem>(queue));
451        } catch (RemoteException e) {
452            Log.wtf("Dead object in setQueue.", e);
453        }
454    }
455
456    /**
457     * Set the title of the play queue. The UI should display this title along
458     * with the play queue itself.
459     * e.g. "Play Queue", "Now Playing", or an album name.
460     *
461     * @param title The title of the play queue.
462     */
463    public void setQueueTitle(@Nullable CharSequence title) {
464        try {
465            mBinder.setQueueTitle(title);
466        } catch (RemoteException e) {
467            Log.wtf("Dead object in setQueueTitle.", e);
468        }
469    }
470
471    /**
472     * Set the style of rating used by this session. Apps trying to set the
473     * rating should use this style. Must be one of the following:
474     * <ul>
475     * <li>{@link Rating#RATING_NONE}</li>
476     * <li>{@link Rating#RATING_3_STARS}</li>
477     * <li>{@link Rating#RATING_4_STARS}</li>
478     * <li>{@link Rating#RATING_5_STARS}</li>
479     * <li>{@link Rating#RATING_HEART}</li>
480     * <li>{@link Rating#RATING_PERCENTAGE}</li>
481     * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li>
482     * </ul>
483     */
484    public void setRatingType(int type) {
485        try {
486            mBinder.setRatingType(type);
487        } catch (RemoteException e) {
488            Log.e(TAG, "Error in setRatingType.", e);
489        }
490    }
491
492    /**
493     * Set some extras that can be associated with the {@link MediaSession}. No assumptions should
494     * be made as to how a {@link MediaController} will handle these extras.
495     * Keys should be fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts.
496     *
497     * @param extras The extras associated with the {@link MediaSession}.
498     */
499    public void setExtras(@Nullable Bundle extras) {
500        try {
501            mBinder.setExtras(extras);
502        } catch (RemoteException e) {
503            Log.wtf("Dead object in setExtras.", e);
504        }
505    }
506
507    /**
508     * Notify the system that the remote volume changed.
509     *
510     * @param provider The provider that is handling volume changes.
511     * @hide
512     */
513    public void notifyRemoteVolumeChanged(VolumeProvider provider) {
514        synchronized (mLock) {
515            if (provider == null || provider != mVolumeProvider) {
516                Log.w(TAG, "Received update from stale volume provider");
517                return;
518            }
519        }
520        try {
521            mBinder.setCurrentVolume(provider.getCurrentVolume());
522        } catch (RemoteException e) {
523            Log.e(TAG, "Error in notifyVolumeChanged", e);
524        }
525    }
526
527    private void dispatchPlay() {
528        postToCallback(CallbackMessageHandler.MSG_PLAY);
529    }
530
531    private void dispatchPlayFromMediaId(String mediaId, Bundle extras) {
532        postToCallback(CallbackMessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras);
533    }
534
535    private void dispatchPlayFromSearch(String query, Bundle extras) {
536        postToCallback(CallbackMessageHandler.MSG_PLAY_SEARCH, query, extras);
537    }
538
539    private void dispatchSkipToItem(long id) {
540        postToCallback(CallbackMessageHandler.MSG_SKIP_TO_ITEM, id);
541    }
542
543    private void dispatchPause() {
544        postToCallback(CallbackMessageHandler.MSG_PAUSE);
545    }
546
547    private void dispatchStop() {
548        postToCallback(CallbackMessageHandler.MSG_STOP);
549    }
550
551    private void dispatchNext() {
552        postToCallback(CallbackMessageHandler.MSG_NEXT);
553    }
554
555    private void dispatchPrevious() {
556        postToCallback(CallbackMessageHandler.MSG_PREVIOUS);
557    }
558
559    private void dispatchFastForward() {
560        postToCallback(CallbackMessageHandler.MSG_FAST_FORWARD);
561    }
562
563    private void dispatchRewind() {
564        postToCallback(CallbackMessageHandler.MSG_REWIND);
565    }
566
567    private void dispatchSeekTo(long pos) {
568        postToCallback(CallbackMessageHandler.MSG_SEEK_TO, pos);
569    }
570
571    private void dispatchRate(Rating rating) {
572        postToCallback(CallbackMessageHandler.MSG_RATE, rating);
573    }
574
575    private void dispatchCustomAction(String action, Bundle args) {
576        postToCallback(CallbackMessageHandler.MSG_CUSTOM_ACTION, action, args);
577    }
578
579    private void dispatchMediaButton(Intent mediaButtonIntent) {
580        postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent);
581    }
582
583    private void dispatchAdjustVolume(int direction) {
584        postToCallback(CallbackMessageHandler.MSG_ADJUST_VOLUME, direction);
585    }
586
587    private void dispatchSetVolumeTo(int volume) {
588        postToCallback(CallbackMessageHandler.MSG_SET_VOLUME, volume);
589    }
590
591    private void postToCallback(int what) {
592        postToCallback(what, null);
593    }
594
595    private void postCommand(String command, Bundle args, ResultReceiver resultCb) {
596        Command cmd = new Command(command, args, resultCb);
597        postToCallback(CallbackMessageHandler.MSG_COMMAND, cmd);
598    }
599
600    private void postToCallback(int what, Object obj) {
601        postToCallback(what, obj, null);
602    }
603
604    private void postToCallback(int what, Object obj, Bundle extras) {
605        synchronized (mLock) {
606            if (mCallback != null) {
607                mCallback.post(what, obj, extras);
608            }
609        }
610    }
611
612    /**
613     * Return true if this is considered an active playback state.
614     *
615     * @hide
616     */
617    public static boolean isActiveState(int state) {
618        switch (state) {
619            case PlaybackState.STATE_FAST_FORWARDING:
620            case PlaybackState.STATE_REWINDING:
621            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
622            case PlaybackState.STATE_SKIPPING_TO_NEXT:
623            case PlaybackState.STATE_BUFFERING:
624            case PlaybackState.STATE_CONNECTING:
625            case PlaybackState.STATE_PLAYING:
626                return true;
627        }
628        return false;
629    }
630
631    /**
632     * Represents an ongoing session. This may be passed to apps by the session
633     * owner to allow them to create a {@link MediaController} to communicate with
634     * the session.
635     */
636    public static final class Token implements Parcelable {
637
638        private ISessionController mBinder;
639
640        /**
641         * @hide
642         */
643        public Token(ISessionController binder) {
644            mBinder = binder;
645        }
646
647        @Override
648        public int describeContents() {
649            return 0;
650        }
651
652        @Override
653        public void writeToParcel(Parcel dest, int flags) {
654            dest.writeStrongBinder(mBinder.asBinder());
655        }
656
657        @Override
658        public int hashCode() {
659            final int prime = 31;
660            int result = 1;
661            result = prime * result + ((mBinder == null) ? 0 : mBinder.asBinder().hashCode());
662            return result;
663        }
664
665        @Override
666        public boolean equals(Object obj) {
667            if (this == obj)
668                return true;
669            if (obj == null)
670                return false;
671            if (getClass() != obj.getClass())
672                return false;
673            Token other = (Token) obj;
674            if (mBinder == null) {
675                if (other.mBinder != null)
676                    return false;
677            } else if (!mBinder.asBinder().equals(other.mBinder.asBinder()))
678                return false;
679            return true;
680        }
681
682        ISessionController getBinder() {
683            return mBinder;
684        }
685
686        public static final Parcelable.Creator<Token> CREATOR
687                = new Parcelable.Creator<Token>() {
688            @Override
689            public Token createFromParcel(Parcel in) {
690                return new Token(ISessionController.Stub.asInterface(in.readStrongBinder()));
691            }
692
693            @Override
694            public Token[] newArray(int size) {
695                return new Token[size];
696            }
697        };
698    }
699
700    /**
701     * Receives media buttons, transport controls, and commands from controllers
702     * and the system. A callback may be set using {@link #setCallback}.
703     */
704    public abstract static class Callback {
705        private MediaSession mSession;
706
707        public Callback() {
708        }
709
710        /**
711         * Called when a controller has sent a command to this session.
712         * The owner of the session may handle custom commands but is not
713         * required to.
714         *
715         * @param command The command name.
716         * @param args Optional parameters for the command, may be null.
717         * @param cb A result receiver to which a result may be sent by the command, may be null.
718         */
719        public void onCommand(@NonNull String command, @Nullable Bundle args,
720                @Nullable ResultReceiver cb) {
721        }
722
723        /**
724         * Called when a media button is pressed and this session has the
725         * highest priority or a controller sends a media button event to the
726         * session. The default behavior will call the relevant method if the
727         * action for it was set.
728         * <p>
729         * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a
730         * KeyEvent in {@link Intent#EXTRA_KEY_EVENT}
731         *
732         * @param mediaButtonIntent an intent containing the KeyEvent as an
733         *            extra
734         * @return True if the event was handled, false otherwise.
735         */
736        public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
737            if (mSession != null
738                    && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
739                KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
740                if (ke != null && ke.getAction() == KeyEvent.ACTION_DOWN) {
741                    PlaybackState state = mSession.mPlaybackState;
742                    long validActions = state == null ? 0 : state.getActions();
743                    switch (ke.getKeyCode()) {
744                        case KeyEvent.KEYCODE_MEDIA_PLAY:
745                            if ((validActions & PlaybackState.ACTION_PLAY) != 0) {
746                                onPlay();
747                                return true;
748                            }
749                            break;
750                        case KeyEvent.KEYCODE_MEDIA_PAUSE:
751                            if ((validActions & PlaybackState.ACTION_PAUSE) != 0) {
752                                onPause();
753                                return true;
754                            }
755                            break;
756                        case KeyEvent.KEYCODE_MEDIA_NEXT:
757                            if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
758                                onSkipToNext();
759                                return true;
760                            }
761                            break;
762                        case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
763                            if ((validActions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
764                                onSkipToPrevious();
765                                return true;
766                            }
767                            break;
768                        case KeyEvent.KEYCODE_MEDIA_STOP:
769                            if ((validActions & PlaybackState.ACTION_STOP) != 0) {
770                                onStop();
771                                return true;
772                            }
773                            break;
774                        case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
775                            if ((validActions & PlaybackState.ACTION_FAST_FORWARD) != 0) {
776                                onFastForward();
777                                return true;
778                            }
779                            break;
780                        case KeyEvent.KEYCODE_MEDIA_REWIND:
781                            if ((validActions & PlaybackState.ACTION_REWIND) != 0) {
782                                onRewind();
783                                return true;
784                            }
785                            break;
786                        case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
787                        case KeyEvent.KEYCODE_HEADSETHOOK:
788                            boolean isPlaying = state == null ? false
789                                    : state.getState() == PlaybackState.STATE_PLAYING;
790                            boolean canPlay = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
791                                    | PlaybackState.ACTION_PLAY)) != 0;
792                            boolean canPause = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
793                                    | PlaybackState.ACTION_PAUSE)) != 0;
794                            if (isPlaying && canPause) {
795                                onPause();
796                                return true;
797                            } else if (!isPlaying && canPlay) {
798                                onPlay();
799                                return true;
800                            }
801                            break;
802                    }
803                }
804            }
805            return false;
806        }
807
808        /**
809         * Override to handle requests to begin playback.
810         */
811        public void onPlay() {
812        }
813
814        /**
815         * Override to handle requests to play a specific mediaId that was
816         * provided by your app's {@link MediaBrowserService}.
817         */
818        public void onPlayFromMediaId(String mediaId, Bundle extras) {
819        }
820
821        /**
822         * Override to handle requests to begin playback from a search query. An
823         * empty query indicates that the app may play any music. The
824         * implementation should attempt to make a smart choice about what to
825         * play.
826         */
827        public void onPlayFromSearch(String query, Bundle extras) {
828        }
829
830        /**
831         * Override to handle requests to play an item with a given id from the
832         * play queue.
833         */
834        public void onSkipToQueueItem(long id) {
835        }
836
837        /**
838         * Override to handle requests to pause playback.
839         */
840        public void onPause() {
841        }
842
843        /**
844         * Override to handle requests to skip to the next media item.
845         */
846        public void onSkipToNext() {
847        }
848
849        /**
850         * Override to handle requests to skip to the previous media item.
851         */
852        public void onSkipToPrevious() {
853        }
854
855        /**
856         * Override to handle requests to fast forward.
857         */
858        public void onFastForward() {
859        }
860
861        /**
862         * Override to handle requests to rewind.
863         */
864        public void onRewind() {
865        }
866
867        /**
868         * Override to handle requests to stop playback.
869         */
870        public void onStop() {
871        }
872
873        /**
874         * Override to handle requests to seek to a specific position in ms.
875         *
876         * @param pos New position to move to, in milliseconds.
877         */
878        public void onSeekTo(long pos) {
879        }
880
881        /**
882         * Override to handle the item being rated.
883         *
884         * @param rating
885         */
886        public void onSetRating(@NonNull Rating rating) {
887        }
888
889        /**
890         * Called when a {@link MediaController} wants a {@link PlaybackState.CustomAction} to be
891         * performed.
892         *
893         * @param action The action that was originally sent in the
894         *               {@link PlaybackState.CustomAction}.
895         * @param extras Optional extras specified by the {@link MediaController}.
896         */
897        public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
898        }
899    }
900
901    /**
902     * @hide
903     */
904    public static class CallbackStub extends ISessionCallback.Stub {
905        private WeakReference<MediaSession> mMediaSession;
906
907        public CallbackStub(MediaSession session) {
908            mMediaSession = new WeakReference<MediaSession>(session);
909        }
910
911        @Override
912        public void onCommand(String command, Bundle args, ResultReceiver cb) {
913            MediaSession session = mMediaSession.get();
914            if (session != null) {
915                session.postCommand(command, args, cb);
916            }
917        }
918
919        @Override
920        public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber,
921                ResultReceiver cb) {
922            MediaSession session = mMediaSession.get();
923            try {
924                if (session != null) {
925                    session.dispatchMediaButton(mediaButtonIntent);
926                }
927            } finally {
928                if (cb != null) {
929                    cb.send(sequenceNumber, null);
930                }
931            }
932        }
933
934        @Override
935        public void onPlay() {
936            MediaSession session = mMediaSession.get();
937            if (session != null) {
938                session.dispatchPlay();
939            }
940        }
941
942        @Override
943        public void onPlayFromMediaId(String mediaId, Bundle extras) {
944            MediaSession session = mMediaSession.get();
945            if (session != null) {
946                session.dispatchPlayFromMediaId(mediaId, extras);
947            }
948        }
949
950        @Override
951        public void onPlayFromSearch(String query, Bundle extras) {
952            MediaSession session = mMediaSession.get();
953            if (session != null) {
954                session.dispatchPlayFromSearch(query, extras);
955            }
956        }
957
958        @Override
959        public void onSkipToTrack(long id) {
960            MediaSession session = mMediaSession.get();
961            if (session != null) {
962                session.dispatchSkipToItem(id);
963            }
964        }
965
966        @Override
967        public void onPause() {
968            MediaSession session = mMediaSession.get();
969            if (session != null) {
970                session.dispatchPause();
971            }
972        }
973
974        @Override
975        public void onStop() {
976            MediaSession session = mMediaSession.get();
977            if (session != null) {
978                session.dispatchStop();
979            }
980        }
981
982        @Override
983        public void onNext() {
984            MediaSession session = mMediaSession.get();
985            if (session != null) {
986                session.dispatchNext();
987            }
988        }
989
990        @Override
991        public void onPrevious() {
992            MediaSession session = mMediaSession.get();
993            if (session != null) {
994                session.dispatchPrevious();
995            }
996        }
997
998        @Override
999        public void onFastForward() {
1000            MediaSession session = mMediaSession.get();
1001            if (session != null) {
1002                session.dispatchFastForward();
1003            }
1004        }
1005
1006        @Override
1007        public void onRewind() {
1008            MediaSession session = mMediaSession.get();
1009            if (session != null) {
1010                session.dispatchRewind();
1011            }
1012        }
1013
1014        @Override
1015        public void onSeekTo(long pos) {
1016            MediaSession session = mMediaSession.get();
1017            if (session != null) {
1018                session.dispatchSeekTo(pos);
1019            }
1020        }
1021
1022        @Override
1023        public void onRate(Rating rating) {
1024            MediaSession session = mMediaSession.get();
1025            if (session != null) {
1026                session.dispatchRate(rating);
1027            }
1028        }
1029
1030        @Override
1031        public void onCustomAction(String action, Bundle args) {
1032            MediaSession session = mMediaSession.get();
1033            if (session != null) {
1034                session.dispatchCustomAction(action, args);
1035            }
1036        }
1037
1038        @Override
1039        public void onAdjustVolume(int direction) {
1040            MediaSession session = mMediaSession.get();
1041            if (session != null) {
1042                session.dispatchAdjustVolume(direction);
1043            }
1044        }
1045
1046        @Override
1047        public void onSetVolumeTo(int value) {
1048            MediaSession session = mMediaSession.get();
1049            if (session != null) {
1050                session.dispatchSetVolumeTo(value);
1051            }
1052        }
1053
1054    }
1055
1056    /**
1057     * A single item that is part of the play queue. It contains a description
1058     * of the item and its id in the queue.
1059     */
1060    public static final class QueueItem implements Parcelable {
1061        /**
1062         * This id is reserved. No items can be explicitly asigned this id.
1063         */
1064        public static final int UNKNOWN_ID = -1;
1065
1066        private final MediaDescription mDescription;
1067        private final long mId;
1068
1069        /**
1070         * Create a new {@link MediaSession.QueueItem}.
1071         *
1072         * @param description The {@link MediaDescription} for this item.
1073         * @param id An identifier for this item. It must be unique within the
1074         *            play queue and cannot be {@link #UNKNOWN_ID}.
1075         */
1076        public QueueItem(MediaDescription description, long id) {
1077            if (description == null) {
1078                throw new IllegalArgumentException("Description cannot be null.");
1079            }
1080            if (id == UNKNOWN_ID) {
1081                throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID");
1082            }
1083            mDescription = description;
1084            mId = id;
1085        }
1086
1087        private QueueItem(Parcel in) {
1088            mDescription = MediaDescription.CREATOR.createFromParcel(in);
1089            mId = in.readLong();
1090        }
1091
1092        /**
1093         * Get the description for this item.
1094         */
1095        public MediaDescription getDescription() {
1096            return mDescription;
1097        }
1098
1099        /**
1100         * Get the queue id for this item.
1101         */
1102        public long getQueueId() {
1103            return mId;
1104        }
1105
1106        @Override
1107        public void writeToParcel(Parcel dest, int flags) {
1108            mDescription.writeToParcel(dest, flags);
1109            dest.writeLong(mId);
1110        }
1111
1112        @Override
1113        public int describeContents() {
1114            return 0;
1115        }
1116
1117        public static final Creator<MediaSession.QueueItem> CREATOR = new Creator<MediaSession.QueueItem>() {
1118
1119            @Override
1120            public MediaSession.QueueItem createFromParcel(Parcel p) {
1121                return new MediaSession.QueueItem(p);
1122            }
1123
1124            @Override
1125            public MediaSession.QueueItem[] newArray(int size) {
1126                return new MediaSession.QueueItem[size];
1127            }
1128        };
1129
1130        @Override
1131        public String toString() {
1132            return "MediaSession.QueueItem {" +
1133                    "Description=" + mDescription +
1134                    ", Id=" + mId + " }";
1135        }
1136    }
1137
1138    private static final class Command {
1139        public final String command;
1140        public final Bundle extras;
1141        public final ResultReceiver stub;
1142
1143        public Command(String command, Bundle extras, ResultReceiver stub) {
1144            this.command = command;
1145            this.extras = extras;
1146            this.stub = stub;
1147        }
1148    }
1149
1150    private class CallbackMessageHandler extends Handler {
1151
1152        private static final int MSG_PLAY = 1;
1153        private static final int MSG_PLAY_MEDIA_ID = 2;
1154        private static final int MSG_PLAY_SEARCH = 3;
1155        private static final int MSG_SKIP_TO_ITEM = 4;
1156        private static final int MSG_PAUSE = 5;
1157        private static final int MSG_STOP = 6;
1158        private static final int MSG_NEXT = 7;
1159        private static final int MSG_PREVIOUS = 8;
1160        private static final int MSG_FAST_FORWARD = 9;
1161        private static final int MSG_REWIND = 10;
1162        private static final int MSG_SEEK_TO = 11;
1163        private static final int MSG_RATE = 12;
1164        private static final int MSG_CUSTOM_ACTION = 13;
1165        private static final int MSG_MEDIA_BUTTON = 14;
1166        private static final int MSG_COMMAND = 15;
1167        private static final int MSG_ADJUST_VOLUME = 16;
1168        private static final int MSG_SET_VOLUME = 17;
1169
1170        private MediaSession.Callback mCallback;
1171
1172        public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) {
1173            super(looper, null, true);
1174            mCallback = callback;
1175        }
1176
1177        public void post(int what, Object obj, Bundle bundle) {
1178            Message msg = obtainMessage(what, obj);
1179            msg.setData(bundle);
1180            msg.sendToTarget();
1181        }
1182
1183        public void post(int what, Object obj) {
1184            obtainMessage(what, obj).sendToTarget();
1185        }
1186
1187        public void post(int what) {
1188            post(what, null);
1189        }
1190
1191        public void post(int what, Object obj, int arg1) {
1192            obtainMessage(what, arg1, 0, obj).sendToTarget();
1193        }
1194
1195        @Override
1196        public void handleMessage(Message msg) {
1197            VolumeProvider vp;
1198            switch (msg.what) {
1199                case MSG_PLAY:
1200                    mCallback.onPlay();
1201                    break;
1202                case MSG_PLAY_MEDIA_ID:
1203                    mCallback.onPlayFromMediaId((String) msg.obj, msg.getData());
1204                    break;
1205                case MSG_PLAY_SEARCH:
1206                    mCallback.onPlayFromSearch((String) msg.obj, msg.getData());
1207                    break;
1208                case MSG_SKIP_TO_ITEM:
1209                    mCallback.onSkipToQueueItem((Long) msg.obj);
1210                    break;
1211                case MSG_PAUSE:
1212                    mCallback.onPause();
1213                    break;
1214                case MSG_STOP:
1215                    mCallback.onStop();
1216                    break;
1217                case MSG_NEXT:
1218                    mCallback.onSkipToNext();
1219                    break;
1220                case MSG_PREVIOUS:
1221                    mCallback.onSkipToPrevious();
1222                    break;
1223                case MSG_FAST_FORWARD:
1224                    mCallback.onFastForward();
1225                    break;
1226                case MSG_REWIND:
1227                    mCallback.onRewind();
1228                    break;
1229                case MSG_SEEK_TO:
1230                    mCallback.onSeekTo((Long) msg.obj);
1231                    break;
1232                case MSG_RATE:
1233                    mCallback.onSetRating((Rating) msg.obj);
1234                    break;
1235                case MSG_CUSTOM_ACTION:
1236                    mCallback.onCustomAction((String) msg.obj, msg.getData());
1237                    break;
1238                case MSG_MEDIA_BUTTON:
1239                    mCallback.onMediaButtonEvent((Intent) msg.obj);
1240                    break;
1241                case MSG_COMMAND:
1242                    Command cmd = (Command) msg.obj;
1243                    mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
1244                    break;
1245                case MSG_ADJUST_VOLUME:
1246                    synchronized (mLock) {
1247                        vp = mVolumeProvider;
1248                    }
1249                    if (vp != null) {
1250                        vp.onAdjustVolume((int) msg.obj);
1251                    }
1252                    break;
1253                case MSG_SET_VOLUME:
1254                    synchronized (mLock) {
1255                        vp = mVolumeProvider;
1256                    }
1257                    if (vp != null) {
1258                        vp.onSetVolumeTo((int) msg.obj);
1259                    }
1260                    break;
1261            }
1262        }
1263    }
1264}
1265