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