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