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.Manifest; 20 import android.app.ActivityManager; 21 import android.app.PendingIntent; 22 import android.app.PendingIntent.CanceledException; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.graphics.Bitmap; 26 import android.media.IRemoteControlDisplay; 27 import android.media.MediaMetadataEditor; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.os.Looper; 31 import android.os.Message; 32 import android.os.RemoteException; 33 import android.os.ServiceManager; 34 import android.os.SystemClock; 35 import android.util.DisplayMetrics; 36 import android.util.Log; 37 import android.view.KeyEvent; 38 39 import java.lang.ref.WeakReference; 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 public final class RemoteController 57 { 58 private final static int MAX_BITMAP_DIMENSION = 512; 59 private final static int TRANSPORT_UNKNOWN = 0; 60 private final static String TAG = "RemoteController"; 61 private final static boolean DEBUG = false; 62 private final static Object mGenLock = new Object(); 63 private final static Object mInfoLock = new Object(); 64 private final RcDisplay mRcd; 65 private final Context mContext; 66 private final AudioManager mAudioManager; 67 private final int mMaxBitmapDimension; 68 private MetadataEditor mMetadataEditor; 69 70 /** 71 * Synchronized on mGenLock 72 */ 73 private int mClientGenerationIdCurrent = 0; 74 75 /** 76 * Synchronized on mInfoLock 77 */ 78 private boolean mIsRegistered = false; 79 private PendingIntent mClientPendingIntentCurrent; 80 private OnClientUpdateListener mOnClientUpdateListener; 81 private PlaybackInfo mLastPlaybackInfo; 82 private int mArtworkWidth = -1; 83 private int mArtworkHeight = -1; 84 private boolean mEnabled = true; 85 86 /** 87 * Class constructor. 88 * @param context the {@link Context}, must be non-null. 89 * @param updateListener the listener to be called whenever new client information is available, 90 * must be non-null. 91 * @throws IllegalArgumentException 92 */ 93 public RemoteController(Context context, OnClientUpdateListener updateListener) 94 throws IllegalArgumentException { 95 this(context, updateListener, null); 96 } 97 98 /** 99 * Class constructor. 100 * @param context the {@link Context}, must be non-null. 101 * @param updateListener the listener to be called whenever new client information is available, 102 * must be non-null. 103 * @param looper the {@link Looper} on which to run the event loop, 104 * or null to use the current thread's looper. 105 * @throws java.lang.IllegalArgumentException 106 */ 107 public RemoteController(Context context, OnClientUpdateListener updateListener, Looper looper) 108 throws IllegalArgumentException { 109 if (context == null) { 110 throw new IllegalArgumentException("Invalid null Context"); 111 } 112 if (updateListener == null) { 113 throw new IllegalArgumentException("Invalid null OnClientUpdateListener"); 114 } 115 if (looper != null) { 116 mEventHandler = new EventHandler(this, looper); 117 } else { 118 Looper l = Looper.myLooper(); 119 if (l != null) { 120 mEventHandler = new EventHandler(this, l); 121 } else { 122 throw new IllegalArgumentException("Calling thread not associated with a looper"); 123 } 124 } 125 mOnClientUpdateListener = updateListener; 126 mContext = context; 127 mRcd = new RcDisplay(this); 128 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 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 */ 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 */ 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 */ 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 */ 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 */ 192 public void onClientMetadataUpdate(MetadataEditor metadataEditor); 193 }; 194 195 196 /** 197 * @hide 198 */ 199 public String getRemoteControlClientPackageName() { 200 return mClientPendingIntentCurrent != null ? 201 mClientPendingIntentCurrent.getCreatorPackage() : null; 202 } 203 204 /** 205 * Return the estimated playback position of the current media track or a negative value 206 * if not available. 207 * 208 * <p>The value returned is estimated by the current process and may not be perfect. 209 * The time returned by this method is calculated from the last state change time based 210 * on the current play position at that time and the last known playback speed. 211 * An application may call {@link #setSynchronizationMode(int)} to apply 212 * a synchronization policy that will periodically re-sync the estimated position 213 * with the RemoteControlClient.</p> 214 * 215 * @return the current estimated playback position in milliseconds or a negative value 216 * if not available 217 * 218 * @see OnClientUpdateListener#onClientPlaybackStateUpdate(int, long, long, float) 219 */ 220 public long getEstimatedMediaPosition() { 221 if (mLastPlaybackInfo != null) { 222 if (!RemoteControlClient.playbackPositionShouldMove(mLastPlaybackInfo.mState)) { 223 return mLastPlaybackInfo.mCurrentPosMs; 224 } 225 226 // Take the current position at the time of state change and estimate. 227 final long thenPos = mLastPlaybackInfo.mCurrentPosMs; 228 if (thenPos < 0) { 229 return -1; 230 } 231 232 final long now = SystemClock.elapsedRealtime(); 233 final long then = mLastPlaybackInfo.mStateChangeTimeMs; 234 final long sinceThen = now - then; 235 final long scaledSinceThen = (long) (sinceThen * mLastPlaybackInfo.mSpeed); 236 return thenPos + scaledSinceThen; 237 } 238 return -1; 239 } 240 241 242 /** 243 * Send a simulated key event for a media button to be received by the current client. 244 * To simulate a key press, you must first send a KeyEvent built with 245 * a {@link KeyEvent#ACTION_DOWN} action, then another event with the {@link KeyEvent#ACTION_UP} 246 * action. 247 * <p>The key event will be sent to the registered receiver 248 * (see {@link AudioManager#registerMediaButtonEventReceiver(PendingIntent)}) whose associated 249 * {@link RemoteControlClient}'s metadata and playback state is published (there may be 250 * none under some circumstances). 251 * @param keyEvent a {@link KeyEvent} instance whose key code is one of 252 * {@link KeyEvent#KEYCODE_MUTE}, 253 * {@link KeyEvent#KEYCODE_HEADSETHOOK}, 254 * {@link KeyEvent#KEYCODE_MEDIA_PLAY}, 255 * {@link KeyEvent#KEYCODE_MEDIA_PAUSE}, 256 * {@link KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE}, 257 * {@link KeyEvent#KEYCODE_MEDIA_STOP}, 258 * {@link KeyEvent#KEYCODE_MEDIA_NEXT}, 259 * {@link KeyEvent#KEYCODE_MEDIA_PREVIOUS}, 260 * {@link KeyEvent#KEYCODE_MEDIA_REWIND}, 261 * {@link KeyEvent#KEYCODE_MEDIA_RECORD}, 262 * {@link KeyEvent#KEYCODE_MEDIA_FAST_FORWARD}, 263 * {@link KeyEvent#KEYCODE_MEDIA_CLOSE}, 264 * {@link KeyEvent#KEYCODE_MEDIA_EJECT}, 265 * or {@link KeyEvent#KEYCODE_MEDIA_AUDIO_TRACK}. 266 * @return true if the event was successfully sent, false otherwise. 267 * @throws IllegalArgumentException 268 */ 269 public boolean sendMediaKeyEvent(KeyEvent keyEvent) throws IllegalArgumentException { 270 if (!MediaFocusControl.isMediaKeyCode(keyEvent.getKeyCode())) { 271 throw new IllegalArgumentException("not a media key event"); 272 } 273 final PendingIntent pi; 274 synchronized(mInfoLock) { 275 if (!mIsRegistered) { 276 Log.e(TAG, "Cannot use sendMediaKeyEvent() from an unregistered RemoteController"); 277 return false; 278 } 279 if (!mEnabled) { 280 Log.e(TAG, "Cannot use sendMediaKeyEvent() from a disabled RemoteController"); 281 return false; 282 } 283 pi = mClientPendingIntentCurrent; 284 } 285 if (pi != null) { 286 Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); 287 intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); 288 try { 289 pi.send(mContext, 0, intent); 290 } catch (CanceledException e) { 291 Log.e(TAG, "Error sending intent for media button down: ", e); 292 return false; 293 } 294 } else { 295 Log.i(TAG, "No-op when sending key click, no receiver right now"); 296 return false; 297 } 298 return true; 299 } 300 301 302 /** 303 * Sets the new playback position. 304 * This method can only be called on a registered RemoteController. 305 * @param timeMs a 0 or positive value for the new playback position, expressed in ms. 306 * @return true if the command to set the playback position was successfully sent. 307 * @throws IllegalArgumentException 308 */ 309 public boolean seekTo(long timeMs) throws IllegalArgumentException { 310 if (!mEnabled) { 311 Log.e(TAG, "Cannot use seekTo() from a disabled RemoteController"); 312 return false; 313 } 314 if (timeMs < 0) { 315 throw new IllegalArgumentException("illegal negative time value"); 316 } 317 final int genId; 318 synchronized (mGenLock) { 319 genId = mClientGenerationIdCurrent; 320 } 321 mAudioManager.setRemoteControlClientPlaybackPosition(genId, timeMs); 322 return true; 323 } 324 325 326 /** 327 * @hide 328 * @param wantBitmap 329 * @param width 330 * @param height 331 * @return true if successful 332 * @throws IllegalArgumentException 333 */ 334 public boolean setArtworkConfiguration(boolean wantBitmap, int width, int height) 335 throws IllegalArgumentException { 336 synchronized (mInfoLock) { 337 if (wantBitmap) { 338 if ((width > 0) && (height > 0)) { 339 if (width > mMaxBitmapDimension) { width = mMaxBitmapDimension; } 340 if (height > mMaxBitmapDimension) { height = mMaxBitmapDimension; } 341 mArtworkWidth = width; 342 mArtworkHeight = height; 343 } else { 344 throw new IllegalArgumentException("Invalid dimensions"); 345 } 346 } else { 347 mArtworkWidth = -1; 348 mArtworkHeight = -1; 349 } 350 if (mIsRegistered) { 351 mAudioManager.remoteControlDisplayUsesBitmapSize(mRcd, 352 mArtworkWidth, mArtworkHeight); 353 } // else new values have been stored, and will be read by AudioManager with 354 // RemoteController.getArtworkSize() when AudioManager.registerRemoteController() 355 // is called. 356 } 357 return true; 358 } 359 360 /** 361 * Set the maximum artwork image dimensions to be received in the metadata. 362 * No bitmaps will be received unless this has been specified. 363 * @param width the maximum width in pixels 364 * @param height the maximum height in pixels 365 * @return true if the artwork dimension was successfully set. 366 * @throws IllegalArgumentException 367 */ 368 public boolean setArtworkConfiguration(int width, int height) throws IllegalArgumentException { 369 return setArtworkConfiguration(true, width, height); 370 } 371 372 /** 373 * Prevents this RemoteController from receiving artwork images. 374 * @return true if receiving artwork images was successfully disabled. 375 */ 376 public boolean clearArtworkConfiguration() { 377 return setArtworkConfiguration(false, -1, -1); 378 } 379 380 381 /** 382 * Default playback position synchronization mode where the RemoteControlClient is not 383 * asked regularly for its playback position to see if it has drifted from the estimated 384 * position. 385 */ 386 public static final int POSITION_SYNCHRONIZATION_NONE = 0; 387 388 /** 389 * The playback position synchronization mode where the RemoteControlClient instances which 390 * expose their playback position to the framework, will be regularly polled to check 391 * whether any drift has been noticed between their estimated position and the one they report. 392 * Note that this mode should only ever be used when needing to display very accurate playback 393 * position, as regularly polling a RemoteControlClient for its position may have an impact 394 * on battery life (if applicable) when this query will trigger network transactions in the 395 * case of remote playback. 396 */ 397 public static final int POSITION_SYNCHRONIZATION_CHECK = 1; 398 399 /** 400 * Set the playback position synchronization mode. 401 * Must be called on a registered RemoteController. 402 * @param sync {@link #POSITION_SYNCHRONIZATION_NONE} or {@link #POSITION_SYNCHRONIZATION_CHECK} 403 * @return true if the synchronization mode was successfully set. 404 * @throws IllegalArgumentException 405 */ 406 public boolean setSynchronizationMode(int sync) throws IllegalArgumentException { 407 if ((sync != POSITION_SYNCHRONIZATION_NONE) && (sync != POSITION_SYNCHRONIZATION_CHECK)) { 408 throw new IllegalArgumentException("Unknown synchronization mode " + sync); 409 } 410 if (!mIsRegistered) { 411 Log.e(TAG, "Cannot set synchronization mode on an unregistered RemoteController"); 412 return false; 413 } 414 mAudioManager.remoteControlDisplayWantsPlaybackPositionSync(mRcd, 415 POSITION_SYNCHRONIZATION_CHECK == sync); 416 return true; 417 } 418 419 420 /** 421 * Creates a {@link MetadataEditor} for updating metadata values of the editable keys of 422 * the current {@link RemoteControlClient}. 423 * This method can only be called on a registered RemoteController. 424 * @return a new MetadataEditor instance. 425 */ 426 public MetadataEditor editMetadata() { 427 MetadataEditor editor = new MetadataEditor(); 428 editor.mEditorMetadata = new Bundle(); 429 editor.mEditorArtwork = null; 430 editor.mMetadataChanged = true; 431 editor.mArtworkChanged = true; 432 editor.mEditableKeys = 0; 433 return editor; 434 } 435 436 437 /** 438 * A class to read the metadata published by a {@link RemoteControlClient}, or send a 439 * {@link RemoteControlClient} new values for keys that can be edited. 440 */ 441 public class MetadataEditor extends MediaMetadataEditor { 442 /** 443 * @hide 444 */ 445 protected MetadataEditor() { } 446 447 /** 448 * @hide 449 */ 450 protected MetadataEditor(Bundle metadata, long editableKeys) { 451 mEditorMetadata = metadata; 452 mEditableKeys = editableKeys; 453 454 mEditorArtwork = (Bitmap) metadata.getParcelable( 455 String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK)); 456 if (mEditorArtwork != null) { 457 cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK); 458 } 459 460 mMetadataChanged = true; 461 mArtworkChanged = true; 462 mApplied = false; 463 } 464 465 private void cleanupBitmapFromBundle(int key) { 466 if (METADATA_KEYS_TYPE.get(key, METADATA_TYPE_INVALID) == METADATA_TYPE_BITMAP) { 467 mEditorMetadata.remove(String.valueOf(key)); 468 } 469 } 470 471 /** 472 * Applies all of the metadata changes that have been set since the MediaMetadataEditor 473 * instance was created with {@link RemoteController#editMetadata()} 474 * or since {@link #clear()} was called. 475 */ 476 public synchronized void apply() { 477 // "applying" a metadata bundle in RemoteController is only for sending edited 478 // key values back to the RemoteControlClient, so here we only care about the only 479 // editable key we support: RATING_KEY_BY_USER 480 if (!mMetadataChanged) { 481 return; 482 } 483 final int genId; 484 synchronized(mGenLock) { 485 genId = mClientGenerationIdCurrent; 486 } 487 synchronized(mInfoLock) { 488 if (mEditorMetadata.containsKey( 489 String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER))) { 490 Rating rating = (Rating) getObject( 491 MediaMetadataEditor.RATING_KEY_BY_USER, null); 492 mAudioManager.updateRemoteControlClientMetadata(genId, 493 MediaMetadataEditor.RATING_KEY_BY_USER, 494 rating); 495 } else { 496 Log.e(TAG, "no metadata to apply"); 497 } 498 // NOT setting mApplied to true as this type of MetadataEditor will be applied 499 // multiple times, whenever the user of a RemoteController needs to change the 500 // metadata (e.g. user changes the rating of a song more than once during playback) 501 mApplied = false; 502 } 503 } 504 505 } 506 507 508 //================================================== 509 // Implementation of IRemoteControlDisplay interface 510 private static class RcDisplay extends IRemoteControlDisplay.Stub { 511 private final WeakReference<RemoteController> mController; 512 513 RcDisplay(RemoteController rc) { 514 mController = new WeakReference<RemoteController>(rc); 515 } 516 517 public void setCurrentClientId(int genId, PendingIntent clientMediaIntent, 518 boolean clearing) { 519 final RemoteController rc = mController.get(); 520 if (rc == null) { 521 return; 522 } 523 boolean isNew = false; 524 synchronized(mGenLock) { 525 if (rc.mClientGenerationIdCurrent != genId) { 526 rc.mClientGenerationIdCurrent = genId; 527 isNew = true; 528 } 529 } 530 if (clientMediaIntent != null) { 531 sendMsg(rc.mEventHandler, MSG_NEW_PENDING_INTENT, SENDMSG_REPLACE, 532 genId /*arg1*/, 0, clientMediaIntent /*obj*/, 0 /*delay*/); 533 } 534 if (isNew || clearing) { 535 sendMsg(rc.mEventHandler, MSG_CLIENT_CHANGE, SENDMSG_REPLACE, 536 genId /*arg1*/, clearing ? 1 : 0, null /*obj*/, 0 /*delay*/); 537 } 538 } 539 540 public void setEnabled(boolean enabled) { 541 final RemoteController rc = mController.get(); 542 if (rc == null) { 543 return; 544 } 545 sendMsg(rc.mEventHandler, MSG_DISPLAY_ENABLE, SENDMSG_REPLACE, 546 enabled ? 1 : 0 /*arg1*/, 0, null /*obj*/, 0 /*delay*/); 547 } 548 549 public void setPlaybackState(int genId, int state, 550 long stateChangeTimeMs, long currentPosMs, float speed) { 551 final RemoteController rc = mController.get(); 552 if (rc == null) { 553 return; 554 } 555 if (DEBUG) { 556 Log.d(TAG, "> new playback state: genId="+genId 557 + " state="+ state 558 + " changeTime="+ stateChangeTimeMs 559 + " pos=" + currentPosMs 560 + "ms speed=" + speed); 561 } 562 563 synchronized(mGenLock) { 564 if (rc.mClientGenerationIdCurrent != genId) { 565 return; 566 } 567 } 568 final PlaybackInfo playbackInfo = 569 new PlaybackInfo(state, stateChangeTimeMs, currentPosMs, speed); 570 sendMsg(rc.mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE, 571 genId /*arg1*/, 0, playbackInfo /*obj*/, 0 /*delay*/); 572 573 } 574 575 public void setTransportControlInfo(int genId, int transportControlFlags, 576 int posCapabilities) { 577 final RemoteController rc = mController.get(); 578 if (rc == null) { 579 return; 580 } 581 synchronized(mGenLock) { 582 if (rc.mClientGenerationIdCurrent != genId) { 583 return; 584 } 585 } 586 sendMsg(rc.mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE, 587 genId /*arg1*/, transportControlFlags /*arg2*/, 588 null /*obj*/, 0 /*delay*/); 589 } 590 591 public void setMetadata(int genId, Bundle metadata) { 592 final RemoteController rc = mController.get(); 593 if (rc == null) { 594 return; 595 } 596 if (DEBUG) { Log.e(TAG, "setMetadata("+genId+")"); } 597 if (metadata == null) { 598 return; 599 } 600 synchronized(mGenLock) { 601 if (rc.mClientGenerationIdCurrent != genId) { 602 return; 603 } 604 } 605 sendMsg(rc.mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, 606 genId /*arg1*/, 0 /*arg2*/, 607 metadata /*obj*/, 0 /*delay*/); 608 } 609 610 public void setArtwork(int genId, Bitmap artwork) { 611 final RemoteController rc = mController.get(); 612 if (rc == null) { 613 return; 614 } 615 if (DEBUG) { Log.v(TAG, "setArtwork("+genId+")"); } 616 synchronized(mGenLock) { 617 if (rc.mClientGenerationIdCurrent != genId) { 618 return; 619 } 620 } 621 Bundle metadata = new Bundle(1); 622 metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK), artwork); 623 sendMsg(rc.mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, 624 genId /*arg1*/, 0 /*arg2*/, 625 metadata /*obj*/, 0 /*delay*/); 626 } 627 628 public void setAllMetadata(int genId, Bundle metadata, Bitmap artwork) { 629 final RemoteController rc = mController.get(); 630 if (rc == null) { 631 return; 632 } 633 if (DEBUG) { Log.e(TAG, "setAllMetadata("+genId+")"); } 634 if ((metadata == null) && (artwork == null)) { 635 return; 636 } 637 synchronized(mGenLock) { 638 if (rc.mClientGenerationIdCurrent != genId) { 639 return; 640 } 641 } 642 if (metadata == null) { 643 metadata = new Bundle(1); 644 } 645 if (artwork != null) { 646 metadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK), 647 artwork); 648 } 649 sendMsg(rc.mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, 650 genId /*arg1*/, 0 /*arg2*/, 651 metadata /*obj*/, 0 /*delay*/); 652 } 653 } 654 655 //================================================== 656 // Event handling 657 private final EventHandler mEventHandler; 658 private final static int MSG_NEW_PENDING_INTENT = 0; 659 private final static int MSG_NEW_PLAYBACK_INFO = 1; 660 private final static int MSG_NEW_TRANSPORT_INFO = 2; 661 private final static int MSG_NEW_METADATA = 3; // msg always has non-null obj parameter 662 private final static int MSG_CLIENT_CHANGE = 4; 663 private final static int MSG_DISPLAY_ENABLE = 5; 664 665 private class EventHandler extends Handler { 666 667 public EventHandler(RemoteController rc, Looper looper) { 668 super(looper); 669 } 670 671 @Override 672 public void handleMessage(Message msg) { 673 switch(msg.what) { 674 case MSG_NEW_PENDING_INTENT: 675 onNewPendingIntent(msg.arg1, (PendingIntent) msg.obj); 676 break; 677 case MSG_NEW_PLAYBACK_INFO: 678 onNewPlaybackInfo(msg.arg1, (PlaybackInfo) msg.obj); 679 break; 680 case MSG_NEW_TRANSPORT_INFO: 681 onNewTransportInfo(msg.arg1, msg.arg2); 682 break; 683 case MSG_NEW_METADATA: 684 onNewMetadata(msg.arg1, (Bundle)msg.obj); 685 break; 686 case MSG_CLIENT_CHANGE: 687 onClientChange(msg.arg1, msg.arg2 == 1); 688 break; 689 case MSG_DISPLAY_ENABLE: 690 onDisplayEnable(msg.arg1 == 1); 691 break; 692 default: 693 Log.e(TAG, "unknown event " + msg.what); 694 } 695 } 696 } 697 698 /** If the msg is already queued, replace it with this one. */ 699 private static final int SENDMSG_REPLACE = 0; 700 /** If the msg is already queued, ignore this one and leave the old. */ 701 private static final int SENDMSG_NOOP = 1; 702 /** If the msg is already queued, queue this one and leave the old. */ 703 private static final int SENDMSG_QUEUE = 2; 704 705 private static void sendMsg(Handler handler, int msg, int existingMsgPolicy, 706 int arg1, int arg2, Object obj, int delayMs) { 707 if (handler == null) { 708 Log.e(TAG, "null event handler, will not deliver message " + msg); 709 return; 710 } 711 if (existingMsgPolicy == SENDMSG_REPLACE) { 712 handler.removeMessages(msg); 713 } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) { 714 return; 715 } 716 handler.sendMessageDelayed(handler.obtainMessage(msg, arg1, arg2, obj), delayMs); 717 } 718 719 private void onNewPendingIntent(int genId, PendingIntent pi) { 720 synchronized(mGenLock) { 721 if (mClientGenerationIdCurrent != genId) { 722 return; 723 } 724 } 725 synchronized(mInfoLock) { 726 mClientPendingIntentCurrent = pi; 727 } 728 } 729 730 private void onNewPlaybackInfo(int genId, PlaybackInfo pi) { 731 synchronized(mGenLock) { 732 if (mClientGenerationIdCurrent != genId) { 733 return; 734 } 735 } 736 final OnClientUpdateListener l; 737 synchronized(mInfoLock) { 738 l = this.mOnClientUpdateListener; 739 mLastPlaybackInfo = pi; 740 } 741 if (l != null) { 742 if (pi.mCurrentPosMs == RemoteControlClient.PLAYBACK_POSITION_ALWAYS_UNKNOWN) { 743 l.onClientPlaybackStateUpdate(pi.mState); 744 } else { 745 l.onClientPlaybackStateUpdate(pi.mState, pi.mStateChangeTimeMs, pi.mCurrentPosMs, 746 pi.mSpeed); 747 } 748 } 749 } 750 751 private void onNewTransportInfo(int genId, int transportControlFlags) { 752 synchronized(mGenLock) { 753 if (mClientGenerationIdCurrent != genId) { 754 return; 755 } 756 } 757 final OnClientUpdateListener l; 758 synchronized(mInfoLock) { 759 l = mOnClientUpdateListener; 760 } 761 if (l != null) { 762 l.onClientTransportControlUpdate(transportControlFlags); 763 } 764 } 765 766 /** 767 * @param genId 768 * @param metadata guaranteed to be always non-null 769 */ 770 private void onNewMetadata(int genId, Bundle metadata) { 771 synchronized(mGenLock) { 772 if (mClientGenerationIdCurrent != genId) { 773 return; 774 } 775 } 776 final OnClientUpdateListener l; 777 final MetadataEditor metadataEditor; 778 // prepare the received Bundle to be used inside a MetadataEditor 779 final long editableKeys = metadata.getLong( 780 String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK), 0); 781 if (editableKeys != 0) { 782 metadata.remove(String.valueOf(MediaMetadataEditor.KEY_EDITABLE_MASK)); 783 } 784 synchronized(mInfoLock) { 785 l = mOnClientUpdateListener; 786 if ((mMetadataEditor != null) && (mMetadataEditor.mEditorMetadata != null)) { 787 if (mMetadataEditor.mEditorMetadata != metadata) { 788 // existing metadata, merge existing and new 789 mMetadataEditor.mEditorMetadata.putAll(metadata); 790 } 791 792 mMetadataEditor.putBitmap(MediaMetadataEditor.BITMAP_KEY_ARTWORK, 793 (Bitmap)metadata.getParcelable( 794 String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK))); 795 mMetadataEditor.cleanupBitmapFromBundle(MediaMetadataEditor.BITMAP_KEY_ARTWORK); 796 } else { 797 mMetadataEditor = new MetadataEditor(metadata, editableKeys); 798 } 799 metadataEditor = mMetadataEditor; 800 } 801 if (l != null) { 802 l.onClientMetadataUpdate(metadataEditor); 803 } 804 } 805 806 private void onClientChange(int genId, boolean clearing) { 807 synchronized(mGenLock) { 808 if (mClientGenerationIdCurrent != genId) { 809 return; 810 } 811 } 812 final OnClientUpdateListener l; 813 synchronized(mInfoLock) { 814 l = mOnClientUpdateListener; 815 mMetadataEditor = null; 816 } 817 if (l != null) { 818 l.onClientChange(clearing); 819 } 820 } 821 822 private void onDisplayEnable(boolean enabled) { 823 final OnClientUpdateListener l; 824 synchronized(mInfoLock) { 825 mEnabled = enabled; 826 l = this.mOnClientUpdateListener; 827 } 828 if (!enabled) { 829 // when disabling, reset all info sent to the user 830 final int genId; 831 synchronized (mGenLock) { 832 genId = mClientGenerationIdCurrent; 833 } 834 // send "stopped" state, happened "now", playback position is 0, speed 0.0f 835 final PlaybackInfo pi = new PlaybackInfo(RemoteControlClient.PLAYSTATE_STOPPED, 836 SystemClock.elapsedRealtime() /*stateChangeTimeMs*/, 837 0 /*currentPosMs*/, 0.0f /*speed*/); 838 sendMsg(mEventHandler, MSG_NEW_PLAYBACK_INFO, SENDMSG_REPLACE, 839 genId /*arg1*/, 0 /*arg2, ignored*/, pi /*obj*/, 0 /*delay*/); 840 // send "blank" transport control info: no controls are supported 841 sendMsg(mEventHandler, MSG_NEW_TRANSPORT_INFO, SENDMSG_REPLACE, 842 genId /*arg1*/, 0 /*arg2, no flags*/, 843 null /*obj, ignored*/, 0 /*delay*/); 844 // send dummy metadata with empty string for title and artist, duration of 0 845 Bundle metadata = new Bundle(3); 846 metadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_TITLE), ""); 847 metadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_ARTIST), ""); 848 metadata.putLong(String.valueOf(MediaMetadataRetriever.METADATA_KEY_DURATION), 0); 849 sendMsg(mEventHandler, MSG_NEW_METADATA, SENDMSG_QUEUE, 850 genId /*arg1*/, 0 /*arg2, ignored*/, metadata /*obj*/, 0 /*delay*/); 851 } 852 } 853 854 //================================================== 855 private static class PlaybackInfo { 856 int mState; 857 long mStateChangeTimeMs; 858 long mCurrentPosMs; 859 float mSpeed; 860 861 PlaybackInfo(int state, long stateChangeTimeMs, long currentPosMs, float speed) { 862 mState = state; 863 mStateChangeTimeMs = stateChangeTimeMs; 864 mCurrentPosMs = currentPosMs; 865 mSpeed = speed; 866 } 867 } 868 869 /** 870 * @hide 871 * Used by AudioManager to mark this instance as registered. 872 * @param registered 873 */ 874 void setIsRegistered(boolean registered) { 875 synchronized (mInfoLock) { 876 mIsRegistered = registered; 877 } 878 } 879 880 /** 881 * @hide 882 * Used by AudioManager to access binder to be registered/unregistered inside MediaFocusControl 883 * @return 884 */ 885 RcDisplay getRcDisplay() { 886 return mRcd; 887 } 888 889 /** 890 * @hide 891 * Used by AudioManager to read the current artwork dimension 892 * @return array containing width (index 0) and height (index 1) of currently set artwork size 893 */ 894 int[] getArtworkSize() { 895 synchronized (mInfoLock) { 896 int[] size = { mArtworkWidth, mArtworkHeight }; 897 return size; 898 } 899 } 900 901 /** 902 * @hide 903 * Used by AudioManager to access user listener receiving the client update notifications 904 * @return 905 */ 906 OnClientUpdateListener getUpdateListener() { 907 return mOnClientUpdateListener; 908 } 909 } 910