• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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;
18 
19 import android.app.ActivityManager;
20 import android.compat.annotation.UnsupportedAppUsage;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.graphics.Bitmap;
24 import android.media.session.MediaController;
25 import android.media.session.MediaSession;
26 import android.media.session.MediaSessionLegacyHelper;
27 import android.media.session.MediaSessionManager;
28 import android.media.session.PlaybackState;
29 import android.os.Build;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.Looper;
33 import android.os.Message;
34 import android.os.UserHandle;
35 import android.util.DisplayMetrics;
36 import android.util.Log;
37 import android.view.KeyEvent;
38 
39 import java.util.List;
40 
41 /**
42  * The RemoteController class is used to control media playback, display and update media metadata
43  * and playback status, published by applications using the {@link RemoteControlClient} class.
44  * <p>
45  * A RemoteController shall be registered through
46  * {@link AudioManager#registerRemoteController(RemoteController)} in order for the system to send
47  * media event updates to the {@link OnClientUpdateListener} listener set in the class constructor.
48  * Implement the methods of the interface to receive the information published by the active
49  * {@link RemoteControlClient} instances.
50  * <br>By default an {@link OnClientUpdateListener} implementation will not receive bitmaps for
51  * album art. Use {@link #setArtworkConfiguration(int, int)} to receive images as well.
52  * <p>
53  * Registration requires the {@link OnClientUpdateListener} listener to be one of the enabled
54  * notification listeners (see {@link android.service.notification.NotificationListenerService}).
55  *
56  * @deprecated Use {@link MediaController} instead.
57  */
58 @Deprecated public final class RemoteController
59 {
60     private final static int MAX_BITMAP_DIMENSION = 512;
61     private final static String TAG = "RemoteController";
62     private final static boolean DEBUG = false;
63     private final static Object mInfoLock = new Object();
64     private final Context mContext;
65     private final int mMaxBitmapDimension;
66     private MetadataEditor mMetadataEditor;
67 
68     private MediaSessionManager mSessionManager;
69     private MediaSessionManager.OnActiveSessionsChangedListener mSessionListener;
70     private MediaController.Callback mSessionCb = new MediaControllerCallback();
71 
72     /**
73      * Synchronized on mInfoLock
74      */
75     private boolean mIsRegistered = false;
76     private OnClientUpdateListener mOnClientUpdateListener;
77     private PlaybackInfo mLastPlaybackInfo;
78     private int mArtworkWidth = -1;
79     private int mArtworkHeight = -1;
80     private boolean mEnabled = true;
81     // synchronized on mInfoLock, for USE_SESSION apis.
82     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
83     private MediaController mCurrentSession;
84 
85     /**
86      * Class constructor.
87      * @param context the {@link Context}, must be non-null.
88      * @param updateListener the listener to be called whenever new client information is available,
89      *     must be non-null.
90      * @throws IllegalArgumentException
91      */
RemoteController(Context context, OnClientUpdateListener updateListener)92     public RemoteController(Context context, OnClientUpdateListener updateListener)
93             throws IllegalArgumentException {
94         this(context, updateListener, null);
95     }
96 
97     /**
98      * Class constructor.
99      * @param context the {@link Context}, must be non-null.
100      * @param updateListener the listener to be called whenever new client information is available,
101      *     must be non-null.
102      * @param looper the {@link Looper} on which to run the event loop,
103      *     or null to use the current thread's looper.
104      * @throws java.lang.IllegalArgumentException
105      */
RemoteController(Context context, OnClientUpdateListener updateListener, Looper looper)106     public RemoteController(Context context, OnClientUpdateListener updateListener, Looper looper)
107             throws IllegalArgumentException {
108         if (context == null) {
109             throw new IllegalArgumentException("Invalid null Context");
110         }
111         if (updateListener == null) {
112             throw new IllegalArgumentException("Invalid null OnClientUpdateListener");
113         }
114         if (looper != null) {
115             mEventHandler = new EventHandler(this, looper);
116         } else {
117             Looper l = Looper.myLooper();
118             if (l != null) {
119                 mEventHandler = new EventHandler(this, l);
120             } else {
121                 throw new IllegalArgumentException("Calling thread not associated with a looper");
122             }
123         }
124         mOnClientUpdateListener = updateListener;
125         mContext = context;
126         mSessionManager = (MediaSessionManager) context
127                 .getSystemService(Context.MEDIA_SESSION_SERVICE);
128         mSessionListener = new TopTransportSessionListener();
129 
130         if (ActivityManager.isLowRamDeviceStatic()) {
131             mMaxBitmapDimension = MAX_BITMAP_DIMENSION;
132         } else {
133             final DisplayMetrics dm = context.getResources().getDisplayMetrics();
134             mMaxBitmapDimension = Math.max(dm.widthPixels, dm.heightPixels);
135         }
136     }
137 
138 
139     /**
140      * Interface definition for the callbacks to be invoked whenever media events, metadata
141      * and playback status are available.
142      */
143     public interface OnClientUpdateListener {
144         /**
145          * Called whenever all information, previously received through the other
146          * methods of the listener, is no longer valid and is about to be refreshed.
147          * This is typically called whenever a new {@link RemoteControlClient} has been selected
148          * by the system to have its media information published.
149          * @param clearing true if there is no selected RemoteControlClient and no information
150          *     is available.
151          */
onClientChange(boolean clearing)152         public void onClientChange(boolean clearing);
153 
154         /**
155          * Called whenever the playback state has changed.
156          * It is called when no information is known about the playback progress in the media and
157          * the playback speed.
158          * @param state one of the playback states authorized
159          *     in {@link RemoteControlClient#setPlaybackState(int)}.
160          */
onClientPlaybackStateUpdate(int state)161         public void onClientPlaybackStateUpdate(int state);
162         /**
163          * Called whenever the playback state has changed, and playback position
164          * and speed are known.
165          * @param state one of the playback states authorized
166          *     in {@link RemoteControlClient#setPlaybackState(int)}.
167          * @param stateChangeTimeMs the system time at which the state change was reported,
168          *     expressed in ms. Based on {@link android.os.SystemClock#elapsedRealtime()}.
169          * @param currentPosMs a positive value for the current media playback position expressed
170          *     in ms, a negative value if the position is temporarily unknown.
171          * @param speed  a value expressed as a ratio of 1x playback: 1.0f is normal playback,
172          *    2.0f is 2x, 0.5f is half-speed, -2.0f is rewind at 2x speed. 0.0f means nothing is
173          *    playing (e.g. when state is {@link RemoteControlClient#PLAYSTATE_ERROR}).
174          */
onClientPlaybackStateUpdate(int state, long stateChangeTimeMs, long currentPosMs, float speed)175         public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs,
176                 long currentPosMs, float speed);
177         /**
178          * Called whenever the transport control flags have changed.
179          * @param transportControlFlags one of the flags authorized
180          *     in {@link RemoteControlClient#setTransportControlFlags(int)}.
181          */
onClientTransportControlUpdate(int transportControlFlags)182         public void onClientTransportControlUpdate(int transportControlFlags);
183         /**
184          * Called whenever new metadata is available.
185          * See the {@link MediaMetadataEditor#putLong(int, long)},
186          *  {@link MediaMetadataEditor#putString(int, String)},
187          *  {@link MediaMetadataEditor#putBitmap(int, Bitmap)}, and
188          *  {@link MediaMetadataEditor#putObject(int, Object)} methods for the various keys that
189          *  can be queried.
190          * @param metadataEditor the container of the new metadata.
191          */
onClientMetadataUpdate(MetadataEditor metadataEditor)192         public void onClientMetadataUpdate(MetadataEditor metadataEditor);
193     };
194 
195     /**
196      * Return the estimated playback position of the current media track or a negative value
197      * if not available.
198      *
199      * <p>The value returned is estimated by the current process and may not be perfect.
200      * The time returned by this method is calculated from the last state change time based
201      * on the current play position at that time and the last known playback speed.
202      * An application may call {@link #setSynchronizationMode(int)} to apply
203      * a synchronization policy that will periodically re-sync the estimated position
204      * with the RemoteControlClient.</p>
205      *
206      * @return the current estimated playback position in milliseconds or a negative value
207      *         if not available
208      *
209      * @see OnClientUpdateListener#onClientPlaybackStateUpdate(int, long, long, float)
210      */
getEstimatedMediaPosition()211     public long getEstimatedMediaPosition() {
212         synchronized (mInfoLock) {
213             if (mCurrentSession != null) {
214                 PlaybackState state = mCurrentSession.getPlaybackState();
215                 if (state != null) {
216                     return state.getPosition();
217                 }
218             }
219         }
220         return -1;
221     }
222 
223     /**
224      * Send a simulated key event for a media button to be received by the current client. To
225      * simulate a key press, you must first send a KeyEvent built with a {@link
226      * KeyEvent#ACTION_DOWN} action, then another event with the {@link KeyEvent#ACTION_UP} action.
227      *
228      * <p>The key event will be sent to the registered receiver (see {@link
229      * AudioManager#registerMediaButtonEventReceiver(PendingIntent)}) whose associated {@link
230      * RemoteControlClient}'s metadata and playback state is published (there may be none under some
231      * circumstances).
232      *
233      * @param keyEvent a media session {@link KeyEvent}, as defined by {@link
234      *     KeyEvent#isMediaSessionKey}.
235      * @return true if the event was successfully sent, false otherwise.
236      * @throws IllegalArgumentException If the provided {@link KeyEvent} is not a media session key,
237      *     as defined by {@link KeyEvent#isMediaSessionKey}.
238      */
sendMediaKeyEvent(KeyEvent keyEvent)239     public boolean sendMediaKeyEvent(KeyEvent keyEvent) throws IllegalArgumentException {
240         if (!KeyEvent.isMediaSessionKey(keyEvent.getKeyCode())) {
241             throw new IllegalArgumentException("not a media key event");
242         }
243         synchronized (mInfoLock) {
244             if (mCurrentSession != null) {
245                 return mCurrentSession.dispatchMediaButtonEvent(keyEvent);
246             }
247             return false;
248         }
249     }
250 
251 
252     /**
253      * Sets the new playback position.
254      * This method can only be called on a registered RemoteController.
255      * @param timeMs a 0 or positive value for the new playback position, expressed in ms.
256      * @return true if the command to set the playback position was successfully sent.
257      * @throws IllegalArgumentException
258      */
seekTo(long timeMs)259     public boolean seekTo(long timeMs) throws IllegalArgumentException {
260         if (!mEnabled) {
261             Log.e(TAG, "Cannot use seekTo() from a disabled RemoteController");
262             return false;
263         }
264         if (timeMs < 0) {
265             throw new IllegalArgumentException("illegal negative time value");
266         }
267         synchronized (mInfoLock) {
268             if (mCurrentSession != null) {
269                 mCurrentSession.getTransportControls().seekTo(timeMs);
270             }
271         }
272         return true;
273     }
274 
275 
276     /**
277      * @hide
278      * @param wantBitmap
279      * @param width
280      * @param height
281      * @return true if successful
282      * @throws IllegalArgumentException
283      */
284     @UnsupportedAppUsage
setArtworkConfiguration(boolean wantBitmap, int width, int height)285     public boolean setArtworkConfiguration(boolean wantBitmap, int width, int height)
286             throws IllegalArgumentException {
287         synchronized (mInfoLock) {
288             if (wantBitmap) {
289                 if ((width > 0) && (height > 0)) {
290                     if (width > mMaxBitmapDimension) { width = mMaxBitmapDimension; }
291                     if (height > mMaxBitmapDimension) { height = mMaxBitmapDimension; }
292                     mArtworkWidth = width;
293                     mArtworkHeight = height;
294                 } else {
295                     throw new IllegalArgumentException("Invalid dimensions");
296                 }
297             } else {
298                 mArtworkWidth = -1;
299                 mArtworkHeight = -1;
300             }
301         }
302         return true;
303     }
304 
305     /**
306      * Set the maximum artwork image dimensions to be received in the metadata.
307      * No bitmaps will be received unless this has been specified.
308      * @param width the maximum width in pixels
309      * @param height  the maximum height in pixels
310      * @return true if the artwork dimension was successfully set.
311      * @throws IllegalArgumentException
312      */
setArtworkConfiguration(int width, int height)313     public boolean setArtworkConfiguration(int width, int height) throws IllegalArgumentException {
314         return setArtworkConfiguration(true, width, height);
315     }
316 
317     /**
318      * Prevents this RemoteController from receiving artwork images.
319      * @return true if receiving artwork images was successfully disabled.
320      */
clearArtworkConfiguration()321     public boolean clearArtworkConfiguration() {
322         return setArtworkConfiguration(false, -1, -1);
323     }
324 
325 
326     /**
327      * Default playback position synchronization mode where the RemoteControlClient is not
328      * asked regularly for its playback position to see if it has drifted from the estimated
329      * position.
330      */
331     public static final int POSITION_SYNCHRONIZATION_NONE = 0;
332 
333     /**
334      * The playback position synchronization mode where the RemoteControlClient instances which
335      * expose their playback position to the framework, will be regularly polled to check
336      * whether any drift has been noticed between their estimated position and the one they report.
337      * Note that this mode should only ever be used when needing to display very accurate playback
338      * position, as regularly polling a RemoteControlClient for its position may have an impact
339      * on battery life (if applicable) when this query will trigger network transactions in the
340      * case of remote playback.
341      */
342     public static final int POSITION_SYNCHRONIZATION_CHECK = 1;
343 
344     /**
345      * Set the playback position synchronization mode.
346      * Must be called on a registered RemoteController.
347      * @param sync {@link #POSITION_SYNCHRONIZATION_NONE} or {@link #POSITION_SYNCHRONIZATION_CHECK}
348      * @return true if the synchronization mode was successfully set.
349      * @throws IllegalArgumentException
350      */
setSynchronizationMode(int sync)351     public boolean setSynchronizationMode(int sync) throws IllegalArgumentException {
352         if ((sync != POSITION_SYNCHRONIZATION_NONE) && (sync != POSITION_SYNCHRONIZATION_CHECK)) {
353             throw new IllegalArgumentException("Unknown synchronization mode " + sync);
354         }
355         if (!mIsRegistered) {
356             Log.e(TAG, "Cannot set synchronization mode on an unregistered RemoteController");
357             return false;
358         }
359         // deprecated, no-op
360         return true;
361     }
362 
363 
364     /**
365      * Creates a {@link MetadataEditor} for updating metadata values of the editable keys of
366      * the current {@link RemoteControlClient}.
367      * This method can only be called on a registered RemoteController.
368      * @return a new MetadataEditor instance.
369      */
editMetadata()370     public MetadataEditor editMetadata() {
371         MetadataEditor editor = new MetadataEditor();
372         editor.mEditorMetadata = new Bundle();
373         editor.mEditorArtwork = null;
374         editor.mMetadataChanged = true;
375         editor.mArtworkChanged = true;
376         editor.mEditableKeys = 0;
377         return editor;
378     }
379 
380     /**
381      * A class to read the metadata published by a {@link RemoteControlClient}, or send a
382      * {@link RemoteControlClient} new values for keys that can be edited.
383      */
384     public class MetadataEditor extends MediaMetadataEditor {
385         /**
386          * @hide
387          */
MetadataEditor()388         protected MetadataEditor() { }
389 
390         /**
391          * @hide
392          */
MetadataEditor(Bundle metadata, long editableKeys)393         protected MetadataEditor(Bundle metadata, long editableKeys) {
394             mEditorMetadata = metadata;
395             mEditableKeys = editableKeys;
396 
397             mEditorArtwork = (Bitmap) metadata.getParcelable(
398                     String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK), android.graphics.Bitmap.class);
399             if (mEditorArtwork != null) {
400                 cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK);
401             }
402 
403             mMetadataChanged = true;
404             mArtworkChanged = true;
405             mApplied = false;
406         }
407 
cleanupBitmapFromBundle(int key)408         private void cleanupBitmapFromBundle(int key) {
409             if (METADATA_KEYS_TYPE.get(key, METADATA_TYPE_INVALID) == METADATA_TYPE_BITMAP) {
410                 mEditorMetadata.remove(String.valueOf(key));
411             }
412         }
413 
414         /**
415          * Applies all of the metadata changes that have been set since the MediaMetadataEditor
416          * instance was created with {@link RemoteController#editMetadata()}
417          * or since {@link #clear()} was called.
418          */
apply()419         public synchronized void apply() {
420             // "applying" a metadata bundle in RemoteController is only for sending edited
421             // key values back to the RemoteControlClient, so here we only care about the only
422             // editable key we support: RATING_KEY_BY_USER
423             if (!mMetadataChanged) {
424                 return;
425             }
426             synchronized (mInfoLock) {
427                 if (mCurrentSession != null) {
428                     if (mEditorMetadata.containsKey(
429                             String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER))) {
430                         Rating rating = (Rating) getObject(
431                                 MediaMetadataEditor.RATING_KEY_BY_USER, null);
432                         if (rating != null) {
433                             mCurrentSession.getTransportControls().setRating(rating);
434                         }
435                     }
436                 }
437             }
438             // NOT setting mApplied to true as this type of MetadataEditor will be applied
439             // multiple times, whenever the user of a RemoteController needs to change the
440             // metadata (e.g. user changes the rating of a song more than once during playback)
441             mApplied = false;
442         }
443 
444     }
445 
446     /**
447      * This receives updates when the current session changes. This is
448      * registered to receive the updates on the handler thread so it can call
449      * directly into the appropriate methods.
450      */
451     private class MediaControllerCallback extends MediaController.Callback {
452         @Override
onPlaybackStateChanged(PlaybackState state)453         public void onPlaybackStateChanged(PlaybackState state) {
454             onNewPlaybackState(state);
455         }
456 
457         @Override
onMetadataChanged(MediaMetadata metadata)458         public void onMetadataChanged(MediaMetadata metadata) {
459             onNewMediaMetadata(metadata);
460         }
461     }
462 
463     /**
464      * Listens for changes to the active session stack and replaces the
465      * currently tracked session if it has changed.
466      */
467     private class TopTransportSessionListener implements
468             MediaSessionManager.OnActiveSessionsChangedListener {
469 
470         @Override
onActiveSessionsChanged(List<MediaController> controllers)471         public void onActiveSessionsChanged(List<MediaController> controllers) {
472             int size = controllers.size();
473             for (int i = 0; i < size; i++) {
474                 MediaController controller = controllers.get(i);
475                 long flags = controller.getFlags();
476                 // We only care about sessions that handle transport controls,
477                 // which will be true for apps using RCC
478                 if ((flags & MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS) != 0) {
479                     updateController(controller);
480                     return;
481                 }
482             }
483             updateController(null);
484         }
485 
486     }
487 
488     //==================================================
489     // Event handling
490     private final EventHandler mEventHandler;
491     private final static int MSG_CLIENT_CHANGE      = 0;
492     private final static int MSG_NEW_PLAYBACK_STATE = 1;
493     private final static int MSG_NEW_MEDIA_METADATA = 2;
494 
495     private class EventHandler extends Handler {
496 
EventHandler(RemoteController rc, Looper looper)497         public EventHandler(RemoteController rc, Looper looper) {
498             super(looper);
499         }
500 
501         @Override
handleMessage(Message msg)502         public void handleMessage(Message msg) {
503             switch(msg.what) {
504                 case MSG_CLIENT_CHANGE:
505                     onClientChange(msg.arg2 == 1);
506                     break;
507                 case MSG_NEW_PLAYBACK_STATE:
508                     onNewPlaybackState((PlaybackState) msg.obj);
509                     break;
510                 case MSG_NEW_MEDIA_METADATA:
511                     onNewMediaMetadata((MediaMetadata) msg.obj);
512                     break;
513                 default:
514                     Log.e(TAG, "unknown event " + msg.what);
515             }
516         }
517     }
518 
519     /**
520      * @hide
521      */
startListeningToSessions()522     void startListeningToSessions() {
523         final ComponentName listenerComponent = new ComponentName(mContext,
524                 mOnClientUpdateListener.getClass());
525         Handler handler = null;
526         if (Looper.myLooper() == null) {
527             handler = new Handler(Looper.getMainLooper());
528         }
529         mSessionManager.addOnActiveSessionsChangedListener(mSessionListener, listenerComponent,
530                 handler);
531         mSessionListener.onActiveSessionsChanged(mSessionManager
532                 .getActiveSessions(listenerComponent));
533         if (DEBUG) {
534             Log.d(TAG, "Registered session listener with component " + listenerComponent
535                     + " for user " + UserHandle.myUserId());
536         }
537     }
538 
539     /**
540      * @hide
541      */
stopListeningToSessions()542     void stopListeningToSessions() {
543         mSessionManager.removeOnActiveSessionsChangedListener(mSessionListener);
544         if (DEBUG) {
545             Log.d(TAG, "Unregistered session listener for user "
546                     + UserHandle.myUserId());
547         }
548     }
549 
550     /** If the msg is already queued, replace it with this one. */
551     private static final int SENDMSG_REPLACE = 0;
552     /** If the msg is already queued, ignore this one and leave the old. */
553     private static final int SENDMSG_NOOP = 1;
554     /** If the msg is already queued, queue this one and leave the old. */
555     private static final int SENDMSG_QUEUE = 2;
556 
sendMsg(Handler handler, int msg, int existingMsgPolicy, int arg1, int arg2, Object obj, int delayMs)557     private static void sendMsg(Handler handler, int msg, int existingMsgPolicy,
558             int arg1, int arg2, Object obj, int delayMs) {
559         if (handler == null) {
560             Log.e(TAG, "null event handler, will not deliver message " + msg);
561             return;
562         }
563         if (existingMsgPolicy == SENDMSG_REPLACE) {
564             handler.removeMessages(msg);
565         } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) {
566             return;
567         }
568         handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delayMs);
569     }
570 
onClientChange(boolean clearing)571     private void onClientChange(boolean clearing) {
572         final OnClientUpdateListener l;
573         synchronized(mInfoLock) {
574             l = mOnClientUpdateListener;
575             mMetadataEditor = null;
576         }
577         if (l != null) {
578             l.onClientChange(clearing);
579         }
580     }
581 
updateController(MediaController controller)582     private void updateController(MediaController controller) {
583         if (DEBUG) {
584             Log.d(TAG, "Updating controller to " + controller + " previous controller is "
585                     + mCurrentSession);
586         }
587         synchronized (mInfoLock) {
588             if (controller == null) {
589                 if (mCurrentSession != null) {
590                     mCurrentSession.unregisterCallback(mSessionCb);
591                     mCurrentSession = null;
592                     sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE,
593                             0 /* arg1 ignored */, 1 /* clearing */, null /* obj */, 0 /* delay */);
594                 }
595             } else if (mCurrentSession == null
596                     || !controller.getSessionToken()
597                             .equals(mCurrentSession.getSessionToken())) {
598                 if (mCurrentSession != null) {
599                     mCurrentSession.unregisterCallback(mSessionCb);
600                 }
601                 sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE,
602                         0 /* arg1 ignored */, 0 /* clearing */, null /* obj */, 0 /* delay */);
603                 mCurrentSession = controller;
604                 mCurrentSession.registerCallback(mSessionCb, mEventHandler);
605 
606                 PlaybackState state = controller.getPlaybackState();
607                 sendMsg(mEventHandler, MSG_NEW_PLAYBACK_STATE, SENDMSG_REPLACE,
608                         0 /* arg1 ignored */, 0 /* arg2 ignored */, state /* obj */, 0 /* delay */);
609 
610                 MediaMetadata metadata = controller.getMetadata();
611                 sendMsg(mEventHandler, MSG_NEW_MEDIA_METADATA, SENDMSG_REPLACE,
612                         0 /* arg1 ignored */, 0 /* arg2 ignored*/, metadata /* obj */, 0 /*delay*/);
613             }
614             // else same controller, no need to update
615         }
616     }
617 
onNewPlaybackState(PlaybackState state)618     private void onNewPlaybackState(PlaybackState state) {
619         final OnClientUpdateListener l;
620         synchronized (mInfoLock) {
621             l = this.mOnClientUpdateListener;
622         }
623         if (l != null) {
624             int playstate = state == null ? RemoteControlClient.PLAYSTATE_NONE
625                     : RemoteControlClient.getRccStateFromState(state.getState());
626             if (state == null || state.getPosition() == PlaybackState.PLAYBACK_POSITION_UNKNOWN) {
627                 l.onClientPlaybackStateUpdate(playstate);
628             } else {
629                 l.onClientPlaybackStateUpdate(playstate, state.getLastPositionUpdateTime(),
630                         state.getPosition(), state.getPlaybackSpeed());
631             }
632             if (state != null) {
633                 l.onClientTransportControlUpdate(
634                         RemoteControlClient.getRccControlFlagsFromActions(state.getActions()));
635             }
636         }
637     }
638 
onNewMediaMetadata(MediaMetadata metadata)639     private void onNewMediaMetadata(MediaMetadata metadata) {
640         if (metadata == null) {
641             // RemoteController only handles non-null metadata
642             return;
643         }
644         final OnClientUpdateListener l;
645         final MetadataEditor metadataEditor;
646         // prepare the received Bundle to be used inside a MetadataEditor
647         synchronized(mInfoLock) {
648             l = mOnClientUpdateListener;
649             boolean canRate = mCurrentSession != null
650                     && mCurrentSession.getRatingType() != Rating.RATING_NONE;
651             long editableKeys = canRate ? MediaMetadataEditor.RATING_KEY_BY_USER : 0;
652             Bundle legacyMetadata = MediaSessionLegacyHelper.getOldMetadata(metadata,
653                     mArtworkWidth, mArtworkHeight);
654             mMetadataEditor = new MetadataEditor(legacyMetadata, editableKeys);
655             metadataEditor = mMetadataEditor;
656         }
657         if (l != null) {
658             l.onClientMetadataUpdate(metadataEditor);
659         }
660     }
661 
662     //==================================================
663     private static class PlaybackInfo {
664         int mState;
665         long mStateChangeTimeMs;
666         long mCurrentPosMs;
667         float mSpeed;
668 
PlaybackInfo(int state, long stateChangeTimeMs, long currentPosMs, float speed)669         PlaybackInfo(int state, long stateChangeTimeMs, long currentPosMs, float speed) {
670             mState = state;
671             mStateChangeTimeMs = stateChangeTimeMs;
672             mCurrentPosMs = currentPosMs;
673             mSpeed = speed;
674         }
675     }
676 
677     /**
678      * @hide
679      * Used by AudioManager to access user listener receiving the client update notifications
680      * @return
681      */
682     @UnsupportedAppUsage
getUpdateListener()683     OnClientUpdateListener getUpdateListener() {
684         return mOnClientUpdateListener;
685     }
686 }
687