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