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