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 17package android.media; 18 19import android.app.ActivityManager; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.Intent; 23import android.graphics.Bitmap; 24import android.media.session.MediaController; 25import android.media.session.MediaSession; 26import android.media.session.MediaSessionLegacyHelper; 27import android.media.session.MediaSessionManager; 28import android.media.session.PlaybackState; 29import android.os.Bundle; 30import android.os.Handler; 31import android.os.Looper; 32import android.os.Message; 33import android.os.UserHandle; 34import android.util.DisplayMetrics; 35import android.util.Log; 36import android.view.KeyEvent; 37 38import java.lang.ref.WeakReference; 39import 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 250 public boolean sendMediaKeyEvent(KeyEvent keyEvent) throws IllegalArgumentException { 251 if (!KeyEvent.isMediaKey(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 */ 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 public boolean setArtworkConfiguration(boolean wantBitmap, int width, int height) 296 throws IllegalArgumentException { 297 synchronized (mInfoLock) { 298 if (wantBitmap) { 299 if ((width > 0) && (height > 0)) { 300 if (width > mMaxBitmapDimension) { width = mMaxBitmapDimension; } 301 if (height > mMaxBitmapDimension) { height = mMaxBitmapDimension; } 302 mArtworkWidth = width; 303 mArtworkHeight = height; 304 } else { 305 throw new IllegalArgumentException("Invalid dimensions"); 306 } 307 } else { 308 mArtworkWidth = -1; 309 mArtworkHeight = -1; 310 } 311 } 312 return true; 313 } 314 315 /** 316 * Set the maximum artwork image dimensions to be received in the metadata. 317 * No bitmaps will be received unless this has been specified. 318 * @param width the maximum width in pixels 319 * @param height the maximum height in pixels 320 * @return true if the artwork dimension was successfully set. 321 * @throws IllegalArgumentException 322 */ 323 public boolean setArtworkConfiguration(int width, int height) throws IllegalArgumentException { 324 return setArtworkConfiguration(true, width, height); 325 } 326 327 /** 328 * Prevents this RemoteController from receiving artwork images. 329 * @return true if receiving artwork images was successfully disabled. 330 */ 331 public boolean clearArtworkConfiguration() { 332 return setArtworkConfiguration(false, -1, -1); 333 } 334 335 336 /** 337 * Default playback position synchronization mode where the RemoteControlClient is not 338 * asked regularly for its playback position to see if it has drifted from the estimated 339 * position. 340 */ 341 public static final int POSITION_SYNCHRONIZATION_NONE = 0; 342 343 /** 344 * The playback position synchronization mode where the RemoteControlClient instances which 345 * expose their playback position to the framework, will be regularly polled to check 346 * whether any drift has been noticed between their estimated position and the one they report. 347 * Note that this mode should only ever be used when needing to display very accurate playback 348 * position, as regularly polling a RemoteControlClient for its position may have an impact 349 * on battery life (if applicable) when this query will trigger network transactions in the 350 * case of remote playback. 351 */ 352 public static final int POSITION_SYNCHRONIZATION_CHECK = 1; 353 354 /** 355 * Set the playback position synchronization mode. 356 * Must be called on a registered RemoteController. 357 * @param sync {@link #POSITION_SYNCHRONIZATION_NONE} or {@link #POSITION_SYNCHRONIZATION_CHECK} 358 * @return true if the synchronization mode was successfully set. 359 * @throws IllegalArgumentException 360 */ 361 public boolean setSynchronizationMode(int sync) throws IllegalArgumentException { 362 if ((sync != POSITION_SYNCHRONIZATION_NONE) && (sync != POSITION_SYNCHRONIZATION_CHECK)) { 363 throw new IllegalArgumentException("Unknown synchronization mode " + sync); 364 } 365 if (!mIsRegistered) { 366 Log.e(TAG, "Cannot set synchronization mode on an unregistered RemoteController"); 367 return false; 368 } 369 // deprecated, no-op 370 return true; 371 } 372 373 374 /** 375 * Creates a {@link MetadataEditor} for updating metadata values of the editable keys of 376 * the current {@link RemoteControlClient}. 377 * This method can only be called on a registered RemoteController. 378 * @return a new MetadataEditor instance. 379 */ 380 public MetadataEditor editMetadata() { 381 MetadataEditor editor = new MetadataEditor(); 382 editor.mEditorMetadata = new Bundle(); 383 editor.mEditorArtwork = null; 384 editor.mMetadataChanged = true; 385 editor.mArtworkChanged = true; 386 editor.mEditableKeys = 0; 387 return editor; 388 } 389 390 /** 391 * A class to read the metadata published by a {@link RemoteControlClient}, or send a 392 * {@link RemoteControlClient} new values for keys that can be edited. 393 */ 394 public class MetadataEditor extends MediaMetadataEditor { 395 /** 396 * @hide 397 */ 398 protected MetadataEditor() { } 399 400 /** 401 * @hide 402 */ 403 protected MetadataEditor(Bundle metadata, long editableKeys) { 404 mEditorMetadata = metadata; 405 mEditableKeys = editableKeys; 406 407 mEditorArtwork = (Bitmap) metadata.getParcelable( 408 String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK)); 409 if (mEditorArtwork != null) { 410 cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK); 411 } 412 413 mMetadataChanged = true; 414 mArtworkChanged = true; 415 mApplied = false; 416 } 417 418 private void cleanupBitmapFromBundle(int key) { 419 if (METADATA_KEYS_TYPE.get(key, METADATA_TYPE_INVALID) == METADATA_TYPE_BITMAP) { 420 mEditorMetadata.remove(String.valueOf(key)); 421 } 422 } 423 424 /** 425 * Applies all of the metadata changes that have been set since the MediaMetadataEditor 426 * instance was created with {@link RemoteController#editMetadata()} 427 * or since {@link #clear()} was called. 428 */ 429 public synchronized void apply() { 430 // "applying" a metadata bundle in RemoteController is only for sending edited 431 // key values back to the RemoteControlClient, so here we only care about the only 432 // editable key we support: RATING_KEY_BY_USER 433 if (!mMetadataChanged) { 434 return; 435 } 436 synchronized (mInfoLock) { 437 if (mCurrentSession != null) { 438 if (mEditorMetadata.containsKey( 439 String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER))) { 440 Rating rating = (Rating) getObject( 441 MediaMetadataEditor.RATING_KEY_BY_USER, null); 442 if (rating != null) { 443 mCurrentSession.getTransportControls().setRating(rating); 444 } 445 } 446 } 447 } 448 // NOT setting mApplied to true as this type of MetadataEditor will be applied 449 // multiple times, whenever the user of a RemoteController needs to change the 450 // metadata (e.g. user changes the rating of a song more than once during playback) 451 mApplied = false; 452 } 453 454 } 455 456 /** 457 * This receives updates when the current session changes. This is 458 * registered to receive the updates on the handler thread so it can call 459 * directly into the appropriate methods. 460 */ 461 private class MediaControllerCallback extends MediaController.Callback { 462 @Override 463 public void onPlaybackStateChanged(PlaybackState state) { 464 onNewPlaybackState(state); 465 } 466 467 @Override 468 public void onMetadataChanged(MediaMetadata metadata) { 469 onNewMediaMetadata(metadata); 470 } 471 } 472 473 /** 474 * Listens for changes to the active session stack and replaces the 475 * currently tracked session if it has changed. 476 */ 477 private class TopTransportSessionListener implements 478 MediaSessionManager.OnActiveSessionsChangedListener { 479 480 @Override 481 public void onActiveSessionsChanged(List<MediaController> controllers) { 482 int size = controllers.size(); 483 for (int i = 0; i < size; i++) { 484 MediaController controller = controllers.get(i); 485 long flags = controller.getFlags(); 486 // We only care about sessions that handle transport controls, 487 // which will be true for apps using RCC 488 if ((flags & MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS) != 0) { 489 updateController(controller); 490 return; 491 } 492 } 493 updateController(null); 494 } 495 496 } 497 498 //================================================== 499 // Event handling 500 private final EventHandler mEventHandler; 501 private final static int MSG_CLIENT_CHANGE = 0; 502 private final static int MSG_NEW_PLAYBACK_STATE = 1; 503 private final static int MSG_NEW_MEDIA_METADATA = 2; 504 505 private class EventHandler extends Handler { 506 507 public EventHandler(RemoteController rc, Looper looper) { 508 super(looper); 509 } 510 511 @Override 512 public void handleMessage(Message msg) { 513 switch(msg.what) { 514 case MSG_CLIENT_CHANGE: 515 onClientChange(msg.arg2 == 1); 516 break; 517 case MSG_NEW_PLAYBACK_STATE: 518 onNewPlaybackState((PlaybackState) msg.obj); 519 break; 520 case MSG_NEW_MEDIA_METADATA: 521 onNewMediaMetadata((MediaMetadata) msg.obj); 522 break; 523 default: 524 Log.e(TAG, "unknown event " + msg.what); 525 } 526 } 527 } 528 529 /** 530 * @hide 531 */ 532 void startListeningToSessions() { 533 final ComponentName listenerComponent = new ComponentName(mContext, 534 mOnClientUpdateListener.getClass()); 535 Handler handler = null; 536 if (Looper.myLooper() == null) { 537 handler = new Handler(Looper.getMainLooper()); 538 } 539 mSessionManager.addOnActiveSessionsChangedListener(mSessionListener, listenerComponent, 540 UserHandle.myUserId(), handler); 541 mSessionListener.onActiveSessionsChanged(mSessionManager 542 .getActiveSessions(listenerComponent)); 543 if (DEBUG) { 544 Log.d(TAG, "Registered session listener with component " + listenerComponent 545 + " for user " + UserHandle.myUserId()); 546 } 547 } 548 549 /** 550 * @hide 551 */ 552 void stopListeningToSessions() { 553 mSessionManager.removeOnActiveSessionsChangedListener(mSessionListener); 554 if (DEBUG) { 555 Log.d(TAG, "Unregistered session listener for user " 556 + UserHandle.myUserId()); 557 } 558 } 559 560 /** If the msg is already queued, replace it with this one. */ 561 private static final int SENDMSG_REPLACE = 0; 562 /** If the msg is already queued, ignore this one and leave the old. */ 563 private static final int SENDMSG_NOOP = 1; 564 /** If the msg is already queued, queue this one and leave the old. */ 565 private static final int SENDMSG_QUEUE = 2; 566 567 private static void sendMsg(Handler handler, int msg, int existingMsgPolicy, 568 int arg1, int arg2, Object obj, int delayMs) { 569 if (handler == null) { 570 Log.e(TAG, "null event handler, will not deliver message " + msg); 571 return; 572 } 573 if (existingMsgPolicy == SENDMSG_REPLACE) { 574 handler.removeMessages(msg); 575 } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) { 576 return; 577 } 578 handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delayMs); 579 } 580 581 private void onClientChange(boolean clearing) { 582 final OnClientUpdateListener l; 583 synchronized(mInfoLock) { 584 l = mOnClientUpdateListener; 585 mMetadataEditor = null; 586 } 587 if (l != null) { 588 l.onClientChange(clearing); 589 } 590 } 591 592 private void updateController(MediaController controller) { 593 if (DEBUG) { 594 Log.d(TAG, "Updating controller to " + controller + " previous controller is " 595 + mCurrentSession); 596 } 597 synchronized (mInfoLock) { 598 if (controller == null) { 599 if (mCurrentSession != null) { 600 mCurrentSession.unregisterCallback(mSessionCb); 601 mCurrentSession = null; 602 sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE, 603 0 /* arg1 ignored */, 1 /* clearing */, null /* obj */, 0 /* delay */); 604 } 605 } else if (mCurrentSession == null 606 || !controller.getSessionToken() 607 .equals(mCurrentSession.getSessionToken())) { 608 if (mCurrentSession != null) { 609 mCurrentSession.unregisterCallback(mSessionCb); 610 } 611 sendMsg(mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE, 612 0 /* arg1 ignored */, 0 /* clearing */, null /* obj */, 0 /* delay */); 613 mCurrentSession = controller; 614 mCurrentSession.registerCallback(mSessionCb, mEventHandler); 615 616 PlaybackState state = controller.getPlaybackState(); 617 sendMsg(mEventHandler, MSG_NEW_PLAYBACK_STATE, SENDMSG_REPLACE, 618 0 /* arg1 ignored */, 0 /* arg2 ignored */, state /* obj */, 0 /* delay */); 619 620 MediaMetadata metadata = controller.getMetadata(); 621 sendMsg(mEventHandler, MSG_NEW_MEDIA_METADATA, SENDMSG_REPLACE, 622 0 /* arg1 ignored */, 0 /* arg2 ignored*/, metadata /* obj */, 0 /*delay*/); 623 } 624 // else same controller, no need to update 625 } 626 } 627 628 private void onNewPlaybackState(PlaybackState state) { 629 final OnClientUpdateListener l; 630 synchronized (mInfoLock) { 631 l = this.mOnClientUpdateListener; 632 } 633 if (l != null) { 634 int playstate = state == null ? RemoteControlClient.PLAYSTATE_NONE : PlaybackState 635 .getRccStateFromState(state.getState()); 636 if (state == null || state.getPosition() == PlaybackState.PLAYBACK_POSITION_UNKNOWN) { 637 l.onClientPlaybackStateUpdate(playstate); 638 } else { 639 l.onClientPlaybackStateUpdate(playstate, state.getLastPositionUpdateTime(), 640 state.getPosition(), state.getPlaybackSpeed()); 641 } 642 if (state != null) { 643 l.onClientTransportControlUpdate( 644 PlaybackState.getRccControlFlagsFromActions(state.getActions())); 645 } 646 } 647 } 648 649 private void onNewMediaMetadata(MediaMetadata metadata) { 650 if (metadata == null) { 651 // RemoteController only handles non-null metadata 652 return; 653 } 654 final OnClientUpdateListener l; 655 final MetadataEditor metadataEditor; 656 // prepare the received Bundle to be used inside a MetadataEditor 657 synchronized(mInfoLock) { 658 l = mOnClientUpdateListener; 659 boolean canRate = mCurrentSession != null 660 && mCurrentSession.getRatingType() != Rating.RATING_NONE; 661 long editableKeys = canRate ? MediaMetadataEditor.RATING_KEY_BY_USER : 0; 662 Bundle legacyMetadata = MediaSessionLegacyHelper.getOldMetadata(metadata, 663 mArtworkWidth, mArtworkHeight); 664 mMetadataEditor = new MetadataEditor(legacyMetadata, editableKeys); 665 metadataEditor = mMetadataEditor; 666 } 667 if (l != null) { 668 l.onClientMetadataUpdate(metadataEditor); 669 } 670 } 671 672 //================================================== 673 private static class PlaybackInfo { 674 int mState; 675 long mStateChangeTimeMs; 676 long mCurrentPosMs; 677 float mSpeed; 678 679 PlaybackInfo(int state, long stateChangeTimeMs, long currentPosMs, float speed) { 680 mState = state; 681 mStateChangeTimeMs = stateChangeTimeMs; 682 mCurrentPosMs = currentPosMs; 683 mSpeed = speed; 684 } 685 } 686 687 /** 688 * @hide 689 * Used by AudioManager to access user listener receiving the client update notifications 690 * @return 691 */ 692 OnClientUpdateListener getUpdateListener() { 693 return mOnClientUpdateListener; 694 } 695} 696