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