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(@Rating.Style 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    /**
516     * Returns the name of the package that sent the last media button, transport control, or
517     * command from controllers and the system. This is only valid while in a request callback, such
518     * as {@link Callback#onPlay}.
519     *
520     * @hide
521     */
522    public String getCallingPackage() {
523        try {
524            return mBinder.getCallingPackage();
525        } catch (RemoteException e) {
526            Log.wtf(TAG, "Dead object in getCallingPackage.", e);
527        }
528        return null;
529    }
530
531    private void dispatchPrepare() {
532        postToCallback(CallbackMessageHandler.MSG_PREPARE);
533    }
534
535    private void dispatchPrepareFromMediaId(String mediaId, Bundle extras) {
536        postToCallback(CallbackMessageHandler.MSG_PREPARE_MEDIA_ID, mediaId, extras);
537    }
538
539    private void dispatchPrepareFromSearch(String query, Bundle extras) {
540        postToCallback(CallbackMessageHandler.MSG_PREPARE_SEARCH, query, extras);
541    }
542
543    private void dispatchPrepareFromUri(Uri uri, Bundle extras) {
544        postToCallback(CallbackMessageHandler.MSG_PREPARE_URI, uri, extras);
545    }
546
547    private void dispatchPlay() {
548        postToCallback(CallbackMessageHandler.MSG_PLAY);
549    }
550
551    private void dispatchPlayFromMediaId(String mediaId, Bundle extras) {
552        postToCallback(CallbackMessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras);
553    }
554
555    private void dispatchPlayFromSearch(String query, Bundle extras) {
556        postToCallback(CallbackMessageHandler.MSG_PLAY_SEARCH, query, extras);
557    }
558
559    private void dispatchPlayFromUri(Uri uri, Bundle extras) {
560        postToCallback(CallbackMessageHandler.MSG_PLAY_URI, uri, extras);
561    }
562
563    private void dispatchSkipToItem(long id) {
564        postToCallback(CallbackMessageHandler.MSG_SKIP_TO_ITEM, id);
565    }
566
567    private void dispatchPause() {
568        postToCallback(CallbackMessageHandler.MSG_PAUSE);
569    }
570
571    private void dispatchStop() {
572        postToCallback(CallbackMessageHandler.MSG_STOP);
573    }
574
575    private void dispatchNext() {
576        postToCallback(CallbackMessageHandler.MSG_NEXT);
577    }
578
579    private void dispatchPrevious() {
580        postToCallback(CallbackMessageHandler.MSG_PREVIOUS);
581    }
582
583    private void dispatchFastForward() {
584        postToCallback(CallbackMessageHandler.MSG_FAST_FORWARD);
585    }
586
587    private void dispatchRewind() {
588        postToCallback(CallbackMessageHandler.MSG_REWIND);
589    }
590
591    private void dispatchSeekTo(long pos) {
592        postToCallback(CallbackMessageHandler.MSG_SEEK_TO, pos);
593    }
594
595    private void dispatchRate(Rating rating) {
596        postToCallback(CallbackMessageHandler.MSG_RATE, rating);
597    }
598
599    private void dispatchCustomAction(String action, Bundle args) {
600        postToCallback(CallbackMessageHandler.MSG_CUSTOM_ACTION, action, args);
601    }
602
603    private void dispatchMediaButton(Intent mediaButtonIntent) {
604        postToCallback(CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent);
605    }
606
607    private void dispatchAdjustVolume(int direction) {
608        postToCallback(CallbackMessageHandler.MSG_ADJUST_VOLUME, direction);
609    }
610
611    private void dispatchSetVolumeTo(int volume) {
612        postToCallback(CallbackMessageHandler.MSG_SET_VOLUME, volume);
613    }
614
615    private void postToCallback(int what) {
616        postToCallback(what, null);
617    }
618
619    private void postCommand(String command, Bundle args, ResultReceiver resultCb) {
620        Command cmd = new Command(command, args, resultCb);
621        postToCallback(CallbackMessageHandler.MSG_COMMAND, cmd);
622    }
623
624    private void postToCallback(int what, Object obj) {
625        postToCallback(what, obj, null);
626    }
627
628    private void postToCallback(int what, Object obj, Bundle extras) {
629        synchronized (mLock) {
630            if (mCallback != null) {
631                mCallback.post(what, obj, extras);
632            }
633        }
634    }
635
636    /**
637     * Return true if this is considered an active playback state.
638     *
639     * @hide
640     */
641    public static boolean isActiveState(int state) {
642        switch (state) {
643            case PlaybackState.STATE_FAST_FORWARDING:
644            case PlaybackState.STATE_REWINDING:
645            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
646            case PlaybackState.STATE_SKIPPING_TO_NEXT:
647            case PlaybackState.STATE_BUFFERING:
648            case PlaybackState.STATE_CONNECTING:
649            case PlaybackState.STATE_PLAYING:
650                return true;
651        }
652        return false;
653    }
654
655    /**
656     * Represents an ongoing session. This may be passed to apps by the session
657     * owner to allow them to create a {@link MediaController} to communicate with
658     * the session.
659     */
660    public static final class Token implements Parcelable {
661
662        private ISessionController mBinder;
663
664        /**
665         * @hide
666         */
667        public Token(ISessionController binder) {
668            mBinder = binder;
669        }
670
671        @Override
672        public int describeContents() {
673            return 0;
674        }
675
676        @Override
677        public void writeToParcel(Parcel dest, int flags) {
678            dest.writeStrongBinder(mBinder.asBinder());
679        }
680
681        @Override
682        public int hashCode() {
683            final int prime = 31;
684            int result = 1;
685            result = prime * result + ((mBinder == null) ? 0 : mBinder.asBinder().hashCode());
686            return result;
687        }
688
689        @Override
690        public boolean equals(Object obj) {
691            if (this == obj)
692                return true;
693            if (obj == null)
694                return false;
695            if (getClass() != obj.getClass())
696                return false;
697            Token other = (Token) obj;
698            if (mBinder == null) {
699                if (other.mBinder != null)
700                    return false;
701            } else if (!mBinder.asBinder().equals(other.mBinder.asBinder()))
702                return false;
703            return true;
704        }
705
706        ISessionController getBinder() {
707            return mBinder;
708        }
709
710        public static final Parcelable.Creator<Token> CREATOR
711                = new Parcelable.Creator<Token>() {
712            @Override
713            public Token createFromParcel(Parcel in) {
714                return new Token(ISessionController.Stub.asInterface(in.readStrongBinder()));
715            }
716
717            @Override
718            public Token[] newArray(int size) {
719                return new Token[size];
720            }
721        };
722    }
723
724    /**
725     * Receives media buttons, transport controls, and commands from controllers
726     * and the system. A callback may be set using {@link #setCallback}.
727     */
728    public abstract static class Callback {
729        private MediaSession mSession;
730
731        public Callback() {
732        }
733
734        /**
735         * Called when a controller has sent a command to this session.
736         * The owner of the session may handle custom commands but is not
737         * required to.
738         *
739         * @param command The command name.
740         * @param args Optional parameters for the command, may be null.
741         * @param cb A result receiver to which a result may be sent by the command, may be null.
742         */
743        public void onCommand(@NonNull String command, @Nullable Bundle args,
744                @Nullable ResultReceiver cb) {
745        }
746
747        /**
748         * Called when a media button is pressed and this session has the
749         * highest priority or a controller sends a media button event to the
750         * session. The default behavior will call the relevant method if the
751         * action for it was set.
752         * <p>
753         * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a
754         * KeyEvent in {@link Intent#EXTRA_KEY_EVENT}
755         *
756         * @param mediaButtonIntent an intent containing the KeyEvent as an
757         *            extra
758         * @return True if the event was handled, false otherwise.
759         */
760        public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) {
761            if (mSession != null
762                    && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) {
763                KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
764                if (ke != null && ke.getAction() == KeyEvent.ACTION_DOWN) {
765                    PlaybackState state = mSession.mPlaybackState;
766                    long validActions = state == null ? 0 : state.getActions();
767                    switch (ke.getKeyCode()) {
768                        case KeyEvent.KEYCODE_MEDIA_PLAY:
769                            if ((validActions & PlaybackState.ACTION_PLAY) != 0) {
770                                onPlay();
771                                return true;
772                            }
773                            break;
774                        case KeyEvent.KEYCODE_MEDIA_PAUSE:
775                            if ((validActions & PlaybackState.ACTION_PAUSE) != 0) {
776                                onPause();
777                                return true;
778                            }
779                            break;
780                        case KeyEvent.KEYCODE_MEDIA_NEXT:
781                            if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) {
782                                onSkipToNext();
783                                return true;
784                            }
785                            break;
786                        case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
787                            if ((validActions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) {
788                                onSkipToPrevious();
789                                return true;
790                            }
791                            break;
792                        case KeyEvent.KEYCODE_MEDIA_STOP:
793                            if ((validActions & PlaybackState.ACTION_STOP) != 0) {
794                                onStop();
795                                return true;
796                            }
797                            break;
798                        case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
799                            if ((validActions & PlaybackState.ACTION_FAST_FORWARD) != 0) {
800                                onFastForward();
801                                return true;
802                            }
803                            break;
804                        case KeyEvent.KEYCODE_MEDIA_REWIND:
805                            if ((validActions & PlaybackState.ACTION_REWIND) != 0) {
806                                onRewind();
807                                return true;
808                            }
809                            break;
810                        case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
811                        case KeyEvent.KEYCODE_HEADSETHOOK:
812                            boolean isPlaying = state == null ? false
813                                    : state.getState() == PlaybackState.STATE_PLAYING;
814                            boolean canPlay = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
815                                    | PlaybackState.ACTION_PLAY)) != 0;
816                            boolean canPause = (validActions & (PlaybackState.ACTION_PLAY_PAUSE
817                                    | PlaybackState.ACTION_PAUSE)) != 0;
818                            if (isPlaying && canPause) {
819                                onPause();
820                                return true;
821                            } else if (!isPlaying && canPlay) {
822                                onPlay();
823                                return true;
824                            }
825                            break;
826                    }
827                }
828            }
829            return false;
830        }
831
832        /**
833         * Override to handle requests to prepare playback. During the preparation, a session should
834         * not hold audio focus in order to allow other sessions play seamlessly. The state of
835         * playback should be updated to {@link PlaybackState#STATE_PAUSED} after the preparation is
836         * done.
837         */
838        public void onPrepare() {
839        }
840
841        /**
842         * Override to handle requests to prepare for playing a specific mediaId that was provided
843         * by your app's {@link MediaBrowserService}. During the preparation, a session should not
844         * hold audio focus in order to allow other sessions play seamlessly. The state of playback
845         * should be updated to {@link PlaybackState#STATE_PAUSED} after the preparation is done.
846         * The playback of the prepared content should start in the implementation of
847         * {@link #onPlay}. Override {@link #onPlayFromMediaId} to handle requests for starting
848         * playback without preparation.
849         */
850        public void onPrepareFromMediaId(String mediaId, Bundle extras) {
851        }
852
853        /**
854         * Override to handle requests to prepare playback from a search query. An empty query
855         * indicates that the app may prepare any music. The implementation should attempt to make a
856         * smart choice about what to play. During the preparation, a session should not hold audio
857         * focus in order to allow other sessions play seamlessly. The state of playback should be
858         * updated to {@link PlaybackState#STATE_PAUSED} after the preparation is done. The playback
859         * of the prepared content should start in the implementation of {@link #onPlay}. Override
860         * {@link #onPlayFromSearch} to handle requests for starting playback without preparation.
861         */
862        public void onPrepareFromSearch(String query, Bundle extras) {
863        }
864
865        /**
866         * Override to handle requests to prepare a specific media item represented by a URI.
867         * During the preparation, a session should not hold audio focus in order to allow
868         * other sessions play seamlessly. The state of playback should be updated to
869         * {@link PlaybackState#STATE_PAUSED} after the preparation is done.
870         * The playback of the prepared content should start in the implementation of
871         * {@link #onPlay}. Override {@link #onPlayFromUri} to handle requests
872         * for starting playback without preparation.
873         */
874        public void onPrepareFromUri(Uri uri, Bundle extras) {
875        }
876
877        /**
878         * Override to handle requests to begin playback.
879         */
880        public void onPlay() {
881        }
882
883        /**
884         * Override to handle requests to begin playback from a search query. An
885         * empty query indicates that the app may play any music. The
886         * implementation should attempt to make a smart choice about what to
887         * play.
888         */
889        public void onPlayFromSearch(String query, Bundle extras) {
890        }
891
892        /**
893         * Override to handle requests to play a specific mediaId that was
894         * provided by your app's {@link MediaBrowserService}.
895         */
896        public void onPlayFromMediaId(String mediaId, Bundle extras) {
897        }
898
899        /**
900         * Override to handle requests to play a specific media item represented by a URI.
901         */
902        public void onPlayFromUri(Uri uri, Bundle extras) {
903        }
904
905        /**
906         * Override to handle requests to play an item with a given id from the
907         * play queue.
908         */
909        public void onSkipToQueueItem(long id) {
910        }
911
912        /**
913         * Override to handle requests to pause playback.
914         */
915        public void onPause() {
916        }
917
918        /**
919         * Override to handle requests to skip to the next media item.
920         */
921        public void onSkipToNext() {
922        }
923
924        /**
925         * Override to handle requests to skip to the previous media item.
926         */
927        public void onSkipToPrevious() {
928        }
929
930        /**
931         * Override to handle requests to fast forward.
932         */
933        public void onFastForward() {
934        }
935
936        /**
937         * Override to handle requests to rewind.
938         */
939        public void onRewind() {
940        }
941
942        /**
943         * Override to handle requests to stop playback.
944         */
945        public void onStop() {
946        }
947
948        /**
949         * Override to handle requests to seek to a specific position in ms.
950         *
951         * @param pos New position to move to, in milliseconds.
952         */
953        public void onSeekTo(long pos) {
954        }
955
956        /**
957         * Override to handle the item being rated.
958         *
959         * @param rating
960         */
961        public void onSetRating(@NonNull Rating rating) {
962        }
963
964        /**
965         * Called when a {@link MediaController} wants a {@link PlaybackState.CustomAction} to be
966         * performed.
967         *
968         * @param action The action that was originally sent in the
969         *               {@link PlaybackState.CustomAction}.
970         * @param extras Optional extras specified by the {@link MediaController}.
971         */
972        public void onCustomAction(@NonNull String action, @Nullable Bundle extras) {
973        }
974    }
975
976    /**
977     * @hide
978     */
979    public static class CallbackStub extends ISessionCallback.Stub {
980        private WeakReference<MediaSession> mMediaSession;
981
982        public CallbackStub(MediaSession session) {
983            mMediaSession = new WeakReference<MediaSession>(session);
984        }
985
986        @Override
987        public void onCommand(String command, Bundle args, ResultReceiver cb) {
988            MediaSession session = mMediaSession.get();
989            if (session != null) {
990                session.postCommand(command, args, cb);
991            }
992        }
993
994        @Override
995        public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber,
996                ResultReceiver cb) {
997            MediaSession session = mMediaSession.get();
998            try {
999                if (session != null) {
1000                    session.dispatchMediaButton(mediaButtonIntent);
1001                }
1002            } finally {
1003                if (cb != null) {
1004                    cb.send(sequenceNumber, null);
1005                }
1006            }
1007        }
1008
1009        @Override
1010        public void onPrepare() {
1011            MediaSession session = mMediaSession.get();
1012            if (session != null) {
1013                session.dispatchPrepare();
1014            }
1015        }
1016
1017        @Override
1018        public void onPrepareFromMediaId(String mediaId, Bundle extras) {
1019            MediaSession session = mMediaSession.get();
1020            if (session != null) {
1021                session.dispatchPrepareFromMediaId(mediaId, extras);
1022            }
1023        }
1024
1025        @Override
1026        public void onPrepareFromSearch(String query, Bundle extras) {
1027            MediaSession session = mMediaSession.get();
1028            if (session != null) {
1029                session.dispatchPrepareFromSearch(query, extras);
1030            }
1031        }
1032
1033        @Override
1034        public void onPrepareFromUri(Uri uri, Bundle extras) {
1035            MediaSession session = mMediaSession.get();
1036            if (session != null) {
1037                session.dispatchPrepareFromUri(uri, extras);
1038            }
1039        }
1040
1041        @Override
1042        public void onPlay() {
1043            MediaSession session = mMediaSession.get();
1044            if (session != null) {
1045                session.dispatchPlay();
1046            }
1047        }
1048
1049        @Override
1050        public void onPlayFromMediaId(String mediaId, Bundle extras) {
1051            MediaSession session = mMediaSession.get();
1052            if (session != null) {
1053                session.dispatchPlayFromMediaId(mediaId, extras);
1054            }
1055        }
1056
1057        @Override
1058        public void onPlayFromSearch(String query, Bundle extras) {
1059            MediaSession session = mMediaSession.get();
1060            if (session != null) {
1061                session.dispatchPlayFromSearch(query, extras);
1062            }
1063        }
1064
1065        @Override
1066        public void onPlayFromUri(Uri uri, Bundle extras) {
1067            MediaSession session = mMediaSession.get();
1068            if (session != null) {
1069                session.dispatchPlayFromUri(uri, extras);
1070            }
1071        }
1072
1073        @Override
1074        public void onSkipToTrack(long id) {
1075            MediaSession session = mMediaSession.get();
1076            if (session != null) {
1077                session.dispatchSkipToItem(id);
1078            }
1079        }
1080
1081        @Override
1082        public void onPause() {
1083            MediaSession session = mMediaSession.get();
1084            if (session != null) {
1085                session.dispatchPause();
1086            }
1087        }
1088
1089        @Override
1090        public void onStop() {
1091            MediaSession session = mMediaSession.get();
1092            if (session != null) {
1093                session.dispatchStop();
1094            }
1095        }
1096
1097        @Override
1098        public void onNext() {
1099            MediaSession session = mMediaSession.get();
1100            if (session != null) {
1101                session.dispatchNext();
1102            }
1103        }
1104
1105        @Override
1106        public void onPrevious() {
1107            MediaSession session = mMediaSession.get();
1108            if (session != null) {
1109                session.dispatchPrevious();
1110            }
1111        }
1112
1113        @Override
1114        public void onFastForward() {
1115            MediaSession session = mMediaSession.get();
1116            if (session != null) {
1117                session.dispatchFastForward();
1118            }
1119        }
1120
1121        @Override
1122        public void onRewind() {
1123            MediaSession session = mMediaSession.get();
1124            if (session != null) {
1125                session.dispatchRewind();
1126            }
1127        }
1128
1129        @Override
1130        public void onSeekTo(long pos) {
1131            MediaSession session = mMediaSession.get();
1132            if (session != null) {
1133                session.dispatchSeekTo(pos);
1134            }
1135        }
1136
1137        @Override
1138        public void onRate(Rating rating) {
1139            MediaSession session = mMediaSession.get();
1140            if (session != null) {
1141                session.dispatchRate(rating);
1142            }
1143        }
1144
1145        @Override
1146        public void onCustomAction(String action, Bundle args) {
1147            MediaSession session = mMediaSession.get();
1148            if (session != null) {
1149                session.dispatchCustomAction(action, args);
1150            }
1151        }
1152
1153        @Override
1154        public void onAdjustVolume(int direction) {
1155            MediaSession session = mMediaSession.get();
1156            if (session != null) {
1157                session.dispatchAdjustVolume(direction);
1158            }
1159        }
1160
1161        @Override
1162        public void onSetVolumeTo(int value) {
1163            MediaSession session = mMediaSession.get();
1164            if (session != null) {
1165                session.dispatchSetVolumeTo(value);
1166            }
1167        }
1168
1169    }
1170
1171    /**
1172     * A single item that is part of the play queue. It contains a description
1173     * of the item and its id in the queue.
1174     */
1175    public static final class QueueItem implements Parcelable {
1176        /**
1177         * This id is reserved. No items can be explicitly asigned this id.
1178         */
1179        public static final int UNKNOWN_ID = -1;
1180
1181        private final MediaDescription mDescription;
1182        private final long mId;
1183
1184        /**
1185         * Create a new {@link MediaSession.QueueItem}.
1186         *
1187         * @param description The {@link MediaDescription} for this item.
1188         * @param id An identifier for this item. It must be unique within the
1189         *            play queue and cannot be {@link #UNKNOWN_ID}.
1190         */
1191        public QueueItem(MediaDescription description, long id) {
1192            if (description == null) {
1193                throw new IllegalArgumentException("Description cannot be null.");
1194            }
1195            if (id == UNKNOWN_ID) {
1196                throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID");
1197            }
1198            mDescription = description;
1199            mId = id;
1200        }
1201
1202        private QueueItem(Parcel in) {
1203            mDescription = MediaDescription.CREATOR.createFromParcel(in);
1204            mId = in.readLong();
1205        }
1206
1207        /**
1208         * Get the description for this item.
1209         */
1210        public MediaDescription getDescription() {
1211            return mDescription;
1212        }
1213
1214        /**
1215         * Get the queue id for this item.
1216         */
1217        public long getQueueId() {
1218            return mId;
1219        }
1220
1221        @Override
1222        public void writeToParcel(Parcel dest, int flags) {
1223            mDescription.writeToParcel(dest, flags);
1224            dest.writeLong(mId);
1225        }
1226
1227        @Override
1228        public int describeContents() {
1229            return 0;
1230        }
1231
1232        public static final Creator<MediaSession.QueueItem> CREATOR = new Creator<MediaSession.QueueItem>() {
1233
1234            @Override
1235            public MediaSession.QueueItem createFromParcel(Parcel p) {
1236                return new MediaSession.QueueItem(p);
1237            }
1238
1239            @Override
1240            public MediaSession.QueueItem[] newArray(int size) {
1241                return new MediaSession.QueueItem[size];
1242            }
1243        };
1244
1245        @Override
1246        public String toString() {
1247            return "MediaSession.QueueItem {" +
1248                    "Description=" + mDescription +
1249                    ", Id=" + mId + " }";
1250        }
1251    }
1252
1253    private static final class Command {
1254        public final String command;
1255        public final Bundle extras;
1256        public final ResultReceiver stub;
1257
1258        public Command(String command, Bundle extras, ResultReceiver stub) {
1259            this.command = command;
1260            this.extras = extras;
1261            this.stub = stub;
1262        }
1263    }
1264
1265    private class CallbackMessageHandler extends Handler {
1266
1267        private static final int MSG_COMMAND = 1;
1268        private static final int MSG_MEDIA_BUTTON = 2;
1269        private static final int MSG_PREPARE = 3;
1270        private static final int MSG_PREPARE_MEDIA_ID = 4;
1271        private static final int MSG_PREPARE_SEARCH = 5;
1272        private static final int MSG_PREPARE_URI = 6;
1273        private static final int MSG_PLAY = 7;
1274        private static final int MSG_PLAY_MEDIA_ID = 8;
1275        private static final int MSG_PLAY_SEARCH = 9;
1276        private static final int MSG_PLAY_URI = 10;
1277        private static final int MSG_SKIP_TO_ITEM = 11;
1278        private static final int MSG_PAUSE = 12;
1279        private static final int MSG_STOP = 13;
1280        private static final int MSG_NEXT = 14;
1281        private static final int MSG_PREVIOUS = 15;
1282        private static final int MSG_FAST_FORWARD = 16;
1283        private static final int MSG_REWIND = 17;
1284        private static final int MSG_SEEK_TO = 18;
1285        private static final int MSG_RATE = 19;
1286        private static final int MSG_CUSTOM_ACTION = 20;
1287        private static final int MSG_ADJUST_VOLUME = 21;
1288        private static final int MSG_SET_VOLUME = 22;
1289
1290        private MediaSession.Callback mCallback;
1291
1292        public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) {
1293            super(looper, null, true);
1294            mCallback = callback;
1295        }
1296
1297        public void post(int what, Object obj, Bundle bundle) {
1298            Message msg = obtainMessage(what, obj);
1299            msg.setData(bundle);
1300            msg.sendToTarget();
1301        }
1302
1303        public void post(int what, Object obj) {
1304            obtainMessage(what, obj).sendToTarget();
1305        }
1306
1307        public void post(int what) {
1308            post(what, null);
1309        }
1310
1311        public void post(int what, Object obj, int arg1) {
1312            obtainMessage(what, arg1, 0, obj).sendToTarget();
1313        }
1314
1315        @Override
1316        public void handleMessage(Message msg) {
1317            VolumeProvider vp;
1318            switch (msg.what) {
1319                case MSG_COMMAND:
1320                    Command cmd = (Command) msg.obj;
1321                    mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
1322                    break;
1323                case MSG_MEDIA_BUTTON:
1324                    mCallback.onMediaButtonEvent((Intent) msg.obj);
1325                    break;
1326                case MSG_PREPARE:
1327                    mCallback.onPrepare();
1328                    break;
1329                case MSG_PREPARE_MEDIA_ID:
1330                    mCallback.onPrepareFromMediaId((String) msg.obj, msg.getData());
1331                    break;
1332                case MSG_PREPARE_SEARCH:
1333                    mCallback.onPrepareFromSearch((String) msg.obj, msg.getData());
1334                    break;
1335                case MSG_PREPARE_URI:
1336                    mCallback.onPrepareFromUri((Uri) msg.obj, msg.getData());
1337                    break;
1338                case MSG_PLAY:
1339                    mCallback.onPlay();
1340                    break;
1341                case MSG_PLAY_MEDIA_ID:
1342                    mCallback.onPlayFromMediaId((String) msg.obj, msg.getData());
1343                    break;
1344                case MSG_PLAY_SEARCH:
1345                    mCallback.onPlayFromSearch((String) msg.obj, msg.getData());
1346                    break;
1347                case MSG_PLAY_URI:
1348                    mCallback.onPlayFromUri((Uri) msg.obj, msg.getData());
1349                    break;
1350                case MSG_SKIP_TO_ITEM:
1351                    mCallback.onSkipToQueueItem((Long) msg.obj);
1352                    break;
1353                case MSG_PAUSE:
1354                    mCallback.onPause();
1355                    break;
1356                case MSG_STOP:
1357                    mCallback.onStop();
1358                    break;
1359                case MSG_NEXT:
1360                    mCallback.onSkipToNext();
1361                    break;
1362                case MSG_PREVIOUS:
1363                    mCallback.onSkipToPrevious();
1364                    break;
1365                case MSG_FAST_FORWARD:
1366                    mCallback.onFastForward();
1367                    break;
1368                case MSG_REWIND:
1369                    mCallback.onRewind();
1370                    break;
1371                case MSG_SEEK_TO:
1372                    mCallback.onSeekTo((Long) msg.obj);
1373                    break;
1374                case MSG_RATE:
1375                    mCallback.onSetRating((Rating) msg.obj);
1376                    break;
1377                case MSG_CUSTOM_ACTION:
1378                    mCallback.onCustomAction((String) msg.obj, msg.getData());
1379                    break;
1380                case MSG_ADJUST_VOLUME:
1381                    synchronized (mLock) {
1382                        vp = mVolumeProvider;
1383                    }
1384                    if (vp != null) {
1385                        vp.onAdjustVolume((int) msg.obj);
1386                    }
1387                    break;
1388                case MSG_SET_VOLUME:
1389                    synchronized (mLock) {
1390                        vp = mVolumeProvider;
1391                    }
1392                    if (vp != null) {
1393                        vp.onSetVolumeTo((int) msg.obj);
1394                    }
1395                    break;
1396            }
1397        }
1398    }
1399}
1400