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