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