MediaSessionCompat.java revision 5c9469e010106467791b47b0fa83efda84491a21
1
2/*
3 * Copyright (C) 2014 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package android.support.v4.media.session;
19
20import android.app.Activity;
21import android.app.PendingIntent;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.media.AudioManager;
26import android.os.Bundle;
27import android.os.Handler;
28import android.os.Parcel;
29import android.os.Parcelable;
30import android.os.ResultReceiver;
31import android.support.v4.media.MediaDescriptionCompat;
32import android.support.v4.media.MediaMetadataCompat;
33import android.support.v4.media.RatingCompat;
34import android.support.v4.media.VolumeProviderCompat;
35import android.text.TextUtils;
36
37import java.util.ArrayList;
38import java.util.List;
39
40/**
41 * Allows interaction with media controllers, volume keys, media buttons, and
42 * transport controls.
43 * <p>
44 * A MediaSession should be created when an app wants to publish media playback
45 * information or handle media keys. In general an app only needs one session
46 * for all playback, though multiple sessions can be created to provide finer
47 * grain controls of media.
48 * <p>
49 * Once a session is created the owner of the session may pass its
50 * {@link #getSessionToken() session token} to other processes to allow them to
51 * create a {@link MediaControllerCompat} to interact with the session.
52 * <p>
53 * To receive commands, media keys, and other events a {@link Callback} must be
54 * set with {@link #setCallback(Callback)}.
55 * <p>
56 * When an app is finished performing playback it must call {@link #release()}
57 * to clean up the session and notify any controllers.
58 * <p>
59 * MediaSessionCompat objects are not thread safe and all calls should be made
60 * from the same thread.
61 * <p>
62 * This is a helper for accessing features in
63 * {@link android.media.session.MediaSession} introduced after API level 4 in a
64 * backwards compatible fashion.
65 */
66public class MediaSessionCompat {
67    private final MediaSessionImpl mImpl;
68    private final MediaControllerCompat mController;
69    private final ArrayList<OnActiveChangeListener>
70            mActiveListeners = new ArrayList<OnActiveChangeListener>();
71
72    /**
73     * Set this flag on the session to indicate that it can handle media button
74     * events.
75     */
76    public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0;
77
78    /**
79     * Set this flag on the session to indicate that it handles transport
80     * control commands through its {@link Callback}.
81     */
82    public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1;
83
84    /**
85     * Creates a new session.
86     *
87     * @param context The context.
88     * @param tag A short name for debugging purposes.
89     * @param mediaButtonEventReceiver The component name for your receiver.
90     *            This must be non-null to support platform versions earlier
91     *            than {@link android.os.Build.VERSION_CODES#LOLLIPOP}.
92     * @param mbrIntent The PendingIntent for your receiver component that
93     *            handles media button events. This is optional and will be used
94     *            on {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} and
95     *            later instead of the component name.
96     */
97    public MediaSessionCompat(Context context, String tag, ComponentName mediaButtonEventReceiver,
98            PendingIntent mbrIntent) {
99        if (context == null) {
100            throw new IllegalArgumentException("context must not be null");
101        }
102        if (TextUtils.isEmpty(tag)) {
103            throw new IllegalArgumentException("tag must not be null or empty");
104        }
105
106        if (android.os.Build.VERSION.SDK_INT >= 21) {
107            mImpl = new MediaSessionImplApi21(context, tag);
108            mImpl.setMediaButtonReceiver(mbrIntent);
109        } else {
110            mImpl = new MediaSessionImplBase(context, mediaButtonEventReceiver, mbrIntent);
111        }
112        mController = new MediaControllerCompat(context, this);
113    }
114
115    private MediaSessionCompat(Context context, MediaSessionImpl impl) {
116        mImpl = impl;
117        mController = new MediaControllerCompat(context, this);
118    }
119
120    /**
121     * Add a callback to receive updates on for the MediaSession. This includes
122     * media button and volume events. The caller's thread will be used to post
123     * events.
124     *
125     * @param callback The callback object
126     */
127    public void setCallback(Callback callback) {
128        setCallback(callback, null);
129    }
130
131    /**
132     * Set the callback to receive updates for the MediaSession. This includes
133     * media button and volume events. Set the callback to null to stop
134     * receiving events.
135     *
136     * @param callback The callback to receive updates on.
137     * @param handler The handler that events should be posted on.
138     */
139    public void setCallback(Callback callback, Handler handler) {
140        mImpl.setCallback(callback, handler != null ? handler : new Handler());
141    }
142
143    /**
144     * Set an intent for launching UI for this Session. This can be used as a
145     * quick link to an ongoing media screen. The intent should be for an
146     * activity that may be started using
147     * {@link Activity#startActivity(Intent)}.
148     *
149     * @param pi The intent to launch to show UI for this Session.
150     */
151    public void setSessionActivity(PendingIntent pi) {
152        mImpl.setSessionActivity(pi);
153    }
154
155    /**
156     * Set a pending intent for your media button receiver to allow restarting
157     * playback after the session has been stopped. If your app is started in
158     * this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via
159     * the pending intent.
160     * <p>
161     * This method will only work on
162     * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. Earlier
163     * platform versions must include the media button receiver in the
164     * constructor.
165     *
166     * @param mbr The {@link PendingIntent} to send the media button event to.
167     */
168    public void setMediaButtonReceiver(PendingIntent mbr) {
169        mImpl.setMediaButtonReceiver(mbr);
170    }
171
172    /**
173     * Set any flags for the session.
174     *
175     * @param flags The flags to set for this session.
176     */
177    public void setFlags(int flags) {
178        mImpl.setFlags(flags);
179    }
180
181    /**
182     * Set the stream this session is playing on. This will affect the system's
183     * volume handling for this session. If {@link #setPlaybackToRemote} was
184     * previously called it will stop receiving volume commands and the system
185     * will begin sending volume changes to the appropriate stream.
186     * <p>
187     * By default sessions are on {@link AudioManager#STREAM_MUSIC}.
188     *
189     * @param stream The {@link AudioManager} stream this session is playing on.
190     */
191    public void setPlaybackToLocal(int stream) {
192        mImpl.setPlaybackToLocal(stream);
193    }
194
195    /**
196     * Configure this session to use remote volume handling. This must be called
197     * to receive volume button events, otherwise the system will adjust the
198     * current stream volume for this session. If {@link #setPlaybackToLocal}
199     * was previously called that stream will stop receiving volume changes for
200     * this session.
201     *
202     * @param volumeProvider The provider that will handle volume changes. May
203     *            not be null.
204     */
205    public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) {
206        if (volumeProvider == null) {
207            throw new IllegalArgumentException("volumeProvider may not be null!");
208        }
209        mImpl.setPlaybackToRemote(volumeProvider);
210    }
211
212    /**
213     * Set if this session is currently active and ready to receive commands. If
214     * set to false your session's controller may not be discoverable. You must
215     * set the session to active before it can start receiving media button
216     * events or transport commands.
217     * <p>
218     * On platforms earlier than
219     * {@link android.os.Build.VERSION_CODES#LOLLIPOP},
220     * {@link #setMediaButtonReceiver(PendingIntent)} must be called before
221     * setting this to true.
222     *
223     * @param active Whether this session is active or not.
224     */
225    public void setActive(boolean active) {
226        mImpl.setActive(active);
227        for (OnActiveChangeListener listener : mActiveListeners) {
228            listener.onActiveChanged();
229        }
230    }
231
232    /**
233     * Get the current active state of this session.
234     *
235     * @return True if the session is active, false otherwise.
236     */
237    public boolean isActive() {
238        return mImpl.isActive();
239    }
240
241    /**
242     * Send a proprietary event to all MediaControllers listening to this
243     * Session. It's up to the Controller/Session owner to determine the meaning
244     * of any events.
245     *
246     * @param event The name of the event to send
247     * @param extras Any extras included with the event
248     */
249    public void sendSessionEvent(String event, Bundle extras) {
250        if (TextUtils.isEmpty(event)) {
251            throw new IllegalArgumentException("event cannot be null or empty");
252        }
253        mImpl.sendSessionEvent(event, extras);
254    }
255
256    /**
257     * This must be called when an app has finished performing playback. If
258     * playback is expected to start again shortly the session can be left open,
259     * but it must be released if your activity or service is being destroyed.
260     */
261    public void release() {
262        mImpl.release();
263    }
264
265    /**
266     * Retrieve a token object that can be used by apps to create a
267     * {@link MediaControllerCompat} for interacting with this session. The owner of
268     * the session is responsible for deciding how to distribute these tokens.
269     *
270     * @return A token that can be used to create a MediaController for this
271     *         session.
272     */
273    public Token getSessionToken() {
274        return mImpl.getSessionToken();
275    }
276
277    /**
278     * Get a controller for this session. This is a convenience method to avoid
279     * having to cache your own controller in process.
280     *
281     * @return A controller for this session.
282     */
283    public MediaControllerCompat getController() {
284        return mController;
285    }
286
287    /**
288     * Update the current playback state.
289     *
290     * @param state The current state of playback
291     */
292    public void setPlaybackState(PlaybackStateCompat state) {
293        mImpl.setPlaybackState(state);
294    }
295
296    /**
297     * Update the current metadata. New metadata can be created using
298     * {@link android.media.MediaMetadata.Builder}.
299     *
300     * @param metadata The new metadata
301     */
302    public void setMetadata(MediaMetadataCompat metadata) {
303        mImpl.setMetadata(metadata);
304    }
305
306    /**
307     * Update the list of items in the play queue. It is an ordered list and
308     * should contain the current item, and previous or upcoming items if they
309     * exist. Specify null if there is no current play queue.
310     * <p>
311     * The queue should be of reasonable size. If the play queue is unbounded
312     * within your app, it is better to send a reasonable amount in a sliding
313     * window instead.
314     *
315     * @param queue A list of items in the play queue.
316     */
317    public void setQueue(List<QueueItem> queue) {
318        mImpl.setQueue(queue);
319    }
320
321    /**
322     * Set the title of the play queue. The UI should display this title along
323     * with the play queue itself. e.g. "Play Queue", "Now Playing", or an album
324     * name.
325     *
326     * @param title The title of the play queue.
327     */
328    public void setQueueTitle(CharSequence title) {
329        mImpl.setQueueTitle(title);
330    }
331
332    /**
333     * Set the style of rating used by this session. Apps trying to set the
334     * rating should use this style. Must be one of the following:
335     * <ul>
336     * <li>{@link RatingCompat#RATING_NONE}</li>
337     * <li>{@link RatingCompat#RATING_3_STARS}</li>
338     * <li>{@link RatingCompat#RATING_4_STARS}</li>
339     * <li>{@link RatingCompat#RATING_5_STARS}</li>
340     * <li>{@link RatingCompat#RATING_HEART}</li>
341     * <li>{@link RatingCompat#RATING_PERCENTAGE}</li>
342     * <li>{@link RatingCompat#RATING_THUMB_UP_DOWN}</li>
343     * </ul>
344     */
345    public void setRatingType(int type) {
346        mImpl.setRatingType(type);
347    }
348
349    /**
350     * Set some extras that can be associated with the
351     * {@link MediaSessionCompat}. No assumptions should be made as to how a
352     * {@link MediaControllerCompat} will handle these extras. Keys should be
353     * fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts.
354     *
355     * @param extras The extras associated with the session.
356     */
357    public void setExtras(Bundle extras) {
358        mImpl.setExtras(extras);
359    }
360
361    /**
362     * Gets the underlying framework {@link android.media.session.MediaSession}
363     * object.
364     * <p>
365     * This method is only supported on API 21+.
366     * </p>
367     *
368     * @return The underlying {@link android.media.session.MediaSession} object,
369     *         or null if none.
370     */
371    public Object getMediaSession() {
372        return mImpl.getMediaSession();
373    }
374
375    /**
376     * Gets the underlying framework {@link android.media.RemoteControlClient}
377     * object.
378     * <p>
379     * This method is only supported on APIs 14-20. On API 21+
380     * {@link #getMediaSession()} should be used instead.
381     *
382     * @return The underlying {@link android.media.RemoteControlClient} object,
383     *         or null if none.
384     */
385    public Object getRemoteControlClient() {
386        return mImpl.getRemoteControlClient();
387    }
388
389    /**
390     * Adds a listener to be notified when the active status of this session
391     * changes. This is primarily used by the support library and should not be
392     * needed by apps.
393     *
394     * @param listener The listener to add.
395     */
396    public void addOnActiveChangeListener(OnActiveChangeListener listener) {
397        if (listener == null) {
398            throw new IllegalArgumentException("Listener may not be null");
399        }
400        mActiveListeners.add(listener);
401    }
402
403    /**
404     * Stops the listener from being notified when the active status of this
405     * session changes.
406     *
407     * @param listener The listener to remove.
408     */
409    public void removeOnActiveChangeListener(OnActiveChangeListener listener) {
410        if (listener == null) {
411            throw new IllegalArgumentException("Listener may not be null");
412        }
413        mActiveListeners.remove(listener);
414    }
415
416    /**
417     * Obtain a compat wrapper for an existing MediaSession.
418     *
419     * @param mediaSession The {@link android.media.session.MediaSession} to
420     *            wrap.
421     * @return A compat wrapper for the provided session.
422     */
423    public static MediaSessionCompat obtain(Context context, Object mediaSession) {
424        return new MediaSessionCompat(context, new MediaSessionImplApi21(mediaSession));
425    }
426
427    /**
428     * Receives transport controls, media buttons, and commands from controllers
429     * and the system. The callback may be set using {@link #setCallback}.
430     */
431    public abstract static class Callback {
432        final Object mCallbackObj;
433
434        public Callback() {
435            if (android.os.Build.VERSION.SDK_INT >= 21) {
436                mCallbackObj = MediaSessionCompatApi21.createCallback(new StubApi21());
437            } else {
438                mCallbackObj = null;
439            }
440        }
441
442        /**
443         * Called when a controller has sent a custom command to this session.
444         * The owner of the session may handle custom commands but is not
445         * required to.
446         *
447         * @param command The command name.
448         * @param extras Optional parameters for the command, may be null.
449         * @param cb A result receiver to which a result may be sent by the command, may be null.
450         */
451        public void onCommand(String command, Bundle extras, ResultReceiver cb) {
452        }
453
454        /**
455         * Override to handle media button events.
456         *
457         * @param mediaButtonEvent The media button event intent.
458         * @return True if the event was handled, false otherwise.
459         */
460        public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
461            return false;
462        }
463
464        /**
465         * Override to handle requests to begin playback.
466         */
467        public void onPlay() {
468        }
469
470        /**
471         * Override to handle requests to pause playback.
472         */
473        public void onPause() {
474        }
475
476        /**
477         * Override to handle requests to skip to the next media item.
478         */
479        public void onSkipToNext() {
480        }
481
482        /**
483         * Override to handle requests to skip to the previous media item.
484         */
485        public void onSkipToPrevious() {
486        }
487
488        /**
489         * Override to handle requests to fast forward.
490         */
491        public void onFastForward() {
492        }
493
494        /**
495         * Override to handle requests to rewind.
496         */
497        public void onRewind() {
498        }
499
500        /**
501         * Override to handle requests to stop playback.
502         */
503        public void onStop() {
504        }
505
506        /**
507         * Override to handle requests to seek to a specific position in ms.
508         *
509         * @param pos New position to move to, in milliseconds.
510         */
511        public void onSeekTo(long pos) {
512        }
513
514        /**
515         * Override to handle the item being rated.
516         *
517         * @param rating
518         */
519        public void onSetRating(RatingCompat rating) {
520        }
521
522        private class StubApi21 implements MediaSessionCompatApi21.Callback {
523
524            @Override
525            public void onCommand(String command, Bundle extras, ResultReceiver cb) {
526                Callback.this.onCommand(command, extras, cb);
527            }
528
529            @Override
530            public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
531                return Callback.this.onMediaButtonEvent(mediaButtonIntent);
532            }
533
534            @Override
535            public void onPlay() {
536                Callback.this.onPlay();
537            }
538
539            @Override
540            public void onPause() {
541                Callback.this.onPause();
542            }
543
544            @Override
545            public void onSkipToNext() {
546                Callback.this.onSkipToNext();
547            }
548
549            @Override
550            public void onSkipToPrevious() {
551                Callback.this.onSkipToPrevious();
552            }
553
554            @Override
555            public void onFastForward() {
556                Callback.this.onFastForward();
557            }
558
559            @Override
560            public void onRewind() {
561                Callback.this.onRewind();
562            }
563
564            @Override
565            public void onStop() {
566                Callback.this.onStop();
567            }
568
569            @Override
570            public void onSeekTo(long pos) {
571                Callback.this.onSeekTo(pos);
572            }
573
574            @Override
575            public void onSetRating(Object ratingObj) {
576                Callback.this.onSetRating(RatingCompat.fromRating(ratingObj));
577            }
578        }
579    }
580
581    /**
582     * Represents an ongoing session. This may be passed to apps by the session
583     * owner to allow them to create a {@link MediaControllerCompat} to communicate with
584     * the session.
585     */
586    public static final class Token implements Parcelable {
587        private final Parcelable mInner;
588
589        Token(Parcelable inner) {
590            mInner = inner;
591        }
592
593        /**
594         * Creates a compat Token from a framework
595         * {@link android.media.session.MediaSession.Token} object.
596         * <p>
597         * This method is only supported on
598         * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later.
599         * </p>
600         *
601         * @param token The framework token object.
602         * @return A compat Token for use with {@link MediaControllerCompat}.
603         */
604        public static Token fromToken(Object token) {
605            if (token == null || android.os.Build.VERSION.SDK_INT < 21) {
606                return null;
607            }
608            return new Token((Parcelable) MediaSessionCompatApi21.verifyToken(token));
609        }
610
611        @Override
612        public int describeContents() {
613            return mInner.describeContents();
614        }
615
616        @Override
617        public void writeToParcel(Parcel dest, int flags) {
618            dest.writeParcelable(mInner, flags);
619        }
620
621        /**
622         * Gets the underlying framework {@link android.media.session.MediaSession.Token} object.
623         * <p>
624         * This method is only supported on API 21+.
625         * </p>
626         *
627         * @return The underlying {@link android.media.session.MediaSession.Token} object,
628         * or null if none.
629         */
630        public Object getToken() {
631            return mInner;
632        }
633
634        public static final Parcelable.Creator<Token> CREATOR
635                = new Parcelable.Creator<Token>() {
636            @Override
637            public Token createFromParcel(Parcel in) {
638                return new Token(in.readParcelable(null));
639            }
640
641            @Override
642            public Token[] newArray(int size) {
643                return new Token[size];
644            }
645        };
646    }
647
648    /**
649     * A single item that is part of the play queue. It contains a description
650     * of the item and its id in the queue.
651     */
652    public static final class QueueItem implements Parcelable {
653        /**
654         * This id is reserved. No items can be explicitly asigned this id.
655         */
656        public static final int UNKNOWN_ID = -1;
657
658        private final MediaDescriptionCompat mDescription;
659        private final long mId;
660
661        private Object mItem;
662
663        /**
664         * Create a new {@link MediaSessionCompat.QueueItem}.
665         *
666         * @param description The {@link MediaDescriptionCompat} for this item.
667         * @param id An identifier for this item. It must be unique within the
668         *            play queue and cannot be {@link #UNKNOWN_ID}.
669         */
670        public QueueItem(MediaDescriptionCompat description, long id) {
671            this(null, description, id);
672        }
673
674        private QueueItem(Object queueItem, MediaDescriptionCompat description, long id) {
675            if (description == null) {
676                throw new IllegalArgumentException("Description cannot be null.");
677            }
678            if (id == UNKNOWN_ID) {
679                throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID");
680            }
681            mDescription = description;
682            mId = id;
683            mItem = queueItem;
684        }
685
686        private QueueItem(Parcel in) {
687            mDescription = MediaDescriptionCompat.CREATOR.createFromParcel(in);
688            mId = in.readLong();
689        }
690
691        /**
692         * Get the description for this item.
693         */
694        public MediaDescriptionCompat getDescription() {
695            return mDescription;
696        }
697
698        /**
699         * Get the queue id for this item.
700         */
701        public long getQueueId() {
702            return mId;
703        }
704
705        @Override
706        public void writeToParcel(Parcel dest, int flags) {
707            mDescription.writeToParcel(dest, flags);
708            dest.writeLong(mId);
709        }
710
711        @Override
712        public int describeContents() {
713            return 0;
714        }
715
716        /**
717         * Get the underlying
718         * {@link android.media.session.MediaSession.QueueItem}.
719         * <p>
720         * On builds before {@link android.os.Build.VERSION_CODES#LOLLIPOP} null
721         * is returned.
722         *
723         * @return The underlying
724         *         {@link android.media.session.MediaSession.QueueItem} or null.
725         */
726        public Object getQueueItem() {
727            if (mItem != null || android.os.Build.VERSION.SDK_INT < 21) {
728                return mItem;
729            }
730            mItem = MediaSessionCompatApi21.QueueItem.createItem(mDescription.getMediaDescription(),
731                    mId);
732            return mItem;
733        }
734
735        /**
736         * Obtain a compat wrapper for an existing QueueItem.
737         *
738         * @param queueItem The {@link android.media.session.MediaSession.QueueItem} to
739         *            wrap.
740         * @return A compat wrapper for the provided item.
741         */
742        public static QueueItem obtain(Object queueItem) {
743            Object descriptionObj = MediaSessionCompatApi21.QueueItem.getDescription(queueItem);
744            MediaDescriptionCompat description = MediaDescriptionCompat.fromMediaDescription(
745                    descriptionObj);
746            long id = MediaSessionCompatApi21.QueueItem.getQueueId(queueItem);
747            return new QueueItem(queueItem, description, id);
748        }
749
750        public static final Creator<MediaSessionCompat.QueueItem>
751                CREATOR = new Creator<MediaSessionCompat.QueueItem>() {
752
753                        @Override
754                    public MediaSessionCompat.QueueItem createFromParcel(Parcel p) {
755                        return new MediaSessionCompat.QueueItem(p);
756                    }
757
758                        @Override
759                    public MediaSessionCompat.QueueItem[] newArray(int size) {
760                        return new MediaSessionCompat.QueueItem[size];
761                    }
762                };
763
764        @Override
765        public String toString() {
766            return "MediaSession.QueueItem {" +
767                    "Description=" + mDescription +
768                    ", Id=" + mId + " }";
769        }
770    }
771
772    public interface OnActiveChangeListener {
773        void onActiveChanged();
774    }
775
776    interface MediaSessionImpl {
777        void setCallback(Callback callback, Handler handler);
778        void setFlags(int flags);
779        void setPlaybackToLocal(int stream);
780        void setPlaybackToRemote(VolumeProviderCompat volumeProvider);
781        void setActive(boolean active);
782        boolean isActive();
783        void sendSessionEvent(String event, Bundle extras);
784        void release();
785        Token getSessionToken();
786        void setPlaybackState(PlaybackStateCompat state);
787        void setMetadata(MediaMetadataCompat metadata);
788
789        void setSessionActivity(PendingIntent pi);
790
791        void setMediaButtonReceiver(PendingIntent mbr);
792        void setQueue(List<QueueItem> queue);
793        void setQueueTitle(CharSequence title);
794
795        void setRatingType(int type);
796        void setExtras(Bundle extras);
797
798        Object getMediaSession();
799
800        Object getRemoteControlClient();
801    }
802
803    // TODO: compatibility implementation
804    static class MediaSessionImplBase implements MediaSessionImpl {
805        private final Context mContext;
806        private final ComponentName mComponentName;
807        private final PendingIntent mMediaButtonEventReceiver;
808        private final Object mRccObj;
809        private Object mToken;
810
811        private boolean mIsActive = false;
812        private boolean mIsRccRegistered = false;
813        private boolean mIsMbrRegistered = false;
814        private Callback mCallback;
815
816        private int mFlags;
817
818        private MediaMetadataCompat mMetadata;
819        private PlaybackStateCompat mState;
820
821        public MediaSessionImplBase(Context context, ComponentName mbrComponent,
822                PendingIntent mbr) {
823            if (mbrComponent == null) {
824                throw new IllegalArgumentException(
825                        "MediaButtonReceiver component may not be null.");
826            }
827            if (mbr == null) {
828                // construct a PendingIntent for the media button
829                Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
830                // the associated intent will be handled by the component being
831                // registered
832                mediaButtonIntent.setComponent(mbrComponent);
833                mbr = PendingIntent.getBroadcast(context,
834                        0/* requestCode, ignored */, mediaButtonIntent, 0/* flags */);
835            }
836            mContext = context;
837            mComponentName = mbrComponent;
838            mMediaButtonEventReceiver = mbr;
839            if (android.os.Build.VERSION.SDK_INT >= 14) {
840                mRccObj = MediaSessionCompatApi14.createRemoteControlClient(mbr);
841            } else {
842                mRccObj = null;
843            }
844        }
845
846        @Override
847        public void setCallback(final Callback callback, Handler handler) {
848            if (callback == mCallback) {
849                return;
850            }
851            if (callback == null || android.os.Build.VERSION.SDK_INT < 18) {
852                // There's nothing to register on API < 18 since media buttons
853                // all go through the media button receiver
854                if (android.os.Build.VERSION.SDK_INT >= 18) {
855                    MediaSessionCompatApi18.setOnPlaybackPositionUpdateListener(mRccObj, null);
856                }
857                if (android.os.Build.VERSION.SDK_INT >= 19) {
858                    MediaSessionCompatApi19.setOnMetadataUpdateListener(mRccObj, null);
859                }
860            } else {
861                if (handler == null) {
862                    handler = new Handler();
863                }
864                MediaSessionCompatApi14.Callback cb14 = new MediaSessionCompatApi14.Callback() {
865                    @Override
866                    public void onStop() {
867                        callback.onStop();
868                    }
869
870                    @Override
871                    public void onSkipToPrevious() {
872                        callback.onSkipToPrevious();
873                    }
874
875                    @Override
876                    public void onSkipToNext() {
877                        callback.onSkipToNext();
878                    }
879
880                    @Override
881                    public void onSetRating(Object ratingObj) {
882                        callback.onSetRating(RatingCompat.fromRating(ratingObj));
883                    }
884
885                    @Override
886                    public void onSeekTo(long pos) {
887                        callback.onSeekTo(pos);
888                    }
889
890                    @Override
891                    public void onRewind() {
892                        callback.onRewind();
893                    }
894
895                    @Override
896                    public void onPlay() {
897                        callback.onPlay();
898                    }
899
900                    @Override
901                    public void onPause() {
902                        callback.onPause();
903                    }
904
905                    @Override
906                    public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
907                        return callback.onMediaButtonEvent(mediaButtonIntent);
908                    }
909
910                    @Override
911                    public void onFastForward() {
912                        callback.onFastForward();
913                    }
914
915                    @Override
916                    public void onCommand(String command, Bundle extras, ResultReceiver cb) {
917                        callback.onCommand(command, extras, cb);
918                    }
919                };
920                if (android.os.Build.VERSION.SDK_INT >= 18) {
921                    Object onPositionUpdateObj = MediaSessionCompatApi18
922                            .createPlaybackPositionUpdateListener(cb14);
923                    MediaSessionCompatApi18.setOnPlaybackPositionUpdateListener(mRccObj,
924                            onPositionUpdateObj);
925                }
926                if (android.os.Build.VERSION.SDK_INT >= 19) {
927                    Object onMetadataUpdateObj = MediaSessionCompatApi19
928                            .createMetadataUpdateListener(cb14);
929                    MediaSessionCompatApi19.setOnMetadataUpdateListener(mRccObj,
930                            onMetadataUpdateObj);
931                }
932            }
933            mCallback = callback;
934        }
935
936        @Override
937        public void setFlags(int flags) {
938            mFlags = flags;
939            update();
940        }
941
942        @Override
943        public void setPlaybackToLocal(int stream) {
944        }
945
946        @Override
947        public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) {
948        }
949
950        @Override
951        public void setActive(boolean active) {
952            if (active == mIsActive) {
953                return;
954            }
955            mIsActive = active;
956            if (update()) {
957                setMetadata(mMetadata);
958                setPlaybackState(mState);
959            }
960        }
961
962        @Override
963        public boolean isActive() {
964            return mIsActive;
965        }
966
967        @Override
968        public void sendSessionEvent(String event, Bundle extras) {
969        }
970
971        @Override
972        public void release() {
973            mIsActive = false;
974            update();
975        }
976
977        @Override
978        public Token getSessionToken() {
979            return null;
980        }
981
982        @Override
983        public void setPlaybackState(PlaybackStateCompat state) {
984            mState = state;
985            if (!mIsActive) {
986                // Don't set the state until after the RCC is registered
987                return;
988            }
989
990            if (state == null) {
991                if (android.os.Build.VERSION.SDK_INT >= 14) {
992                    MediaSessionCompatApi14.setState(mRccObj, PlaybackStateCompat.STATE_NONE);
993                }
994            } else {
995                if (android.os.Build.VERSION.SDK_INT >= 18) {
996                    MediaSessionCompatApi18.setState(mRccObj, state.getState(), state.getPosition(),
997                            state.getPlaybackSpeed(), state.getLastPositionUpdateTime());
998                } else if (android.os.Build.VERSION.SDK_INT >= 14) {
999                    MediaSessionCompatApi14.setState(mRccObj, state.getState());
1000                }
1001            }
1002        }
1003
1004        @Override
1005        public void setMetadata(MediaMetadataCompat metadata) {
1006            mMetadata = metadata;
1007            if (!mIsActive) {
1008                // Don't set metadata until after the rcc has been registered
1009                return;
1010            }
1011            if (android.os.Build.VERSION.SDK_INT >= 19) {
1012                boolean canRate = mState != null
1013                        && (mState.getActions() & PlaybackStateCompat.ACTION_SET_RATING) != 0;
1014                MediaSessionCompatApi19.setMetadata(mRccObj,
1015                        metadata == null ? null : metadata.getBundle(), canRate);
1016            } else if (android.os.Build.VERSION.SDK_INT >= 14) {
1017                MediaSessionCompatApi14.setMetadata(mRccObj,
1018                        metadata == null ? null : metadata.getBundle());
1019            }
1020        }
1021
1022        @Override
1023        public void setSessionActivity(PendingIntent pi) {
1024        }
1025
1026        @Override
1027        public void setMediaButtonReceiver(PendingIntent mbr) {
1028            // Do nothing, changing this is not supported before API 21.
1029        }
1030
1031        @Override
1032        public void setQueue(List<QueueItem> queue) {
1033        }
1034
1035        @Override
1036        public void setQueueTitle(CharSequence title) {
1037        }
1038
1039        @Override
1040        public Object getMediaSession() {
1041            return null;
1042        }
1043
1044        @Override
1045        public Object getRemoteControlClient() {
1046            return mRccObj;
1047        }
1048
1049        @Override
1050        public void setRatingType(int type) {
1051        }
1052
1053        @Override
1054        public void setExtras(Bundle extras) {
1055        }
1056
1057        // Registers/unregisters the RCC and MediaButtonEventReceiver as needed.
1058        private boolean update() {
1059            boolean registeredRcc = false;
1060            if (mIsActive) {
1061                // On API 8+ register a MBR if it's supported, unregister it
1062                // if support was removed.
1063                if (android.os.Build.VERSION.SDK_INT >= 8) {
1064                    if (!mIsMbrRegistered && (mFlags & FLAG_HANDLES_MEDIA_BUTTONS) != 0) {
1065                        if (android.os.Build.VERSION.SDK_INT >= 18) {
1066                            MediaSessionCompatApi18.registerMediaButtonEventReceiver(mContext,
1067                                    mMediaButtonEventReceiver);
1068                        } else {
1069                            MediaSessionCompatApi8.registerMediaButtonEventReceiver(mContext,
1070                                    mComponentName);
1071                        }
1072                        mIsMbrRegistered = true;
1073                    } else if (mIsMbrRegistered && (mFlags & FLAG_HANDLES_MEDIA_BUTTONS) == 0) {
1074                        if (android.os.Build.VERSION.SDK_INT >= 18) {
1075                            MediaSessionCompatApi18.unregisterMediaButtonEventReceiver(mContext,
1076                                    mMediaButtonEventReceiver);
1077                        } else {
1078                            MediaSessionCompatApi8.unregisterMediaButtonEventReceiver(mContext,
1079                                    mComponentName);
1080                        }
1081                        mIsMbrRegistered = false;
1082                    }
1083                }
1084                // On API 14+ register a RCC if it's supported, unregister it if
1085                // not.
1086                if (android.os.Build.VERSION.SDK_INT >= 14) {
1087                    if (!mIsRccRegistered && (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) != 0) {
1088                        MediaSessionCompatApi14.registerRemoteControlClient(mContext, mRccObj);
1089                        mIsRccRegistered = true;
1090                        registeredRcc = true;
1091                    } else if (mIsRccRegistered
1092                            && (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) == 0) {
1093                        MediaSessionCompatApi14.unregisterRemoteControlClient(mContext, mRccObj);
1094                        mIsRccRegistered = false;
1095                    }
1096                }
1097            } else {
1098                // When inactive remove any registered components.
1099                if (mIsMbrRegistered) {
1100                    if (android.os.Build.VERSION.SDK_INT >= 18) {
1101                        MediaSessionCompatApi18.unregisterMediaButtonEventReceiver(mContext,
1102                                mMediaButtonEventReceiver);
1103                    } else {
1104                        MediaSessionCompatApi8.unregisterMediaButtonEventReceiver(mContext,
1105                                mComponentName);
1106                    }
1107                    mIsMbrRegistered = false;
1108                }
1109                if (mIsRccRegistered) {
1110                    MediaSessionCompatApi14.unregisterRemoteControlClient(mContext, mRccObj);
1111                    mIsRccRegistered = false;
1112                }
1113            }
1114            return registeredRcc;
1115        }
1116    }
1117
1118    static class MediaSessionImplApi21 implements MediaSessionImpl {
1119        private final Object mSessionObj;
1120        private final Token mToken;
1121
1122        private PendingIntent mMediaButtonIntent;
1123
1124        public MediaSessionImplApi21(Context context, String tag) {
1125            mSessionObj = MediaSessionCompatApi21.createSession(context, tag);
1126            mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj));
1127        }
1128
1129        public MediaSessionImplApi21(Object mediaSession) {
1130            mSessionObj = MediaSessionCompatApi21.verifySession(mediaSession);
1131            mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj));
1132        }
1133
1134        @Override
1135        public void setCallback(Callback callback, Handler handler) {
1136            MediaSessionCompatApi21.setCallback(mSessionObj, callback.mCallbackObj, handler);
1137        }
1138
1139        @Override
1140        public void setFlags(int flags) {
1141            MediaSessionCompatApi21.setFlags(mSessionObj, flags);
1142        }
1143
1144        @Override
1145        public void setPlaybackToLocal(int stream) {
1146            MediaSessionCompatApi21.setPlaybackToLocal(mSessionObj, stream);
1147        }
1148
1149        @Override
1150        public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) {
1151            MediaSessionCompatApi21.setPlaybackToRemote(mSessionObj,
1152                    volumeProvider.getVolumeProvider());
1153        }
1154
1155        @Override
1156        public void setActive(boolean active) {
1157            MediaSessionCompatApi21.setActive(mSessionObj, active);
1158        }
1159
1160        @Override
1161        public boolean isActive() {
1162            return MediaSessionCompatApi21.isActive(mSessionObj);
1163        }
1164
1165        @Override
1166        public void sendSessionEvent(String event, Bundle extras) {
1167            MediaSessionCompatApi21.sendSessionEvent(mSessionObj, event, extras);
1168        }
1169
1170        @Override
1171        public void release() {
1172            MediaSessionCompatApi21.release(mSessionObj);
1173        }
1174
1175        @Override
1176        public Token getSessionToken() {
1177            return mToken;
1178        }
1179
1180        @Override
1181        public void setPlaybackState(PlaybackStateCompat state) {
1182            MediaSessionCompatApi21.setPlaybackState(mSessionObj, state.getPlaybackState());
1183        }
1184
1185        @Override
1186        public void setMetadata(MediaMetadataCompat metadata) {
1187            MediaSessionCompatApi21.setMetadata(mSessionObj, metadata.getMediaMetadata());
1188        }
1189
1190        @Override
1191        public void setSessionActivity(PendingIntent pi) {
1192            MediaSessionCompatApi21.setSessionActivity(mSessionObj, pi);
1193        }
1194
1195        @Override
1196        public void setMediaButtonReceiver(PendingIntent mbr) {
1197            mMediaButtonIntent = mbr;
1198            MediaSessionCompatApi21.setMediaButtonReceiver(mSessionObj, mbr);
1199        }
1200
1201        @Override
1202        public void setQueue(List<QueueItem> queue) {
1203            List<Object> queueObjs = null;
1204            if (queue != null) {
1205                queueObjs = new ArrayList<Object>();
1206                for (QueueItem item : queue) {
1207                    queueObjs.add(item.getQueueItem());
1208                }
1209            }
1210            MediaSessionCompatApi21.setQueue(mSessionObj, queueObjs);
1211        }
1212
1213        @Override
1214        public void setQueueTitle(CharSequence title) {
1215            MediaSessionCompatApi21.setQueueTitle(mSessionObj, title);
1216        }
1217
1218        @Override
1219        public void setRatingType(int type) {
1220            if (android.os.Build.VERSION.SDK_INT < 22) {
1221                // TODO figure out 21 implementation
1222            } else {
1223                MediaSessionCompatApi22.setRatingType(mSessionObj, type);
1224            }
1225        }
1226
1227        @Override
1228        public void setExtras(Bundle extras) {
1229            MediaSessionCompatApi21.setExtras(mSessionObj, extras);
1230        }
1231
1232        @Override
1233        public Object getMediaSession() {
1234            return mSessionObj;
1235        }
1236
1237        @Override
1238        public Object getRemoteControlClient() {
1239            return null;
1240        }
1241    }
1242}
1243