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