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