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