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 
17 package android.media.session;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.PendingIntent;
22 import android.content.Context;
23 import android.content.pm.ParceledListSlice;
24 import android.media.AudioAttributes;
25 import android.media.AudioManager;
26 import android.media.MediaMetadata;
27 import android.media.Rating;
28 import android.media.VolumeProvider;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.Looper;
33 import android.os.Message;
34 import android.os.RemoteException;
35 import android.os.ResultReceiver;
36 import android.text.TextUtils;
37 import android.util.Log;
38 import android.view.KeyEvent;
39 
40 import java.lang.ref.WeakReference;
41 import java.util.ArrayList;
42 import 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  */
56 public 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      */
MediaController(Context context, ISessionController sessionBinder)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      */
MediaController(@onNull Context context, @NonNull MediaSession.Token token)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      */
getTransportControls()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      */
dispatchMediaButtonEvent(@onNull KeyEvent keyEvent)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      */
getPlaybackState()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      */
getMetadata()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      */
getQueue()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      */
getQueueTitle()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      */
getExtras()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      */
getRatingType()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      */
getFlags()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      */
getPlaybackInfo()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      */
getSessionActivity()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      */
getSessionToken()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      */
setVolumeTo(int value, int flags)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      */
adjustVolume(int direction, int flags)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      */
registerCallback(@onNull Callback callback)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      */
registerCallback(@onNull Callback callback, @Nullable Handler handler)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      */
unregisterCallback(@onNull Callback callback)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      */
sendCommand(@onNull String command, @Nullable Bundle args, @Nullable ResultReceiver cb)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      */
getPackageName()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      */
getTag()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      */
getSessionBinder()433     ISessionController getSessionBinder() {
434         return mSessionBinder;
435     }
436 
437     /**
438      * @hide
439      */
controlsSameSession(MediaController other)440     public boolean controlsSameSession(MediaController other) {
441         if (other == null) return false;
442         return mSessionBinder.asBinder() == other.getSessionBinder().asBinder();
443     }
444 
addCallbackLocked(Callback cb, Handler handler)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 
removeCallbackLocked(Callback cb)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 
getHandlerForCallbackLocked(Callback cb)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 
postMessage(int what, Object obj, Bundle data)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          */
onSessionDestroyed()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          */
onSessionEvent(@onNull String event, @Nullable Bundle extras)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          */
onPlaybackStateChanged(@onNull PlaybackState state)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          */
onMetadataChanged(@ullable MediaMetadata metadata)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          */
onQueueChanged(@ullable List<MediaSession.QueueItem> queue)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          */
onQueueTitleChanged(@ullable CharSequence title)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          */
onExtrasChanged(@ullable Bundle extras)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          */
onAudioInfoChanged(PlaybackInfo info)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 
TransportControls()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          */
prepare()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          */
prepareFromMediaId(String mediaId, Bundle extras)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          */
prepareFromSearch(String query, Bundle extras)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          */
prepareFromUri(Uri uri, Bundle extras)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          */
play()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          */
playFromMediaId(String mediaId, Bundle extras)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          */
playFromSearch(String query, Bundle extras)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          */
playFromUri(Uri uri, Bundle extras)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          */
skipToQueueItem(long id)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          */
pause()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          */
stop()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          */
seekTo(long pos)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          */
fastForward()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          */
skipToNext()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          */
rewind()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          */
skipToPrevious()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          */
setRating(Rating rating)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          */
sendCustomAction(@onNull PlaybackState.CustomAction customAction, @Nullable Bundle args)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          */
sendCustomAction(@onNull String action, @Nullable Bundle args)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          */
PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current)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          */
getPlaybackType()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          */
getAudioAttributes()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          */
getVolumeControl()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          */
getMaxVolume()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          */
getCurrentVolume()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 
CallbackStub(MediaController controller)993         public CallbackStub(MediaController controller) {
994             mController = new WeakReference<MediaController>(controller);
995         }
996 
997         @Override
onSessionDestroyed()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
onEvent(String event, Bundle extras)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
onPlaybackStateChanged(PlaybackState state)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
onMetadataChanged(MediaMetadata metadata)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
onQueueChanged(ParceledListSlice parceledQueue)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
onQueueTitleChanged(CharSequence title)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
onExtrasChanged(Bundle extras)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
onVolumeInfoChanged(ParcelableVolumeInfo pvi)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 
MessageHandler(Looper looper, MediaController.Callback cb)1071         public MessageHandler(Looper looper, MediaController.Callback cb) {
1072             super(looper, null, true);
1073             mCallback = cb;
1074         }
1075 
1076         @Override
handleMessage(Message msg)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 
post(int what, Object obj, Bundle data)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