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