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    private final int mMaxBitmapSize;
108
109    private final MediaSession.Token mSessionToken;
110    private final MediaController mController;
111    private final ISession mBinder;
112    private final CallbackStub mCbStub;
113
114    private CallbackMessageHandler mCallback;
115    private VolumeProvider mVolumeProvider;
116    private PlaybackState mPlaybackState;
117
118    private boolean mActive = false;
119
120    /**
121     * Creates a new session. The session will automatically be registered with
122     * the system but will not be published until {@link #setActive(boolean)
123     * setActive(true)} is called. You must call {@link #release()} when
124     * finished with the session.
125     *
126     * @param context The context to use to create the session.
127     * @param tag A short name for debugging purposes.
128     */
129    public MediaSession(@NonNull Context context, @NonNull String tag) {
130        this(context, tag, UserHandle.myUserId());
131    }
132
133    /**
134     * Creates a new session as the specified user. To create a session as a
135     * user other than your own you must hold the
136     * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL}
137     * permission.
138     *
139     * @param context The context to use to create the session.
140     * @param tag A short name for debugging purposes.
141     * @param userId The user id to create the session as.
142     * @hide
143     */
144    public MediaSession(@NonNull Context context, @NonNull String tag, int userId) {
145        if (context == null) {
146            throw new IllegalArgumentException("context cannot be null.");
147        }
148        if (TextUtils.isEmpty(tag)) {
149            throw new IllegalArgumentException("tag cannot be null or empty");
150        }
151        mMaxBitmapSize = context.getResources().getDimensionPixelSize(
152                com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize);
153        mCbStub = new CallbackStub(this);
154        MediaSessionManager manager = (MediaSessionManager) context
155                .getSystemService(Context.MEDIA_SESSION_SERVICE);
156        try {
157            mBinder = manager.createSession(mCbStub, tag, userId);
158            mSessionToken = new Token(mBinder.getController());
159            mController = new MediaController(context, mSessionToken);
160        } catch (RemoteException e) {
161            throw new RuntimeException("Remote error creating session.", e);
162        }
163    }
164
165    /**
166     * Set the callback to receive updates for the MediaSession. This includes
167     * media button events and transport controls. The caller's thread will be
168     * used to post updates.
169     * <p>
170     * Set the callback to null to stop receiving updates.
171     *
172     * @param callback The callback object
173     */
174    public void setCallback(@Nullable Callback callback) {
175        setCallback(callback, null);
176    }
177
178    /**
179     * Set the callback to receive updates for the MediaSession. This includes
180     * media button events and transport controls.
181     * <p>
182     * Set the callback to null to stop receiving updates.
183     *
184     * @param callback The callback to receive updates on.
185     * @param handler The handler that events should be posted on.
186     */
187    public void setCallback(@Nullable Callback callback, @Nullable Handler handler) {
188        synchronized (mLock) {
189            if (callback == null) {
190                if (mCallback != null) {
191                    mCallback.mCallback.mSession = null;
192                }
193                mCallback = null;
194                return;
195            }
196            if (mCallback != null) {
197                // We're updating the callback, clear the session from the old
198                // one.
199                mCallback.mCallback.mSession = null;
200            }
201            if (handler == null) {
202                handler = new Handler();
203            }
204            callback.mSession = this;
205            CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(),
206                    callback);
207            mCallback = msgHandler;
208        }
209    }
210
211    /**
212     * Set an intent for launching UI for this Session. This can be used as a
213     * quick link to an ongoing media screen. The intent should be for an
214     * activity that may be started using {@link Activity#startActivity(Intent)}.
215     *
216     * @param pi The intent to launch to show UI for this Session.
217     */
218    public void setSessionActivity(@Nullable PendingIntent pi) {
219        try {
220            mBinder.setLaunchPendingIntent(pi);
221        } catch (RemoteException e) {
222            Log.wtf(TAG, "Failure in setLaunchPendingIntent.", e);
223        }
224    }
225
226    /**
227     * 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        synchronized (mLock) {
293            mVolumeProvider = volumeProvider;
294        }
295        volumeProvider.setCallback(new VolumeProvider.Callback() {
296            @Override
297            public void onVolumeChanged(VolumeProvider volumeProvider) {
298                notifyRemoteVolumeChanged(volumeProvider);
299            }
300        });
301
302        try {
303            mBinder.setPlaybackToRemote(volumeProvider.getVolumeControl(),
304                    volumeProvider.getMaxVolume());
305            mBinder.setCurrentVolume(volumeProvider.getCurrentVolume());
306        } catch (RemoteException e) {
307            Log.wtf(TAG, "Failure in setPlaybackToRemote.", e);
308        }
309    }
310
311    /**
312     * Set if this session is currently active and ready to receive commands. If
313     * set to false your session's controller may not be discoverable. You must
314     * set the session to active before it can start receiving media button
315     * events or transport commands.
316     *
317     * @param active Whether this session is active or not.
318     */
319    public void setActive(boolean active) {
320        if (mActive == active) {
321            return;
322        }
323        try {
324            mBinder.setActive(active);
325            mActive = active;
326        } catch (RemoteException e) {
327            Log.wtf(TAG, "Failure in setActive.", e);
328        }
329    }
330
331    /**
332     * Get the current active state of this session.
333     *
334     * @return True if the session is active, false otherwise.
335     */
336    public boolean isActive() {
337        return mActive;
338    }
339
340    /**
341     * Send a proprietary event to all MediaControllers listening to this
342     * Session. It's up to the Controller/Session owner to determine the meaning
343     * of any events.
344     *
345     * @param event The name of the event to send
346     * @param extras Any extras included with the event
347     */
348    public void sendSessionEvent(@NonNull String event, @Nullable Bundle extras) {
349        if (TextUtils.isEmpty(event)) {
350            throw new IllegalArgumentException("event cannot be null or empty");
351        }
352        try {
353            mBinder.sendEvent(event, extras);
354        } catch (RemoteException e) {
355            Log.wtf(TAG, "Error sending event", e);
356        }
357    }
358
359    /**
360     * This must be called when an app has finished performing playback. If
361     * playback is expected to start again shortly the session can be left open,
362     * but it must be released if your activity or service is being destroyed.
363     */
364    public void release() {
365        try {
366            mBinder.destroy();
367        } catch (RemoteException e) {
368            Log.wtf(TAG, "Error releasing session: ", e);
369        }
370    }
371
372    /**
373     * Retrieve a token object that can be used by apps to create a
374     * {@link MediaController} for interacting with this session. The owner of
375     * the session is responsible for deciding how to distribute these tokens.
376     *
377     * @return A token that can be used to create a MediaController for this
378     *         session
379     */
380    public @NonNull Token getSessionToken() {
381        return mSessionToken;
382    }
383
384    /**
385     * Get a controller for this session. This is a convenience method to avoid
386     * having to cache your own controller in process.
387     *
388     * @return A controller for this session.
389     */
390    public @NonNull MediaController getController() {
391        return mController;
392    }
393
394    /**
395     * Update the current playback state.
396     *
397     * @param state The current state of playback
398     */
399    public void setPlaybackState(@Nullable PlaybackState state) {
400        mPlaybackState = state;
401        try {
402            mBinder.setPlaybackState(state);
403        } catch (RemoteException e) {
404            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
405        }
406    }
407
408    /**
409     * Update the current metadata. New metadata can be created using
410     * {@link android.media.MediaMetadata.Builder}.
411     *
412     * @param metadata The new metadata
413     */
414    public void setMetadata(@Nullable MediaMetadata metadata) {
415        if (metadata != null ) {
416            metadata = (new MediaMetadata.Builder(metadata, mMaxBitmapSize)).build();
417        }
418        try {
419            mBinder.setMetadata(metadata);
420        } catch (RemoteException e) {
421            Log.wtf(TAG, "Dead object in setPlaybackState.", e);
422        }
423    }
424
425    /**
426     * Update the list of items in the play queue. It is an ordered list and
427     * should contain the current item, and previous or upcoming items if they
428     * exist. Specify null if there is no current play queue.
429     * <p>
430     * The queue should be of reasonable size. If the play queue is unbounded
431     * within your app, it is better to send a reasonable amount in a sliding
432     * window instead.
433     *
434     * @param queue A list of items in the play queue.
435     */
436    public void setQueue(@Nullable List<QueueItem> queue) {
437        try {
438            mBinder.setQueue(queue == null ? null : new ParceledListSlice<QueueItem>(queue));
439        } catch (RemoteException e) {
440            Log.wtf("Dead object in setQueue.", e);
441        }
442    }
443
444    /**
445     * Set the title of the play queue. The UI should display this title along
446     * with the play queue itself.
447     * e.g. "Play Queue", "Now Playing", or an album name.
448     *
449     * @param title The title of the play queue.
450     */
451    public void setQueueTitle(@Nullable CharSequence title) {
452        try {
453            mBinder.setQueueTitle(title);
454        } catch (RemoteException e) {
455            Log.wtf("Dead object in setQueueTitle.", e);
456        }
457    }
458
459    /**
460     * Set the style of rating used by this session. Apps trying to set the
461     * rating should use this style. Must be one of the following:
462     * <ul>
463     * <li>{@link Rating#RATING_NONE}</li>
464     * <li>{@link Rating#RATING_3_STARS}</li>
465     * <li>{@link Rating#RATING_4_STARS}</li>
466     * <li>{@link Rating#RATING_5_STARS}</li>
467     * <li>{@link Rating#RATING_HEART}</li>
468     * <li>{@link Rating#RATING_PERCENTAGE}</li>
469     * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li>
470     * </ul>
471     */
472    public void setRatingType(int type) {
473        try {
474            mBinder.setRatingType(type);
475        } catch (RemoteException e) {
476            Log.e(TAG, "Error in setRatingType.", e);
477        }
478    }
479
480    /**
481     * Set some extras that can be associated with the {@link MediaSession}. No assumptions should
482     * be made as to how a {@link MediaController} will handle these extras.
483     * Keys should be fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts.
484     *
485     * @param extras The extras associated with the {@link MediaSession}.
486     */
487    public void setExtras(@Nullable Bundle extras) {
488        try {
489            mBinder.setExtras(extras);
490        } catch (RemoteException e) {
491            Log.wtf("Dead object in setExtras.", e);
492        }
493    }
494
495    /**
496     * Notify the system that the remote volume changed.
497     *
498     * @param provider The provider that is handling volume changes.
499     * @hide
500     */
501    public void notifyRemoteVolumeChanged(VolumeProvider provider) {
502        synchronized (mLock) {
503            if (provider == null || provider != mVolumeProvider) {
504                Log.w(TAG, "Received update from stale volume provider");
505                return;
506            }
507        }
508        try {
509            mBinder.setCurrentVolume(provider.getCurrentVolume());
510        } catch (RemoteException e) {
511            Log.e(TAG, "Error in notifyVolumeChanged", e);
512        }
513    }
514
515    private void dispatchPlay() {
516        postToCallback(CallbackMessageHandler.MSG_PLAY);
517    }
518
519    private void dispatchPlayFromMediaId(String mediaId, Bundle extras) {
520        postToCallback(CallbackMessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras);
521    }
522
523    private void dispatchPlayFromSearch(String query, Bundle extras) {
524        postToCallback(CallbackMessageHandler.MSG_PLAY_SEARCH, query, extras);
525    }
526
527    private void dispatchPlayFromUri(Uri uri, Bundle extras) {
528        postToCallback(CallbackMessageHandler.MSG_PLAY_URI, uri, extras);
529    }
530
531    private void dispatchSkipToItem(long id) {
532        postToCallback(CallbackMessageHandler.MSG_SKIP_TO_ITEM, id);
533    }
534
535    private void dispatchPause() {
536        postToCallback(CallbackMessageHandler.MSG_PAUSE);
537    }
538
539    private void dispatchStop() {
540        postToCallback(CallbackMessageHandler.MSG_STOP);
541    }
542
543    private void dispatchNext() {
544        postToCallback(CallbackMessageHandler.MSG_NEXT);
545    }
546
547    private void dispatchPrevious() {
548        postToCallback(CallbackMessageHandler.MSG_PREVIOUS);
549    }
550
551    private void dispatchFastForward() {
552        postToCallback(CallbackMessageHandler.MSG_FAST_FORWARD);
553    }
554
555    private void dispatchRewind() {
556        postToCallback(CallbackMessageHandler.MSG_REWIND);
557    }
558
559    private void dispatchSeekTo(long pos) {
560        postToCallback(CallbackMessageHandler.MSG_SEEK_TO, pos);
561    }
562
563    private void dispatchRate(Rating rating) {
564        postToCallback(CallbackMessageHandler.MSG_RATE, rating);
565    }
566
567    private void dispatchCustomAction(String action, Bundle args) {
568        postToCallback(CallbackMessageHandler.MSG_CUSTOM_ACTION, action, args);
569    }
570
571    private void dispatchMediaButton(Intent mediaButtonIntent) {
572        postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent);
573    }
574
575    private void dispatchAdjustVolume(int direction) {
576        postToCallback(CallbackMessageHandler.MSG_ADJUST_VOLUME, direction);
577    }
578
579    private void dispatchSetVolumeTo(int volume) {
580        postToCallback(CallbackMessageHandler.MSG_SET_VOLUME, volume);
581    }
582
583    private void postToCallback(int what) {
584        postToCallback(what, null);
585    }
586
587    private void postCommand(String command, Bundle args, ResultReceiver resultCb) {
588        Command cmd = new Command(command, args, resultCb);
589        postToCallback(CallbackMessageHandler.MSG_COMMAND, cmd);
590    }
591
592    private void postToCallback(int what, Object obj) {
593        postToCallback(what, obj, null);
594    }
595
596    private void postToCallback(int what, Object obj, Bundle extras) {
597        synchronized (mLock) {
598            if (mCallback != null) {
599                mCallback.post(what, obj, extras);
600            }
601        }
602    }
603
604    /**
605     * Return true if this is considered an active playback state.
606     *
607     * @hide
608     */
609    public static boolean isActiveState(int state) {
610        switch (state) {
611            case PlaybackState.STATE_FAST_FORWARDING:
612            case PlaybackState.STATE_REWINDING:
613            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
614            case PlaybackState.STATE_SKIPPING_TO_NEXT:
615            case PlaybackState.STATE_BUFFERING:
616            case PlaybackState.STATE_CONNECTING:
617            case PlaybackState.STATE_PLAYING:
618                return true;
619        }
620        return false;
621    }
622
623    /**
624     * Represents an ongoing session. This may be passed to apps by the session
625     * owner to allow them to create a {@link MediaController} to communicate with
626     * the session.
627     */
628    public static final class Token implements Parcelable {
629
630        private ISessionController mBinder;
631
632        /**
633         * @hide
634         */
635        public Token(ISessionController binder) {
636            mBinder = binder;
637        }
638
639        @Override
640        public int describeContents() {
641            return 0;
642        }
643
644        @Override
645        public void writeToParcel(Parcel dest, int flags) {
646            dest.writeStrongBinder(mBinder.asBinder());
647        }
648
649        @Override
650        public int hashCode() {
651            final int prime = 31;
652            int result = 1;
653            result = prime * result + ((mBinder == null) ? 0 : mBinder.asBinder().hashCode());
654            return result;
655        }
656
657        @Override
658        public boolean equals(Object obj) {
659            if (this == obj)
660                return true;
661            if (obj == null)
662                return false;
663            if (getClass() != obj.getClass())
664                return false;
665            Token other = (Token) obj;
666            if (mBinder == null) {
667                if (other.mBinder != null)
668                    return false;
669            } else if (!mBinder.asBinder().equals(other.mBinder.asBinder()))
670                return false;
671            return true;
672        }
673
674        ISessionController getBinder() {
675            return mBinder;
676        }
677
678        public static final Parcelable.Creator<Token> CREATOR
679                = new Parcelable.Creator<Token>() {
680            @Override
681            public Token createFromParcel(Parcel in) {
682                return new Token(ISessionController.Stub.asInterface(in.readStrongBinder()));
683            }
684
685            @Override
686            public Token[] newArray(int size) {
687                return new Token[size];
688            }
689        };
690    }
691
692    /**
693     * Receives media buttons, transport controls, and commands from controllers
694     * and the system. A callback may be set using {@link #setCallback}.
695     */
696    public abstract static class Callback {
697        private MediaSession mSession;
698
699        public Callback() {
700        }
701
702        /**
703         * Called when a controller has sent a command to this session.
704         * The owner of the session may handle custom commands but is not
705         * required to.
706         *
707         * @param command The command name.
708         * @param args Optional parameters for the command, may be null.
709         * @param cb A result receiver to which a result may be sent by the command, may be null.
710         */
711        public void onCommand(@NonNull String command, @Nullable Bundle args,
712                @Nullable ResultReceiver cb) {
713        }
714
715        /**
716         * Called when a media button is pressed and this session has the
717         * highest priority or a controller sends a media button event to the
718         * session. The default behavior will call the relevant method if the
719         * action for it was set.
720         * <p>
721         * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a
722         * KeyEvent in {@link Intent#EXTRA_KEY_EVENT}
723         *
724         * @param mediaButtonIntent an intent containing the KeyEvent as an
725         *            extra
726         * @return True if the event was handled, false otherwise.
727         */
728        public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
729            if (mSession != null
730                    && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
731                KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
732                if (ke != null && ke.getAction() == KeyEvent.ACTION_DOWN) {
733                    PlaybackState state = mSession.mPlaybackState;
734                    long validActions = state == null ? 0 : state.getActions();
735                    switch (ke.getKeyCode()) {
736                        case KeyEvent.KEYCODE_MEDIA_PLAY:
737                            if ((validActions & PlaybackState.ACTION_PLAY) != 0) {
738                                onPlay();
739                                return true;
740                            }
741                            break;
742                        case KeyEvent.KEYCODE_MEDIA_PAUSE:
743                            if ((validActions & PlaybackState.ACTION_PAUSE) != 0) {
744                                onPause();
745                                return true;
746                            }
747                            break;
748                        case KeyEvent.KEYCODE_MEDIA_NEXT:
749                            if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
750                                onSkipToNext();
751                                return true;
752                            }
753                            break;
754                        case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
755                            if ((validActions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
756                                onSkipToPrevious();
757                                return true;
758                            }
759                            break;
760                        case KeyEvent.KEYCODE_MEDIA_STOP:
761                            if ((validActions & PlaybackState.ACTION_STOP) != 0) {
762                                onStop();
763                                return true;
764                            }
765                            break;
766                        case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
767                            if ((validActions & PlaybackState.ACTION_FAST_FORWARD) != 0) {
768                                onFastForward();
769                                return true;
770                            }
771                            break;
772                        case KeyEvent.KEYCODE_MEDIA_REWIND:
773                            if ((validActions & PlaybackState.ACTION_REWIND) != 0) {
774                                onRewind();
775                                return true;
776                            }
777                            break;
778                        case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
779                        case KeyEvent.KEYCODE_HEADSETHOOK:
780                            boolean isPlaying = state == null ? false
781                                    : state.getState() == PlaybackState.STATE_PLAYING;
782                            boolean canPlay = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
783                                    | PlaybackState.ACTION_PLAY)) != 0;
784                            boolean canPause = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
785                                    | PlaybackState.ACTION_PAUSE)) != 0;
786                            if (isPlaying && canPause) {
787                                onPause();
788                                return true;
789                            } else if (!isPlaying && canPlay) {
790                                onPlay();
791                                return true;
792                            }
793                            break;
794                    }
795                }
796            }
797            return false;
798        }
799
800        /**
801         * Override to handle requests to begin playback.
802         */
803        public void onPlay() {
804        }
805
806        /**
807         * Override to handle requests to play a specific mediaId that was
808         * provided by your app's {@link MediaBrowserService}.
809         */
810        public void onPlayFromMediaId(String mediaId, Bundle extras) {
811        }
812
813        /**
814         * Override to handle requests to begin playback from a search query. An
815         * empty query indicates that the app may play any music. The
816         * implementation should attempt to make a smart choice about what to
817         * play.
818         */
819        public void onPlayFromSearch(String query, Bundle extras) {
820        }
821
822        /**
823         * Override to handle requests to play a specific media item represented by a URI.
824         */
825        public void onPlayFromUri(Uri uri, Bundle extras) {
826        }
827
828        /**
829         * Override to handle requests to play an item with a given id from the
830         * play queue.
831         */
832        public void onSkipToQueueItem(long id) {
833        }
834
835        /**
836         * Override to handle requests to pause playback.
837         */
838        public void onPause() {
839        }
840
841        /**
842         * Override to handle requests to skip to the next media item.
843         */
844        public void onSkipToNext() {
845        }
846
847        /**
848         * Override to handle requests to skip to the previous media item.
849         */
850        public void onSkipToPrevious() {
851        }
852
853        /**
854         * Override to handle requests to fast forward.
855         */
856        public void onFastForward() {
857        }
858
859        /**
860         * Override to handle requests to rewind.
861         */
862        public void onRewind() {
863        }
864
865        /**
866         * Override to handle requests to stop playback.
867         */
868        public void onStop() {
869        }
870
871        /**
872         * Override to handle requests to seek to a specific position in ms.
873         *
874         * @param pos New position to move to, in milliseconds.
875         */
876        public void onSeekTo(long pos) {
877        }
878
879        /**
880         * Override to handle the item being rated.
881         *
882         * @param rating
883         */
884        public void onSetRating(@NonNull Rating rating) {
885        }
886
887        /**
888         * Called when a {@link MediaController} wants a {@link PlaybackState.CustomAction} to be
889         * performed.
890         *
891         * @param action The action that was originally sent in the
892         *               {@link PlaybackState.CustomAction}.
893         * @param extras Optional extras specified by the {@link MediaController}.
894         */
895        public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
896        }
897    }
898
899    /**
900     * @hide
901     */
902    public static class CallbackStub extends ISessionCallback.Stub {
903        private WeakReference<MediaSession> mMediaSession;
904
905        public CallbackStub(MediaSession session) {
906            mMediaSession = new WeakReference<MediaSession>(session);
907        }
908
909        @Override
910        public void onCommand(String command, Bundle args, ResultReceiver cb) {
911            MediaSession session = mMediaSession.get();
912            if (session != null) {
913                session.postCommand(command, args, cb);
914            }
915        }
916
917        @Override
918        public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber,
919                ResultReceiver cb) {
920            MediaSession session = mMediaSession.get();
921            try {
922                if (session != null) {
923                    session.dispatchMediaButton(mediaButtonIntent);
924                }
925            } finally {
926                if (cb != null) {
927                    cb.send(sequenceNumber, null);
928                }
929            }
930        }
931
932        @Override
933        public void onPlay() {
934            MediaSession session = mMediaSession.get();
935            if (session != null) {
936                session.dispatchPlay();
937            }
938        }
939
940        @Override
941        public void onPlayFromMediaId(String mediaId, Bundle extras) {
942            MediaSession session = mMediaSession.get();
943            if (session != null) {
944                session.dispatchPlayFromMediaId(mediaId, extras);
945            }
946        }
947
948        @Override
949        public void onPlayFromSearch(String query, Bundle extras) {
950            MediaSession session = mMediaSession.get();
951            if (session != null) {
952                session.dispatchPlayFromSearch(query, extras);
953            }
954        }
955
956        @Override
957        public void onPlayFromUri(Uri uri, Bundle extras) {
958            MediaSession session = mMediaSession.get();
959            if (session != null) {
960                session.dispatchPlayFromUri(uri, 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        private static final int MSG_PLAY_URI = 18;
1176
1177        private MediaSession.Callback mCallback;
1178
1179        public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) {
1180            super(looper, null, true);
1181            mCallback = callback;
1182        }
1183
1184        public void post(int what, Object obj, Bundle bundle) {
1185            Message msg = obtainMessage(what, obj);
1186            msg.setData(bundle);
1187            msg.sendToTarget();
1188        }
1189
1190        public void post(int what, Object obj) {
1191            obtainMessage(what, obj).sendToTarget();
1192        }
1193
1194        public void post(int what) {
1195            post(what, null);
1196        }
1197
1198        public void post(int what, Object obj, int arg1) {
1199            obtainMessage(what, arg1, 0, obj).sendToTarget();
1200        }
1201
1202        @Override
1203        public void handleMessage(Message msg) {
1204            VolumeProvider vp;
1205            switch (msg.what) {
1206                case MSG_PLAY:
1207                    mCallback.onPlay();
1208                    break;
1209                case MSG_PLAY_MEDIA_ID:
1210                    mCallback.onPlayFromMediaId((String) msg.obj, msg.getData());
1211                    break;
1212                case MSG_PLAY_SEARCH:
1213                    mCallback.onPlayFromSearch((String) msg.obj, msg.getData());
1214                    break;
1215                case MSG_PLAY_URI:
1216                    mCallback.onPlayFromUri((Uri) msg.obj, msg.getData());
1217                    break;
1218                case MSG_SKIP_TO_ITEM:
1219                    mCallback.onSkipToQueueItem((Long) msg.obj);
1220                    break;
1221                case MSG_PAUSE:
1222                    mCallback.onPause();
1223                    break;
1224                case MSG_STOP:
1225                    mCallback.onStop();
1226                    break;
1227                case MSG_NEXT:
1228                    mCallback.onSkipToNext();
1229                    break;
1230                case MSG_PREVIOUS:
1231                    mCallback.onSkipToPrevious();
1232                    break;
1233                case MSG_FAST_FORWARD:
1234                    mCallback.onFastForward();
1235                    break;
1236                case MSG_REWIND:
1237                    mCallback.onRewind();
1238                    break;
1239                case MSG_SEEK_TO:
1240                    mCallback.onSeekTo((Long) msg.obj);
1241                    break;
1242                case MSG_RATE:
1243                    mCallback.onSetRating((Rating) msg.obj);
1244                    break;
1245                case MSG_CUSTOM_ACTION:
1246                    mCallback.onCustomAction((String) msg.obj, msg.getData());
1247                    break;
1248                case MSG_MEDIA_BUTTON:
1249                    mCallback.onMediaButtonEvent((Intent) msg.obj);
1250                    break;
1251                case MSG_COMMAND:
1252                    Command cmd = (Command) msg.obj;
1253                    mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
1254                    break;
1255                case MSG_ADJUST_VOLUME:
1256                    synchronized (mLock) {
1257                        vp = mVolumeProvider;
1258                    }
1259                    if (vp != null) {
1260                        vp.onAdjustVolume((int) msg.obj);
1261                    }
1262                    break;
1263                case MSG_SET_VOLUME:
1264                    synchronized (mLock) {
1265                        vp = mVolumeProvider;
1266                    }
1267                    if (vp != null) {
1268                        vp.onSetVolumeTo((int) msg.obj);
1269                    }
1270                    break;
1271            }
1272        }
1273    }
1274}
1275