1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.media.session;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.app.PendingIntent;
22import android.content.Context;
23import android.content.pm.ParceledListSlice;
24import android.media.AudioAttributes;
25import android.media.AudioManager;
26import android.media.MediaMetadata;
27import android.media.Rating;
28import android.media.VolumeProvider;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.Handler;
32import android.os.Looper;
33import android.os.Message;
34import android.os.RemoteException;
35import android.os.ResultReceiver;
36import android.text.TextUtils;
37import android.util.Log;
38import android.view.KeyEvent;
39
40import java.lang.ref.WeakReference;
41import java.util.ArrayList;
42import java.util.List;
43
44/**
45 * Allows an app to interact with an ongoing media session. Media buttons and
46 * other commands can be sent to the session. A callback may be registered to
47 * receive updates from the session, such as metadata and play state changes.
48 * <p>
49 * A MediaController can be created through {@link MediaSessionManager} if you
50 * hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or are an
51 * enabled notification listener or by getting a {@link MediaSession.Token}
52 * directly from the session owner.
53 * <p>
54 * MediaController objects are thread-safe.
55 */
56public final class MediaController {
57    private static final String TAG = "MediaController";
58
59    private static final int MSG_EVENT = 1;
60    private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
61    private static final int MSG_UPDATE_METADATA = 3;
62    private static final int MSG_UPDATE_VOLUME = 4;
63    private static final int MSG_UPDATE_QUEUE = 5;
64    private static final int MSG_UPDATE_QUEUE_TITLE = 6;
65    private static final int MSG_UPDATE_EXTRAS = 7;
66    private static final int MSG_DESTROYED = 8;
67
68    private final ISessionController mSessionBinder;
69
70    private final MediaSession.Token mToken;
71    private final Context mContext;
72    private final CallbackStub mCbStub = new CallbackStub(this);
73    private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>();
74    private final Object mLock = new Object();
75
76    private boolean mCbRegistered = false;
77    private String mPackageName;
78    private String mTag;
79
80    private final TransportControls mTransportControls;
81
82    /**
83     * Call for creating a MediaController directly from a binder. Should only
84     * be used by framework code.
85     *
86     * @hide
87     */
88    public MediaController(Context context, ISessionController sessionBinder) {
89        if (sessionBinder == null) {
90            throw new IllegalArgumentException("Session token cannot be null");
91        }
92        if (context == null) {
93            throw new IllegalArgumentException("Context cannot be null");
94        }
95        mSessionBinder = sessionBinder;
96        mTransportControls = new TransportControls();
97        mToken = new MediaSession.Token(sessionBinder);
98        mContext = context;
99    }
100
101    /**
102     * Create a new MediaController from a session's token.
103     *
104     * @param context The caller's context.
105     * @param token The token for the session.
106     */
107    public MediaController(@NonNull Context context, @NonNull MediaSession.Token token) {
108        this(context, token.getBinder());
109    }
110
111    /**
112     * Get a {@link TransportControls} instance to send transport actions to
113     * the associated session.
114     *
115     * @return A transport controls instance.
116     */
117    public @NonNull TransportControls getTransportControls() {
118        return mTransportControls;
119    }
120
121    /**
122     * Send the specified media button event to the session. Only media keys can
123     * be sent by this method, other keys will be ignored.
124     *
125     * @param keyEvent The media button event to dispatch.
126     * @return true if the event was sent to the session, false otherwise.
127     */
128    public boolean dispatchMediaButtonEvent(@NonNull KeyEvent keyEvent) {
129        if (keyEvent == null) {
130            throw new IllegalArgumentException("KeyEvent may not be null");
131        }
132        if (!KeyEvent.isMediaKey(keyEvent.getKeyCode())) {
133            return false;
134        }
135        try {
136            return mSessionBinder.sendMediaButton(keyEvent);
137        } catch (RemoteException e) {
138            // System is dead. =(
139        }
140        return false;
141    }
142
143    /**
144     * Get the current playback state for this session.
145     *
146     * @return The current PlaybackState or null
147     */
148    public @Nullable PlaybackState getPlaybackState() {
149        try {
150            return mSessionBinder.getPlaybackState();
151        } catch (RemoteException e) {
152            Log.wtf(TAG, "Error calling getPlaybackState.", e);
153            return null;
154        }
155    }
156
157    /**
158     * Get the current metadata for this session.
159     *
160     * @return The current MediaMetadata or null.
161     */
162    public @Nullable MediaMetadata getMetadata() {
163        try {
164            return mSessionBinder.getMetadata();
165        } catch (RemoteException e) {
166            Log.wtf(TAG, "Error calling getMetadata.", e);
167            return null;
168        }
169    }
170
171    /**
172     * Get the current play queue for this session if one is set. If you only
173     * care about the current item {@link #getMetadata()} should be used.
174     *
175     * @return The current play queue or null.
176     */
177    public @Nullable List<MediaSession.QueueItem> getQueue() {
178        try {
179            ParceledListSlice queue = mSessionBinder.getQueue();
180            if (queue != null) {
181                return queue.getList();
182            }
183        } catch (RemoteException e) {
184            Log.wtf(TAG, "Error calling getQueue.", e);
185        }
186        return null;
187    }
188
189    /**
190     * Get the queue title for this session.
191     */
192    public @Nullable CharSequence getQueueTitle() {
193        try {
194            return mSessionBinder.getQueueTitle();
195        } catch (RemoteException e) {
196            Log.wtf(TAG, "Error calling getQueueTitle", e);
197        }
198        return null;
199    }
200
201    /**
202     * Get the extras for this session.
203     */
204    public @Nullable Bundle getExtras() {
205        try {
206            return mSessionBinder.getExtras();
207        } catch (RemoteException e) {
208            Log.wtf(TAG, "Error calling getExtras", e);
209        }
210        return null;
211    }
212
213    /**
214     * Get the rating type supported by the session. One of:
215     * <ul>
216     * <li>{@link Rating#RATING_NONE}</li>
217     * <li>{@link Rating#RATING_HEART}</li>
218     * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li>
219     * <li>{@link Rating#RATING_3_STARS}</li>
220     * <li>{@link Rating#RATING_4_STARS}</li>
221     * <li>{@link Rating#RATING_5_STARS}</li>
222     * <li>{@link Rating#RATING_PERCENTAGE}</li>
223     * </ul>
224     *
225     * @return The supported rating type
226     */
227    public int getRatingType() {
228        try {
229            return mSessionBinder.getRatingType();
230        } catch (RemoteException e) {
231            Log.wtf(TAG, "Error calling getRatingType.", e);
232            return Rating.RATING_NONE;
233        }
234    }
235
236    /**
237     * Get the flags for this session. Flags are defined in {@link MediaSession}.
238     *
239     * @return The current set of flags for the session.
240     */
241    public @MediaSession.SessionFlags long getFlags() {
242        try {
243            return mSessionBinder.getFlags();
244        } catch (RemoteException e) {
245            Log.wtf(TAG, "Error calling getFlags.", e);
246        }
247        return 0;
248    }
249
250    /**
251     * Get the current playback info for this session.
252     *
253     * @return The current playback info or null.
254     */
255    public @Nullable PlaybackInfo getPlaybackInfo() {
256        try {
257            ParcelableVolumeInfo result = mSessionBinder.getVolumeAttributes();
258            return new PlaybackInfo(result.volumeType, result.audioAttrs, result.controlType,
259                    result.maxVolume, result.currentVolume);
260
261        } catch (RemoteException e) {
262            Log.wtf(TAG, "Error calling getAudioInfo.", e);
263        }
264        return null;
265    }
266
267    /**
268     * Get an intent for launching UI associated with this session if one
269     * exists.
270     *
271     * @return A {@link PendingIntent} to launch UI or null.
272     */
273    public @Nullable PendingIntent getSessionActivity() {
274        try {
275            return mSessionBinder.getLaunchPendingIntent();
276        } catch (RemoteException e) {
277            Log.wtf(TAG, "Error calling getPendingIntent.", e);
278        }
279        return null;
280    }
281
282    /**
283     * Get the token for the session this is connected to.
284     *
285     * @return The token for the connected session.
286     */
287    public @NonNull MediaSession.Token getSessionToken() {
288        return mToken;
289    }
290
291    /**
292     * Set the volume of the output this session is playing on. The command will
293     * be ignored if it does not support
294     * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in
295     * {@link AudioManager} may be used to affect the handling.
296     *
297     * @see #getPlaybackInfo()
298     * @param value The value to set it to, between 0 and the reported max.
299     * @param flags Flags from {@link AudioManager} to include with the volume
300     *            request.
301     */
302    public void setVolumeTo(int value, int flags) {
303        try {
304            mSessionBinder.setVolumeTo(value, flags, mContext.getPackageName());
305        } catch (RemoteException e) {
306            Log.wtf(TAG, "Error calling setVolumeTo.", e);
307        }
308    }
309
310    /**
311     * Adjust the volume of the output this session is playing on. The direction
312     * must be one of {@link AudioManager#ADJUST_LOWER},
313     * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
314     * The command will be ignored if the session does not support
315     * {@link VolumeProvider#VOLUME_CONTROL_RELATIVE} or
316     * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in
317     * {@link AudioManager} may be used to affect the handling.
318     *
319     * @see #getPlaybackInfo()
320     * @param direction The direction to adjust the volume in.
321     * @param flags Any flags to pass with the command.
322     */
323    public void adjustVolume(int direction, int flags) {
324        try {
325            mSessionBinder.adjustVolume(direction, flags, mContext.getPackageName());
326        } catch (RemoteException e) {
327            Log.wtf(TAG, "Error calling adjustVolumeBy.", e);
328        }
329    }
330
331    /**
332     * Registers a callback to receive updates from the Session. Updates will be
333     * posted on the caller's thread.
334     *
335     * @param callback The callback object, must not be null.
336     */
337    public void registerCallback(@NonNull Callback callback) {
338        registerCallback(callback, null);
339    }
340
341    /**
342     * Registers a callback to receive updates from the session. Updates will be
343     * posted on the specified handler's thread.
344     *
345     * @param callback The callback object, must not be null.
346     * @param handler The handler to post updates on. If null the callers thread
347     *            will be used.
348     */
349    public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) {
350        if (callback == null) {
351            throw new IllegalArgumentException("callback must not be null");
352        }
353        if (handler == null) {
354            handler = new Handler();
355        }
356        synchronized (mLock) {
357            addCallbackLocked(callback, handler);
358        }
359    }
360
361    /**
362     * Unregisters the specified callback. If an update has already been posted
363     * you may still receive it after calling this method.
364     *
365     * @param callback The callback to remove.
366     */
367    public void unregisterCallback(@NonNull Callback callback) {
368        if (callback == null) {
369            throw new IllegalArgumentException("callback must not be null");
370        }
371        synchronized (mLock) {
372            removeCallbackLocked(callback);
373        }
374    }
375
376    /**
377     * Sends a generic command to the session. It is up to the session creator
378     * to decide what commands and parameters they will support. As such,
379     * commands should only be sent to sessions that the controller owns.
380     *
381     * @param command The command to send
382     * @param args Any parameters to include with the command
383     * @param cb The callback to receive the result on
384     */
385    public void sendCommand(@NonNull String command, @Nullable Bundle args,
386            @Nullable ResultReceiver cb) {
387        if (TextUtils.isEmpty(command)) {
388            throw new IllegalArgumentException("command cannot be null or empty");
389        }
390        try {
391            mSessionBinder.sendCommand(command, args, cb);
392        } catch (RemoteException e) {
393            Log.d(TAG, "Dead object in sendCommand.", e);
394        }
395    }
396
397    /**
398     * Get the session owner's package name.
399     *
400     * @return The package name of of the session owner.
401     */
402    public String getPackageName() {
403        if (mPackageName == null) {
404            try {
405                mPackageName = mSessionBinder.getPackageName();
406            } catch (RemoteException e) {
407                Log.d(TAG, "Dead object in getPackageName.", e);
408            }
409        }
410        return mPackageName;
411    }
412
413    /**
414     * Get the session's tag for debugging purposes.
415     *
416     * @return The session's tag.
417     * @hide
418     */
419    public String getTag() {
420        if (mTag == null) {
421            try {
422                mTag = mSessionBinder.getTag();
423            } catch (RemoteException e) {
424                Log.d(TAG, "Dead object in getTag.", e);
425            }
426        }
427        return mTag;
428    }
429
430    /*
431     * @hide
432     */
433    ISessionController getSessionBinder() {
434        return mSessionBinder;
435    }
436
437    /**
438     * @hide
439     */
440    public boolean controlsSameSession(MediaController other) {
441        if (other == null) return false;
442        return mSessionBinder.asBinder() == other.getSessionBinder().asBinder();
443    }
444
445    private void addCallbackLocked(Callback cb, Handler handler) {
446        if (getHandlerForCallbackLocked(cb) != null) {
447            Log.w(TAG, "Callback is already added, ignoring");
448            return;
449        }
450        MessageHandler holder = new MessageHandler(handler.getLooper(), cb);
451        mCallbacks.add(holder);
452        holder.mRegistered = true;
453
454        if (!mCbRegistered) {
455            try {
456                mSessionBinder.registerCallbackListener(mCbStub);
457                mCbRegistered = true;
458            } catch (RemoteException e) {
459                Log.e(TAG, "Dead object in registerCallback", e);
460            }
461        }
462    }
463
464    private boolean removeCallbackLocked(Callback cb) {
465        boolean success = false;
466        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
467            MessageHandler handler = mCallbacks.get(i);
468            if (cb == handler.mCallback) {
469                mCallbacks.remove(i);
470                success = true;
471                handler.mRegistered = false;
472            }
473        }
474        if (mCbRegistered && mCallbacks.size() == 0) {
475            try {
476                mSessionBinder.unregisterCallbackListener(mCbStub);
477            } catch (RemoteException e) {
478                Log.e(TAG, "Dead object in removeCallbackLocked");
479            }
480            mCbRegistered = false;
481        }
482        return success;
483    }
484
485    private MessageHandler getHandlerForCallbackLocked(Callback cb) {
486        if (cb == null) {
487            throw new IllegalArgumentException("Callback cannot be null");
488        }
489        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
490            MessageHandler handler = mCallbacks.get(i);
491            if (cb == handler.mCallback) {
492                return handler;
493            }
494        }
495        return null;
496    }
497
498    private final void postMessage(int what, Object obj, Bundle data) {
499        synchronized (mLock) {
500            for (int i = mCallbacks.size() - 1; i >= 0; i--) {
501                mCallbacks.get(i).post(what, obj, data);
502            }
503        }
504    }
505
506    /**
507     * Callback for receiving updates from the session. A Callback can be
508     * registered using {@link #registerCallback}.
509     */
510    public static abstract class Callback {
511        /**
512         * Override to handle the session being destroyed. The session is no
513         * longer valid after this call and calls to it will be ignored.
514         */
515        public void onSessionDestroyed() {
516        }
517
518        /**
519         * Override to handle custom events sent by the session owner without a
520         * specified interface. Controllers should only handle these for
521         * sessions they own.
522         *
523         * @param event The event from the session.
524         * @param extras Optional parameters for the event, may be null.
525         */
526        public void onSessionEvent(@NonNull String event, @Nullable Bundle extras) {
527        }
528
529        /**
530         * Override to handle changes in playback state.
531         *
532         * @param state The new playback state of the session
533         */
534        public void onPlaybackStateChanged(@NonNull PlaybackState state) {
535        }
536
537        /**
538         * Override to handle changes to the current metadata.
539         *
540         * @param metadata The current metadata for the session or null if none.
541         * @see MediaMetadata
542         */
543        public void onMetadataChanged(@Nullable MediaMetadata metadata) {
544        }
545
546        /**
547         * Override to handle changes to items in the queue.
548         *
549         * @param queue A list of items in the current play queue. It should
550         *            include the currently playing item as well as previous and
551         *            upcoming items if applicable.
552         * @see MediaSession.QueueItem
553         */
554        public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) {
555        }
556
557        /**
558         * Override to handle changes to the queue title.
559         *
560         * @param title The title that should be displayed along with the play queue such as
561         *              "Now Playing". May be null if there is no such title.
562         */
563        public void onQueueTitleChanged(@Nullable CharSequence title) {
564        }
565
566        /**
567         * Override to handle changes to the {@link MediaSession} extras.
568         *
569         * @param extras The extras that can include other information associated with the
570         *               {@link MediaSession}.
571         */
572        public void onExtrasChanged(@Nullable Bundle extras) {
573        }
574
575        /**
576         * Override to handle changes to the audio info.
577         *
578         * @param info The current audio info for this session.
579         */
580        public void onAudioInfoChanged(PlaybackInfo info) {
581        }
582    }
583
584    /**
585     * Interface for controlling media playback on a session. This allows an app
586     * to send media transport commands to the session.
587     */
588    public final class TransportControls {
589        private static final String TAG = "TransportController";
590
591        private TransportControls() {
592        }
593
594        /**
595         * Request that the player prepare its playback. In other words, other sessions can continue
596         * to play during the preparation of this session. This method can be used to speed up the
597         * start of the playback. Once the preparation is done, the session will change its playback
598         * state to {@link PlaybackState#STATE_PAUSED}. Afterwards, {@link #play} can be called to
599         * start playback.
600         */
601        public void prepare() {
602            try {
603                mSessionBinder.prepare();
604            } catch (RemoteException e) {
605                Log.wtf(TAG, "Error calling prepare.", e);
606            }
607        }
608
609        /**
610         * Request that the player prepare playback for a specific media id. In other words, other
611         * sessions can continue to play during the preparation of this session. This method can be
612         * used to speed up the start of the playback. Once the preparation is done, the session
613         * will change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards,
614         * {@link #play} can be called to start playback. If the preparation is not needed,
615         * {@link #playFromMediaId} can be directly called without this method.
616         *
617         * @param mediaId The id of the requested media.
618         * @param extras Optional extras that can include extra information about the media item
619         *               to be prepared.
620         */
621        public void prepareFromMediaId(String mediaId, Bundle extras) {
622            if (TextUtils.isEmpty(mediaId)) {
623                throw new IllegalArgumentException(
624                        "You must specify a non-empty String for prepareFromMediaId.");
625            }
626            try {
627                mSessionBinder.prepareFromMediaId(mediaId, extras);
628            } catch (RemoteException e) {
629                Log.wtf(TAG, "Error calling prepare(" + mediaId + ").", e);
630            }
631        }
632
633        /**
634         * Request that the player prepare playback for a specific search query. An empty or null
635         * query should be treated as a request to prepare any music. In other words, other sessions
636         * can continue to play during the preparation of this session. This method can be used to
637         * speed up the start of the playback. Once the preparation is done, the session will
638         * change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards,
639         * {@link #play} can be called to start playback. If the preparation is not needed,
640         * {@link #playFromSearch} can be directly called without this method.
641         *
642         * @param query The search query.
643         * @param extras Optional extras that can include extra information
644         *               about the query.
645         */
646        public void prepareFromSearch(String query, Bundle extras) {
647            if (query == null) {
648                // This is to remain compatible with
649                // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
650                query = "";
651            }
652            try {
653                mSessionBinder.prepareFromSearch(query, extras);
654            } catch (RemoteException e) {
655                Log.wtf(TAG, "Error calling prepare(" + query + ").", e);
656            }
657        }
658
659        /**
660         * Request that the player prepare playback for a specific {@link Uri}. In other words,
661         * other sessions can continue to play during the preparation of this session. This method
662         * can be used to speed up the start of the playback. Once the preparation is done, the
663         * session will change its playback state to {@link PlaybackState#STATE_PAUSED}. Afterwards,
664         * {@link #play} can be called to start playback. If the preparation is not needed,
665         * {@link #playFromUri} can be directly called without this method.
666         *
667         * @param uri The URI of the requested media.
668         * @param extras Optional extras that can include extra information about the media item
669         *               to be prepared.
670         */
671        public void prepareFromUri(Uri uri, Bundle extras) {
672            if (uri == null || Uri.EMPTY.equals(uri)) {
673                throw new IllegalArgumentException(
674                        "You must specify a non-empty Uri for prepareFromUri.");
675            }
676            try {
677                mSessionBinder.prepareFromUri(uri, extras);
678            } catch (RemoteException e) {
679                Log.wtf(TAG, "Error calling prepare(" + uri + ").", e);
680            }
681        }
682
683        /**
684         * Request that the player start its playback at its current position.
685         */
686        public void play() {
687            try {
688                mSessionBinder.play();
689            } catch (RemoteException e) {
690                Log.wtf(TAG, "Error calling play.", e);
691            }
692        }
693
694        /**
695         * Request that the player start playback for a specific media id.
696         *
697         * @param mediaId The id of the requested media.
698         * @param extras Optional extras that can include extra information about the media item
699         *               to be played.
700         */
701        public void playFromMediaId(String mediaId, Bundle extras) {
702            if (TextUtils.isEmpty(mediaId)) {
703                throw new IllegalArgumentException(
704                        "You must specify a non-empty String for playFromMediaId.");
705            }
706            try {
707                mSessionBinder.playFromMediaId(mediaId, extras);
708            } catch (RemoteException e) {
709                Log.wtf(TAG, "Error calling play(" + mediaId + ").", e);
710            }
711        }
712
713        /**
714         * Request that the player start playback for a specific search query.
715         * An empty or null query should be treated as a request to play any
716         * music.
717         *
718         * @param query The search query.
719         * @param extras Optional extras that can include extra information
720         *               about the query.
721         */
722        public void playFromSearch(String query, Bundle extras) {
723            if (query == null) {
724                // This is to remain compatible with
725                // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH
726                query = "";
727            }
728            try {
729                mSessionBinder.playFromSearch(query, extras);
730            } catch (RemoteException e) {
731                Log.wtf(TAG, "Error calling play(" + query + ").", e);
732            }
733        }
734
735        /**
736         * Request that the player start playback for a specific {@link Uri}.
737         *
738         * @param uri The URI of the requested media.
739         * @param extras Optional extras that can include extra information about the media item
740         *               to be played.
741         */
742        public void playFromUri(Uri uri, Bundle extras) {
743            if (uri == null || Uri.EMPTY.equals(uri)) {
744                throw new IllegalArgumentException(
745                        "You must specify a non-empty Uri for playFromUri.");
746            }
747            try {
748                mSessionBinder.playFromUri(uri, extras);
749            } catch (RemoteException e) {
750                Log.wtf(TAG, "Error calling play(" + uri + ").", e);
751            }
752        }
753
754        /**
755         * Play an item with a specific id in the play queue. If you specify an
756         * id that is not in the play queue, the behavior is undefined.
757         */
758        public void skipToQueueItem(long id) {
759            try {
760                mSessionBinder.skipToQueueItem(id);
761            } catch (RemoteException e) {
762                Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e);
763            }
764        }
765
766        /**
767         * Request that the player pause its playback and stay at its current
768         * position.
769         */
770        public void pause() {
771            try {
772                mSessionBinder.pause();
773            } catch (RemoteException e) {
774                Log.wtf(TAG, "Error calling pause.", e);
775            }
776        }
777
778        /**
779         * Request that the player stop its playback; it may clear its state in
780         * whatever way is appropriate.
781         */
782        public void stop() {
783            try {
784                mSessionBinder.stop();
785            } catch (RemoteException e) {
786                Log.wtf(TAG, "Error calling stop.", e);
787            }
788        }
789
790        /**
791         * Move to a new location in the media stream.
792         *
793         * @param pos Position to move to, in milliseconds.
794         */
795        public void seekTo(long pos) {
796            try {
797                mSessionBinder.seekTo(pos);
798            } catch (RemoteException e) {
799                Log.wtf(TAG, "Error calling seekTo.", e);
800            }
801        }
802
803        /**
804         * Start fast forwarding. If playback is already fast forwarding this
805         * may increase the rate.
806         */
807        public void fastForward() {
808            try {
809                mSessionBinder.fastForward();
810            } catch (RemoteException e) {
811                Log.wtf(TAG, "Error calling fastForward.", e);
812            }
813        }
814
815        /**
816         * Skip to the next item.
817         */
818        public void skipToNext() {
819            try {
820                mSessionBinder.next();
821            } catch (RemoteException e) {
822                Log.wtf(TAG, "Error calling next.", e);
823            }
824        }
825
826        /**
827         * Start rewinding. If playback is already rewinding this may increase
828         * the rate.
829         */
830        public void rewind() {
831            try {
832                mSessionBinder.rewind();
833            } catch (RemoteException e) {
834                Log.wtf(TAG, "Error calling rewind.", e);
835            }
836        }
837
838        /**
839         * Skip to the previous item.
840         */
841        public void skipToPrevious() {
842            try {
843                mSessionBinder.previous();
844            } catch (RemoteException e) {
845                Log.wtf(TAG, "Error calling previous.", e);
846            }
847        }
848
849        /**
850         * Rate the current content. This will cause the rating to be set for
851         * the current user. The Rating type must match the type returned by
852         * {@link #getRatingType()}.
853         *
854         * @param rating The rating to set for the current content
855         */
856        public void setRating(Rating rating) {
857            try {
858                mSessionBinder.rate(rating);
859            } catch (RemoteException e) {
860                Log.wtf(TAG, "Error calling rate.", e);
861            }
862        }
863
864        /**
865         * Send a custom action back for the {@link MediaSession} to perform.
866         *
867         * @param customAction The action to perform.
868         * @param args Optional arguments to supply to the {@link MediaSession} for this
869         *             custom action.
870         */
871        public void sendCustomAction(@NonNull PlaybackState.CustomAction customAction,
872                    @Nullable Bundle args) {
873            if (customAction == null) {
874                throw new IllegalArgumentException("CustomAction cannot be null.");
875            }
876            sendCustomAction(customAction.getAction(), args);
877        }
878
879        /**
880         * Send the id and args from a custom action back for the {@link MediaSession} to perform.
881         *
882         * @see #sendCustomAction(PlaybackState.CustomAction action, Bundle args)
883         * @param action The action identifier of the {@link PlaybackState.CustomAction} as
884         *               specified by the {@link MediaSession}.
885         * @param args Optional arguments to supply to the {@link MediaSession} for this
886         *             custom action.
887         */
888        public void sendCustomAction(@NonNull String action, @Nullable Bundle args) {
889            if (TextUtils.isEmpty(action)) {
890                throw new IllegalArgumentException("CustomAction cannot be null.");
891            }
892            try {
893                mSessionBinder.sendCustomAction(action, args);
894            } catch (RemoteException e) {
895                Log.d(TAG, "Dead object in sendCustomAction.", e);
896            }
897        }
898    }
899
900    /**
901     * Holds information about the current playback and how audio is handled for
902     * this session.
903     */
904    public static final class PlaybackInfo {
905        /**
906         * The session uses remote playback.
907         */
908        public static final int PLAYBACK_TYPE_REMOTE = 2;
909        /**
910         * The session uses local playback.
911         */
912        public static final int PLAYBACK_TYPE_LOCAL = 1;
913
914        private final int mVolumeType;
915        private final int mVolumeControl;
916        private final int mMaxVolume;
917        private final int mCurrentVolume;
918        private final AudioAttributes mAudioAttrs;
919
920        /**
921         * @hide
922         */
923        public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) {
924            mVolumeType = type;
925            mAudioAttrs = attrs;
926            mVolumeControl = control;
927            mMaxVolume = max;
928            mCurrentVolume = current;
929        }
930
931        /**
932         * Get the type of playback which affects volume handling. One of:
933         * <ul>
934         * <li>{@link #PLAYBACK_TYPE_LOCAL}</li>
935         * <li>{@link #PLAYBACK_TYPE_REMOTE}</li>
936         * </ul>
937         *
938         * @return The type of playback this session is using.
939         */
940        public int getPlaybackType() {
941            return mVolumeType;
942        }
943
944        /**
945         * Get the audio attributes for this session. The attributes will affect
946         * volume handling for the session. When the volume type is
947         * {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be ignored by the
948         * remote volume handler.
949         *
950         * @return The attributes for this session.
951         */
952        public AudioAttributes getAudioAttributes() {
953            return mAudioAttrs;
954        }
955
956        /**
957         * Get the type of volume control that can be used. One of:
958         * <ul>
959         * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li>
960         * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li>
961         * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li>
962         * </ul>
963         *
964         * @return The type of volume control that may be used with this
965         *         session.
966         */
967        public int getVolumeControl() {
968            return mVolumeControl;
969        }
970
971        /**
972         * Get the maximum volume that may be set for this session.
973         *
974         * @return The maximum allowed volume where this session is playing.
975         */
976        public int getMaxVolume() {
977            return mMaxVolume;
978        }
979
980        /**
981         * Get the current volume for this session.
982         *
983         * @return The current volume where this session is playing.
984         */
985        public int getCurrentVolume() {
986            return mCurrentVolume;
987        }
988    }
989
990    private final static class CallbackStub extends ISessionControllerCallback.Stub {
991        private final WeakReference<MediaController> mController;
992
993        public CallbackStub(MediaController controller) {
994            mController = new WeakReference<MediaController>(controller);
995        }
996
997        @Override
998        public void onSessionDestroyed() {
999            MediaController controller = mController.get();
1000            if (controller != null) {
1001                controller.postMessage(MSG_DESTROYED, null, null);
1002            }
1003        }
1004
1005        @Override
1006        public void onEvent(String event, Bundle extras) {
1007            MediaController controller = mController.get();
1008            if (controller != null) {
1009                controller.postMessage(MSG_EVENT, event, extras);
1010            }
1011        }
1012
1013        @Override
1014        public void onPlaybackStateChanged(PlaybackState state) {
1015            MediaController controller = mController.get();
1016            if (controller != null) {
1017                controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null);
1018            }
1019        }
1020
1021        @Override
1022        public void onMetadataChanged(MediaMetadata metadata) {
1023            MediaController controller = mController.get();
1024            if (controller != null) {
1025                controller.postMessage(MSG_UPDATE_METADATA, metadata, null);
1026            }
1027        }
1028
1029        @Override
1030        public void onQueueChanged(ParceledListSlice parceledQueue) {
1031            List<MediaSession.QueueItem> queue = parceledQueue == null ? null : parceledQueue
1032                    .getList();
1033            MediaController controller = mController.get();
1034            if (controller != null) {
1035                controller.postMessage(MSG_UPDATE_QUEUE, queue, null);
1036            }
1037        }
1038
1039        @Override
1040        public void onQueueTitleChanged(CharSequence title) {
1041            MediaController controller = mController.get();
1042            if (controller != null) {
1043                controller.postMessage(MSG_UPDATE_QUEUE_TITLE, title, null);
1044            }
1045        }
1046
1047        @Override
1048        public void onExtrasChanged(Bundle extras) {
1049            MediaController controller = mController.get();
1050            if (controller != null) {
1051                controller.postMessage(MSG_UPDATE_EXTRAS, extras, null);
1052            }
1053        }
1054
1055        @Override
1056        public void onVolumeInfoChanged(ParcelableVolumeInfo pvi) {
1057            MediaController controller = mController.get();
1058            if (controller != null) {
1059                PlaybackInfo info = new PlaybackInfo(pvi.volumeType, pvi.audioAttrs, pvi.controlType,
1060                        pvi.maxVolume, pvi.currentVolume);
1061                controller.postMessage(MSG_UPDATE_VOLUME, info, null);
1062            }
1063        }
1064
1065    }
1066
1067    private final static class MessageHandler extends Handler {
1068        private final MediaController.Callback mCallback;
1069        private boolean mRegistered = false;
1070
1071        public MessageHandler(Looper looper, MediaController.Callback cb) {
1072            super(looper, null, true);
1073            mCallback = cb;
1074        }
1075
1076        @Override
1077        public void handleMessage(Message msg) {
1078            if (!mRegistered) {
1079                return;
1080            }
1081            switch (msg.what) {
1082                case MSG_EVENT:
1083                    mCallback.onSessionEvent((String) msg.obj, msg.getData());
1084                    break;
1085                case MSG_UPDATE_PLAYBACK_STATE:
1086                    mCallback.onPlaybackStateChanged((PlaybackState) msg.obj);
1087                    break;
1088                case MSG_UPDATE_METADATA:
1089                    mCallback.onMetadataChanged((MediaMetadata) msg.obj);
1090                    break;
1091                case MSG_UPDATE_QUEUE:
1092                    mCallback.onQueueChanged((List<MediaSession.QueueItem>) msg.obj);
1093                    break;
1094                case MSG_UPDATE_QUEUE_TITLE:
1095                    mCallback.onQueueTitleChanged((CharSequence) msg.obj);
1096                    break;
1097                case MSG_UPDATE_EXTRAS:
1098                    mCallback.onExtrasChanged((Bundle) msg.obj);
1099                    break;
1100                case MSG_UPDATE_VOLUME:
1101                    mCallback.onAudioInfoChanged((PlaybackInfo) msg.obj);
1102                    break;
1103                case MSG_DESTROYED:
1104                    mCallback.onSessionDestroyed();
1105                    break;
1106            }
1107        }
1108
1109        public void post(int what, Object obj, Bundle data) {
1110            Message msg = obtainMessage(what, obj);
1111            msg.setData(data);
1112            msg.sendToTarget();
1113        }
1114    }
1115
1116}
1117