1 /* 2 * Copyright 2018 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 com.android.bluetooth.avrcp; 18 19 import android.annotation.Nullable; 20 import android.media.MediaMetadata; 21 import android.media.session.MediaSession; 22 import android.media.session.PlaybackState; 23 import android.os.Handler; 24 import android.os.Looper; 25 import android.os.Message; 26 import android.util.Log; 27 28 import com.android.internal.annotations.GuardedBy; 29 import com.android.internal.annotations.VisibleForTesting; 30 31 import java.util.List; 32 import java.util.Objects; 33 34 /* 35 * A class to synchronize Media Controller Callbacks and only pass through 36 * an update once all the relevant information is current. 37 * 38 * TODO (apanicke): Once MediaPlayer2 is supported better, replace this class 39 * with that. 40 */ 41 class MediaPlayerWrapper { 42 private static final String TAG = "AvrcpMediaPlayerWrapper"; 43 private static final boolean DEBUG = false; 44 static boolean sTesting = false; 45 46 private MediaController mMediaController; 47 private String mPackageName; 48 private Looper mLooper; 49 50 private MediaData mCurrentData; 51 52 @GuardedBy("mCallbackLock") 53 private MediaControllerListener mControllerCallbacks = null; 54 private final Object mCallbackLock = new Object(); 55 private Callback mRegisteredCallback = null; 56 MediaPlayerWrapper()57 protected MediaPlayerWrapper() { 58 mCurrentData = new MediaData(null, null, null); 59 } 60 61 public interface Callback { mediaUpdatedCallback(MediaData data)62 void mediaUpdatedCallback(MediaData data); 63 } 64 isPlaybackStateReady()65 boolean isPlaybackStateReady() { 66 if (getPlaybackState() == null) { 67 d("isPlaybackStateReady(): PlaybackState is null"); 68 return false; 69 } 70 71 return true; 72 } 73 isMetadataReady()74 boolean isMetadataReady() { 75 if (getMetadata() == null) { 76 d("isMetadataReady(): Metadata is null"); 77 return false; 78 } 79 80 return true; 81 } 82 83 // TODO (apanicke): Implement a factory to make testing and creating interop wrappers easier wrap(MediaController controller, Looper looper)84 static MediaPlayerWrapper wrap(MediaController controller, Looper looper) { 85 if (controller == null || looper == null) { 86 e("MediaPlayerWrapper.wrap(): Null parameter - Controller: " + controller 87 + " | Looper: " + looper); 88 return null; 89 } 90 91 MediaPlayerWrapper newWrapper; 92 if (controller.getPackageName().equals("com.google.android.music")) { 93 Log.v(TAG, "Creating compatibility wrapper for Google Play Music"); 94 newWrapper = new GPMWrapper(); 95 } else { 96 newWrapper = new MediaPlayerWrapper(); 97 } 98 99 newWrapper.mMediaController = controller; 100 newWrapper.mPackageName = controller.getPackageName(); 101 newWrapper.mLooper = looper; 102 103 newWrapper.mCurrentData.queue = Util.toMetadataList(newWrapper.getQueue()); 104 newWrapper.mCurrentData.metadata = Util.toMetadata(newWrapper.getMetadata()); 105 newWrapper.mCurrentData.state = newWrapper.getPlaybackState(); 106 return newWrapper; 107 } 108 cleanup()109 void cleanup() { 110 unregisterCallback(); 111 112 mMediaController = null; 113 mLooper = null; 114 } 115 getPackageName()116 String getPackageName() { 117 return mPackageName; 118 } 119 getQueue()120 protected List<MediaSession.QueueItem> getQueue() { 121 return mMediaController.getQueue(); 122 } 123 getMetadata()124 protected MediaMetadata getMetadata() { 125 return mMediaController.getMetadata(); 126 } 127 getCurrentMetadata()128 Metadata getCurrentMetadata() { 129 return Util.toMetadata(getMetadata()); 130 } 131 getPlaybackState()132 PlaybackState getPlaybackState() { 133 return mMediaController.getPlaybackState(); 134 } 135 getActiveQueueID()136 long getActiveQueueID() { 137 if (mMediaController.getPlaybackState() == null) return -1; 138 return mMediaController.getPlaybackState().getActiveQueueItemId(); 139 } 140 getCurrentQueue()141 List<Metadata> getCurrentQueue() { 142 return mCurrentData.queue; 143 } 144 145 // We don't return the cached info here in order to always provide the freshest data. getCurrentMediaData()146 MediaData getCurrentMediaData() { 147 MediaData data = new MediaData( 148 getCurrentMetadata(), 149 getPlaybackState(), 150 getCurrentQueue()); 151 return data; 152 } 153 playItemFromQueue(long qid)154 void playItemFromQueue(long qid) { 155 // Return immediately if no queue exists. 156 if (getQueue() == null) { 157 Log.w(TAG, "playItemFromQueue: Trying to play item for player that has no queue: " 158 + mPackageName); 159 return; 160 } 161 162 MediaController.TransportControls controller = mMediaController.getTransportControls(); 163 controller.skipToQueueItem(qid); 164 } 165 166 // TODO (apanicke): Implement shuffle and repeat support. Right now these use custom actions 167 // and it may only be possible to do this with Google Play Music isShuffleSupported()168 boolean isShuffleSupported() { 169 return false; 170 } 171 isRepeatSupported()172 boolean isRepeatSupported() { 173 return false; 174 } 175 toggleShuffle(boolean on)176 void toggleShuffle(boolean on) { 177 return; 178 } 179 toggleRepeat(boolean on)180 void toggleRepeat(boolean on) { 181 return; 182 } 183 184 /** 185 * Return whether the queue, metadata, and queueID are all in sync. 186 */ isMetadataSynced()187 boolean isMetadataSynced() { 188 if (getQueue() != null && getActiveQueueID() != -1) { 189 // Check if currentPlayingQueueId is in the current Queue 190 MediaSession.QueueItem currItem = null; 191 192 for (MediaSession.QueueItem item : getQueue()) { 193 if (item.getQueueId() 194 == getActiveQueueID()) { // The item exists in the current queue 195 currItem = item; 196 break; 197 } 198 } 199 200 // Check if current playing song in Queue matches current Metadata 201 Metadata qitem = Util.toMetadata(currItem); 202 Metadata mdata = Util.toMetadata(getMetadata()); 203 if (currItem == null || !qitem.equals(mdata)) { 204 if (DEBUG) { 205 Log.d(TAG, "Metadata currently out of sync for " + mPackageName); 206 Log.d(TAG, " └ Current queueItem: " + qitem); 207 Log.d(TAG, " └ Current metadata : " + mdata); 208 } 209 return false; 210 } 211 } 212 213 return true; 214 } 215 216 /** 217 * Register a callback which gets called when media updates happen. The callbacks are 218 * called on the same Looper that was passed in to create this object. 219 */ registerCallback(Callback callback)220 void registerCallback(Callback callback) { 221 if (callback == null) { 222 e("Cannot register null callbacks for " + mPackageName); 223 return; 224 } 225 226 synchronized (mCallbackLock) { 227 mRegisteredCallback = callback; 228 } 229 230 // Update the current data since it could have changed while we weren't registered for 231 // updates 232 mCurrentData = new MediaData( 233 Util.toMetadata(getMetadata()), 234 getPlaybackState(), 235 Util.toMetadataList(getQueue())); 236 237 mControllerCallbacks = new MediaControllerListener(mMediaController, mLooper); 238 } 239 240 /** 241 * Unregisters from updates. Note, this doesn't require the looper to be shut down. 242 */ unregisterCallback()243 void unregisterCallback() { 244 // Prevent a race condition where a callback could be called while shutting down 245 synchronized (mCallbackLock) { 246 mRegisteredCallback = null; 247 } 248 249 if (mControllerCallbacks == null) return; 250 mControllerCallbacks.cleanup(); 251 mControllerCallbacks = null; 252 } 253 updateMediaController(MediaController newController)254 void updateMediaController(MediaController newController) { 255 if (newController == mMediaController) return; 256 257 mMediaController = newController; 258 259 synchronized (mCallbackLock) { 260 if (mRegisteredCallback == null || mControllerCallbacks == null) { 261 d("Controller for " + mPackageName + " maybe is not activated."); 262 return; 263 } 264 } 265 266 mControllerCallbacks.cleanup(); 267 268 // Update the current data since it could be different on the new controller for the player 269 mCurrentData = new MediaData( 270 Util.toMetadata(getMetadata()), 271 getPlaybackState(), 272 Util.toMetadataList(getQueue())); 273 274 mControllerCallbacks = new MediaControllerListener(mMediaController, mLooper); 275 d("Controller for " + mPackageName + " was updated."); 276 } 277 sendMediaUpdate()278 private void sendMediaUpdate() { 279 MediaData newData = new MediaData( 280 Util.toMetadata(getMetadata()), 281 getPlaybackState(), 282 Util.toMetadataList(getQueue())); 283 284 if (newData.equals(mCurrentData)) { 285 // This may happen if the controller is fully synced by the time the 286 // first update is completed 287 Log.v(TAG, "Trying to update with last sent metadata"); 288 return; 289 } 290 291 synchronized (mCallbackLock) { 292 if (mRegisteredCallback == null) { 293 Log.e(TAG, mPackageName 294 + ": Trying to send an update with no registered callback"); 295 return; 296 } 297 298 Log.v(TAG, "trySendMediaUpdate(): Metadata has been updated for " + mPackageName); 299 mRegisteredCallback.mediaUpdatedCallback(newData); 300 } 301 302 mCurrentData = newData; 303 } 304 305 class TimeoutHandler extends Handler { 306 private static final int MSG_TIMEOUT = 0; 307 private static final long CALLBACK_TIMEOUT_MS = 2000; 308 TimeoutHandler(Looper looper)309 TimeoutHandler(Looper looper) { 310 super(looper); 311 } 312 313 @Override handleMessage(Message msg)314 public void handleMessage(Message msg) { 315 if (msg.what != MSG_TIMEOUT) { 316 Log.wtf(TAG, "Unknown message on timeout handler: " + msg.what); 317 return; 318 } 319 320 Log.e(TAG, "Timeout while waiting for metadata to sync for " + mPackageName); 321 Log.e(TAG, " └ Current Metadata: " + Util.toMetadata(getMetadata())); 322 Log.e(TAG, " └ Current Playstate: " + getPlaybackState()); 323 List<Metadata> current_queue = Util.toMetadataList(getQueue()); 324 for (int i = 0; i < current_queue.size(); i++) { 325 Log.e(TAG, " └ QueueItem(" + i + "): " + current_queue.get(i)); 326 } 327 328 sendMediaUpdate(); 329 330 // TODO(apanicke): Add metric collection here. 331 332 if (sTesting) Log.wtfStack(TAG, "Crashing the stack"); 333 } 334 } 335 336 class MediaControllerListener extends MediaController.Callback { 337 private final Object mTimeoutHandlerLock = new Object(); 338 private Handler mTimeoutHandler; 339 private MediaController mController; 340 MediaControllerListener(MediaController controller, Looper newLooper)341 MediaControllerListener(MediaController controller, Looper newLooper) { 342 synchronized (mTimeoutHandlerLock) { 343 mTimeoutHandler = new TimeoutHandler(newLooper); 344 345 mController = controller; 346 // Register the callbacks to execute on the same thread as the timeout thread. This 347 // prevents a race condition where a timeout happens at the same time as an update. 348 mController.registerCallback(this, mTimeoutHandler); 349 } 350 } 351 cleanup()352 void cleanup() { 353 synchronized (mTimeoutHandlerLock) { 354 mController.unregisterCallback(this); 355 mController = null; 356 mTimeoutHandler.removeMessages(TimeoutHandler.MSG_TIMEOUT); 357 mTimeoutHandler = null; 358 } 359 } 360 trySendMediaUpdate()361 void trySendMediaUpdate() { 362 synchronized (mTimeoutHandlerLock) { 363 if (mTimeoutHandler == null) return; 364 mTimeoutHandler.removeMessages(TimeoutHandler.MSG_TIMEOUT); 365 366 if (!isMetadataSynced()) { 367 d("trySendMediaUpdate(): Starting media update timeout"); 368 mTimeoutHandler.sendEmptyMessageDelayed(TimeoutHandler.MSG_TIMEOUT, 369 TimeoutHandler.CALLBACK_TIMEOUT_MS); 370 return; 371 } 372 } 373 374 sendMediaUpdate(); 375 } 376 377 @Override onMetadataChanged(@ullable MediaMetadata metadata)378 public void onMetadataChanged(@Nullable MediaMetadata metadata) { 379 if (!isMetadataReady()) { 380 Log.v(TAG, "onMetadataChanged(): " + mPackageName 381 + " tried to update with no queue"); 382 return; 383 } 384 385 if (DEBUG) { 386 Log.v(TAG, "onMetadataChanged(): " + mPackageName + " : " 387 + Util.toMetadata(metadata)); 388 } 389 390 if (!Objects.equals(metadata, getMetadata())) { 391 e("The callback metadata doesn't match controller metadata"); 392 } 393 394 // TODO: Certain players update different metadata fields as they load, such as Album 395 // Art. For track changed updates we only care about the song information like title 396 // and album and duration. In the future we can use this to know when Album art is 397 // loaded. 398 399 // TODO: Spotify needs a metadata update debouncer as it sometimes updates the metadata 400 // twice in a row with the only difference being that the song duration is rounded to 401 // the nearest second. 402 if (Objects.equals(metadata, mCurrentData.metadata)) { 403 Log.w(TAG, "onMetadataChanged(): " + mPackageName 404 + " tried to update with no new data"); 405 return; 406 } 407 408 trySendMediaUpdate(); 409 } 410 411 @Override onPlaybackStateChanged(@ullable PlaybackState state)412 public void onPlaybackStateChanged(@Nullable PlaybackState state) { 413 if (!isPlaybackStateReady()) { 414 Log.v(TAG, "onPlaybackStateChanged(): " + mPackageName 415 + " tried to update with no queue"); 416 return; 417 } 418 419 Log.v(TAG, "onPlaybackStateChanged(): " + mPackageName + " : " + state.toString()); 420 421 if (!playstateEquals(state, getPlaybackState())) { 422 e("The callback playback state doesn't match the current state"); 423 } 424 425 if (playstateEquals(state, mCurrentData.state)) { 426 Log.w(TAG, "onPlaybackStateChanged(): " + mPackageName 427 + " tried to update with no new data"); 428 return; 429 } 430 431 // If there is no playstate, ignore the update. 432 if (state.getState() == PlaybackState.STATE_NONE) { 433 Log.v(TAG, "Waiting to send update as controller has no playback state"); 434 return; 435 } 436 437 trySendMediaUpdate(); 438 } 439 440 @Override onQueueChanged(@ullable List<MediaSession.QueueItem> queue)441 public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) { 442 if (!isPlaybackStateReady() || !isMetadataReady()) { 443 Log.v(TAG, "onQueueChanged(): " + mPackageName 444 + " tried to update with no queue"); 445 return; 446 } 447 448 Log.v(TAG, "onQueueChanged(): " + mPackageName); 449 450 if (!Objects.equals(queue, getQueue())) { 451 e("The callback queue isn't the current queue"); 452 } 453 454 List<Metadata> current_queue = Util.toMetadataList(queue); 455 if (current_queue.equals(mCurrentData.queue)) { 456 Log.w(TAG, "onQueueChanged(): " + mPackageName 457 + " tried to update with no new data"); 458 return; 459 } 460 461 if (DEBUG) { 462 for (int i = 0; i < current_queue.size(); i++) { 463 Log.d(TAG, " └ QueueItem(" + i + "): " + current_queue.get(i)); 464 } 465 } 466 467 trySendMediaUpdate(); 468 } 469 470 @Override onSessionDestroyed()471 public void onSessionDestroyed() { 472 Log.w(TAG, "The session was destroyed " + mPackageName); 473 } 474 475 @VisibleForTesting getTimeoutHandler()476 Handler getTimeoutHandler() { 477 return mTimeoutHandler; 478 } 479 } 480 481 /** 482 * Checks wheter the core information of two PlaybackStates match. This function allows a 483 * certain amount of deviation between the position fields of the PlaybackStates. This is to 484 * prevent matches from failing when updates happen in quick succession. 485 * 486 * The maximum allowed deviation is defined by PLAYSTATE_BOUNCE_IGNORE_PERIOD and is measured 487 * in milliseconds. 488 */ 489 private static final long PLAYSTATE_BOUNCE_IGNORE_PERIOD = 500; playstateEquals(PlaybackState a, PlaybackState b)490 static boolean playstateEquals(PlaybackState a, PlaybackState b) { 491 if (a == b) return true; 492 493 if (a != null && b != null 494 && a.getState() == b.getState() 495 && a.getActiveQueueItemId() == b.getActiveQueueItemId() 496 && Math.abs(a.getPosition() - b.getPosition()) < PLAYSTATE_BOUNCE_IGNORE_PERIOD) { 497 return true; 498 } 499 500 return false; 501 } 502 e(String message)503 private static void e(String message) { 504 if (sTesting) { 505 Log.wtfStack(TAG, message); 506 } else { 507 Log.e(TAG, message); 508 } 509 } 510 d(String message)511 private void d(String message) { 512 if (DEBUG) Log.d(TAG, mPackageName + ": " + message); 513 } 514 515 @VisibleForTesting getTimeoutHandler()516 Handler getTimeoutHandler() { 517 if (mControllerCallbacks == null) return null; 518 return mControllerCallbacks.getTimeoutHandler(); 519 } 520 521 @Override toString()522 public String toString() { 523 StringBuilder sb = new StringBuilder(); 524 sb.append(mMediaController.toString() + "\n"); 525 sb.append("Current Data:\n"); 526 sb.append(" Song: " + mCurrentData.metadata + "\n"); 527 sb.append(" PlayState: " + mCurrentData.state + "\n"); 528 sb.append(" Queue: size=" + mCurrentData.queue.size() + "\n"); 529 for (Metadata data : mCurrentData.queue) { 530 sb.append(" " + data + "\n"); 531 } 532 return sb.toString(); 533 } 534 } 535