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