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