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