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