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.NonNull; 20 import android.content.BroadcastReceiver; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ResolveInfo; 27 import android.media.session.MediaSession; 28 import android.media.session.MediaSessionManager; 29 import android.media.session.PlaybackState; 30 import android.os.Handler; 31 import android.os.Looper; 32 import android.util.Log; 33 import android.view.KeyEvent; 34 35 import com.android.bluetooth.Utils; 36 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.HashMap; 40 import java.util.HashSet; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.regex.Matcher; 44 import java.util.regex.Pattern; 45 46 /** 47 * This class is directly responsible of maintaining the list of Browsable Players as well as 48 * the list of Addressable Players. This variation of the list doesn't actually list all the 49 * available players for a getAvailableMediaPlayers request. Instead it only reports one media 50 * player with ID=0 and all the other browsable players are folders in the root of that player. 51 * 52 * Changing the directory to a browsable player will allow you to traverse that player as normal. 53 * By only having one root player, we never have to send Addressed Player Changed notifications, 54 * UIDs Changed notifications, or Available Players Changed notifications. 55 * 56 * TODO (apanicke): Add non-browsable players as song items to the root folder. Selecting that 57 * player would effectively cause player switch by sending a play command to that player. 58 */ 59 public class MediaPlayerList { 60 private static final String TAG = "AvrcpMediaPlayerList"; 61 private static final boolean DEBUG = true; 62 static boolean sTesting = false; 63 64 private static final String PACKAGE_SCHEME = "package"; 65 private static final int NO_ACTIVE_PLAYER = 0; 66 private static final int BLUETOOTH_PLAYER_ID = 0; 67 private static final String BLUETOOTH_PLAYER_NAME = "Bluetooth Player"; 68 69 // mediaId's for the now playing list will be in the form of "NowPlayingId[XX]" where [XX] 70 // is the Queue ID for the requested item. 71 private static final String NOW_PLAYING_ID_PATTERN = Util.NOW_PLAYING_PREFIX + "([0-9]*)"; 72 73 // mediaId's for folder browsing will be in the form of [XX][mediaid], where [XX] is a 74 // two digit representation of the player id and [mediaid] is the original media id as a 75 // string. 76 private static final String BROWSE_ID_PATTERN = "\\d\\d.*"; 77 78 private Context mContext; 79 private Looper mLooper; // Thread all media player callbacks and timeouts happen on 80 private PackageManager mPackageManager; 81 private MediaSessionManager mMediaSessionManager; 82 83 private Map<Integer, MediaPlayerWrapper> mMediaPlayers = 84 Collections.synchronizedMap(new HashMap<Integer, MediaPlayerWrapper>()); 85 private Map<String, Integer> mMediaPlayerIds = 86 Collections.synchronizedMap(new HashMap<String, Integer>()); 87 private Map<Integer, BrowsedPlayerWrapper> mBrowsablePlayers = 88 Collections.synchronizedMap(new HashMap<Integer, BrowsedPlayerWrapper>()); 89 private int mActivePlayerId = NO_ACTIVE_PLAYER; 90 91 private AvrcpTargetService.ListCallback mCallback; 92 private BrowsablePlayerConnector mBrowsablePlayerConnector; 93 94 interface MediaUpdateCallback { run(MediaData data)95 void run(MediaData data); 96 } 97 98 interface GetPlayerRootCallback { run(int playerId, boolean success, String rootId, int numItems)99 void run(int playerId, boolean success, String rootId, int numItems); 100 } 101 102 interface GetFolderItemsCallback { run(String parentId, List<ListItem> items)103 void run(String parentId, List<ListItem> items); 104 } 105 106 interface FolderUpdateCallback { run(boolean availablePlayers, boolean addressedPlayers, boolean uids)107 void run(boolean availablePlayers, boolean addressedPlayers, boolean uids); 108 } 109 MediaPlayerList(Looper looper, Context context)110 MediaPlayerList(Looper looper, Context context) { 111 Log.v(TAG, "Creating MediaPlayerList"); 112 113 mLooper = looper; 114 mContext = context; 115 116 // Register for intents where available players might have changed 117 IntentFilter pkgFilter = new IntentFilter(); 118 pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 119 pkgFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED); 120 pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 121 pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); 122 pkgFilter.addDataScheme(PACKAGE_SCHEME); 123 context.registerReceiver(mPackageChangedBroadcastReceiver, pkgFilter); 124 125 mMediaSessionManager = 126 (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); 127 mMediaSessionManager.addOnActiveSessionsChangedListener( 128 mActiveSessionsChangedListener, null, new Handler(looper)); 129 mMediaSessionManager.setCallback(mButtonDispatchCallback, null); 130 } 131 init(AvrcpTargetService.ListCallback callback)132 void init(AvrcpTargetService.ListCallback callback) { 133 Log.v(TAG, "Initializing MediaPlayerList"); 134 mCallback = callback; 135 136 // Build the list of browsable players and afterwards, build the list of media players 137 Intent intent = new Intent(android.service.media.MediaBrowserService.SERVICE_INTERFACE); 138 List<ResolveInfo> playerList = 139 mContext 140 .getApplicationContext() 141 .getPackageManager() 142 .queryIntentServices(intent, PackageManager.MATCH_ALL); 143 144 mBrowsablePlayerConnector = BrowsablePlayerConnector.connectToPlayers(mContext, mLooper, 145 playerList, (List<BrowsedPlayerWrapper> players) -> { 146 Log.i(TAG, "init: Browsable Player list size is " + players.size()); 147 148 // Check to see if the list has been cleaned up before this completed 149 if (mMediaSessionManager == null) { 150 return; 151 } 152 153 for (BrowsedPlayerWrapper wrapper : players) { 154 // Generate new id and add the browsable player 155 if (!mMediaPlayerIds.containsKey(wrapper.getPackageName())) { 156 mMediaPlayerIds.put(wrapper.getPackageName(), getFreeMediaPlayerId()); 157 } 158 159 d("Adding Browser Wrapper for " + wrapper.getPackageName() + " with id " 160 + mMediaPlayerIds.get(wrapper.getPackageName())); 161 162 mBrowsablePlayers.put(mMediaPlayerIds.get(wrapper.getPackageName()), wrapper); 163 164 wrapper.getFolderItems(wrapper.getRootId(), 165 (int status, String mediaId, List<ListItem> results) -> { 166 d("Got the contents for: " + mediaId + " : num results=" 167 + results.size()); 168 }); 169 } 170 171 // Construct the list of current players 172 d("Initializing list of current media players"); 173 List<android.media.session.MediaController> controllers = 174 mMediaSessionManager.getActiveSessions(null); 175 176 for (android.media.session.MediaController controller : controllers) { 177 addMediaPlayer(controller); 178 } 179 180 // If there were any active players and we don't already have one due to the Media 181 // Framework Callbacks then set the highest priority one to active 182 if (mActivePlayerId == 0 && mMediaPlayers.size() > 0) setActivePlayer(1); 183 }); 184 } 185 cleanup()186 void cleanup() { 187 mContext.unregisterReceiver(mPackageChangedBroadcastReceiver); 188 189 mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveSessionsChangedListener); 190 mMediaSessionManager.setCallback(null, null); 191 mMediaSessionManager = null; 192 193 mMediaPlayerIds.clear(); 194 195 for (MediaPlayerWrapper player : mMediaPlayers.values()) { 196 player.cleanup(); 197 } 198 mMediaPlayers.clear(); 199 200 if (mBrowsablePlayerConnector != null) { 201 mBrowsablePlayerConnector.cleanup(); 202 } 203 for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { 204 player.disconnect(); 205 } 206 mBrowsablePlayers.clear(); 207 } 208 getCurrentPlayerId()209 int getCurrentPlayerId() { 210 return BLUETOOTH_PLAYER_ID; 211 } 212 getFreeMediaPlayerId()213 int getFreeMediaPlayerId() { 214 int id = 1; 215 while (mMediaPlayerIds.containsValue(id)) { 216 id++; 217 } 218 return id; 219 } 220 getActivePlayer()221 MediaPlayerWrapper getActivePlayer() { 222 return mMediaPlayers.get(mActivePlayerId); 223 } 224 225 226 227 // In this case the displayed player is the Bluetooth Player, the number of items is equal 228 // to the number of players. The root ID will always be empty string in this case as well. getPlayerRoot(int playerId, GetPlayerRootCallback cb)229 void getPlayerRoot(int playerId, GetPlayerRootCallback cb) { 230 cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", mBrowsablePlayers.size()); 231 } 232 233 // Return the "Bluetooth Player" as the only player always getMediaPlayerList()234 List<PlayerInfo> getMediaPlayerList() { 235 PlayerInfo info = new PlayerInfo(); 236 info.id = BLUETOOTH_PLAYER_ID; 237 info.name = BLUETOOTH_PLAYER_NAME; 238 info.browsable = true; 239 List<PlayerInfo> ret = new ArrayList<PlayerInfo>(); 240 ret.add(info); 241 242 return ret; 243 } 244 245 @NonNull getCurrentMediaId()246 String getCurrentMediaId() { 247 final MediaPlayerWrapper player = getActivePlayer(); 248 if (player == null) return ""; 249 250 final PlaybackState state = player.getPlaybackState(); 251 final List<Metadata> queue = player.getCurrentQueue(); 252 253 // Disable the now playing list if the player doesn't have a queue or provide an active 254 // queue ID that can be used to determine the active song in the queue. 255 if (state == null 256 || state.getActiveQueueItemId() == MediaSession.QueueItem.UNKNOWN_ID 257 || queue.size() == 0) { 258 d("getCurrentMediaId: No active queue item Id sending empty mediaId: PlaybackState=" 259 + state); 260 return ""; 261 } 262 263 return Util.NOW_PLAYING_PREFIX + state.getActiveQueueItemId(); 264 } 265 266 @NonNull getCurrentSongInfo()267 Metadata getCurrentSongInfo() { 268 final MediaPlayerWrapper player = getActivePlayer(); 269 if (player == null) return Util.empty_data(); 270 271 return player.getCurrentMetadata(); 272 } 273 getCurrentPlayStatus()274 PlaybackState getCurrentPlayStatus() { 275 final MediaPlayerWrapper player = getActivePlayer(); 276 if (player == null) return null; 277 278 return player.getPlaybackState(); 279 } 280 281 @NonNull getNowPlayingList()282 List<Metadata> getNowPlayingList() { 283 // Only send the current song for the now playing if there is no active song. See 284 // |getCurrentMediaId()| for reasons why there might be no active song. 285 if (getCurrentMediaId().equals("")) { 286 List<Metadata> ret = new ArrayList<Metadata>(); 287 Metadata data = getCurrentSongInfo(); 288 data.mediaId = ""; 289 ret.add(data); 290 return ret; 291 } 292 293 return getActivePlayer().getCurrentQueue(); 294 } 295 playItem(int playerId, boolean nowPlaying, String mediaId)296 void playItem(int playerId, boolean nowPlaying, String mediaId) { 297 if (nowPlaying) { 298 playNowPlayingItem(mediaId); 299 } else { 300 playFolderItem(mediaId); 301 } 302 } 303 playNowPlayingItem(String mediaId)304 private void playNowPlayingItem(String mediaId) { 305 d("playNowPlayingItem: mediaId=" + mediaId); 306 307 Pattern regex = Pattern.compile(NOW_PLAYING_ID_PATTERN); 308 Matcher m = regex.matcher(mediaId); 309 if (!m.find()) { 310 // This should never happen since we control the media ID's reported 311 Log.wtf(TAG, "playNowPlayingItem: Couldn't match mediaId to pattern: mediaId=" 312 + mediaId); 313 } 314 315 long queueItemId = Long.parseLong(m.group(1)); 316 if (getActivePlayer() != null) { 317 getActivePlayer().playItemFromQueue(queueItemId); 318 } 319 } 320 playFolderItem(String mediaId)321 private void playFolderItem(String mediaId) { 322 d("playFolderItem: mediaId=" + mediaId); 323 324 if (!mediaId.matches(BROWSE_ID_PATTERN)) { 325 // This should never happen since we control the media ID's reported 326 Log.wtf(TAG, "playFolderItem: mediaId didn't match pattern: mediaId=" + mediaId); 327 } 328 329 int playerIndex = Integer.parseInt(mediaId.substring(0, 2)); 330 String itemId = mediaId.substring(2); 331 332 if (!mBrowsablePlayers.containsKey(playerIndex)) { 333 e("playFolderItem: Do not have the a browsable player with ID " + playerIndex); 334 return; 335 } 336 337 mBrowsablePlayers.get(playerIndex).playItem(itemId); 338 } 339 getFolderItemsMediaPlayerList(GetFolderItemsCallback cb)340 void getFolderItemsMediaPlayerList(GetFolderItemsCallback cb) { 341 d("getFolderItemsMediaPlayerList: Sending Media Player list for root directory"); 342 343 ArrayList<ListItem> playerList = new ArrayList<ListItem>(); 344 for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { 345 346 String displayName = Util.getDisplayName(mContext, player.getPackageName()); 347 int id = mMediaPlayerIds.get(player.getPackageName()); 348 349 d("getFolderItemsMediaPlayerList: Adding player " + displayName); 350 Folder playerFolder = new Folder(String.format("%02d", id), false, displayName); 351 playerList.add(new ListItem(playerFolder)); 352 } 353 cb.run("", playerList); 354 return; 355 } 356 getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb)357 void getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb) { 358 // The playerId is unused since we always assume the remote device is using the 359 // Bluetooth Player. 360 d("getFolderItems(): playerId=" + playerId + ", mediaId=" + mediaId); 361 362 // The device is requesting the content of the root folder. This folder contains a list of 363 // Browsable Media Players displayed as folders with their contents contained within. 364 if (mediaId.equals("")) { 365 getFolderItemsMediaPlayerList(cb); 366 return; 367 } 368 369 if (!mediaId.matches(BROWSE_ID_PATTERN)) { 370 // This should never happen since we control the media ID's reported 371 Log.wtf(TAG, "getFolderItems: mediaId didn't match pattern: mediaId=" + mediaId); 372 } 373 374 int playerIndex = Integer.parseInt(mediaId.substring(0, 2)); 375 String itemId = mediaId.substring(2); 376 377 // TODO (apanicke): Add timeouts for looking up folder items since media browsers don't 378 // have to respond. 379 if (mBrowsablePlayers.containsKey(playerIndex)) { 380 BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(playerIndex); 381 if (itemId.equals("")) { 382 Log.i(TAG, "Empty media id, getting the root for " 383 + wrapper.getPackageName()); 384 itemId = wrapper.getRootId(); 385 } 386 387 wrapper.getFolderItems(itemId, (status, id, results) -> { 388 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { 389 cb.run(mediaId, new ArrayList<ListItem>()); 390 return; 391 } 392 393 String playerPrefix = String.format("%02d", playerIndex); 394 for (ListItem item : results) { 395 if (item.isFolder) { 396 item.folder.mediaId = playerPrefix.concat(item.folder.mediaId); 397 } else { 398 item.song.mediaId = playerPrefix.concat(item.song.mediaId); 399 } 400 } 401 cb.run(mediaId, results); 402 }); 403 return; 404 } else { 405 cb.run(mediaId, new ArrayList<ListItem>()); 406 } 407 } 408 409 // Adds the controller to the MediaPlayerList or updates the controller if we already had 410 // a controller for a package. Returns the new ID of the controller where its added or its 411 // previous value if it already existed. Returns -1 if the controller passed in is invalid addMediaPlayer(android.media.session.MediaController controller)412 int addMediaPlayer(android.media.session.MediaController controller) { 413 if (controller == null) return -1; 414 415 // Each new player has an ID of 1 plus the highest ID. The ID 0 is reserved to signify that 416 // there is no active player. If we already have a browsable player for the package, reuse 417 // that key. 418 String packageName = controller.getPackageName(); 419 if (!mMediaPlayerIds.containsKey(packageName)) { 420 mMediaPlayerIds.put(packageName, getFreeMediaPlayerId()); 421 } 422 423 int playerId = mMediaPlayerIds.get(packageName); 424 425 // If we already have a controller for the package, then update it with this new controller 426 // as the old controller has probably gone stale. 427 if (mMediaPlayers.containsKey(playerId)) { 428 d("Already have a controller for the player: " + packageName + ", updating instead"); 429 MediaPlayerWrapper player = mMediaPlayers.get(playerId); 430 player.updateMediaController(MediaControllerFactory.wrap(controller)); 431 432 // If the media controller we updated was the active player check if the media updated 433 if (playerId == mActivePlayerId) { 434 sendMediaUpdate(getActivePlayer().getCurrentMediaData()); 435 } 436 437 return playerId; 438 } 439 440 MediaPlayerWrapper newPlayer = MediaPlayerWrapper.wrap( 441 MediaControllerFactory.wrap(controller), 442 mLooper); 443 444 Log.i(TAG, "Adding wrapped media player: " + packageName + " at key: " 445 + mMediaPlayerIds.get(controller.getPackageName())); 446 447 mMediaPlayers.put(playerId, newPlayer); 448 return playerId; 449 } 450 removeMediaPlayer(int playerId)451 void removeMediaPlayer(int playerId) { 452 if (!mMediaPlayers.containsKey(playerId)) { 453 e("Trying to remove nonexistent media player: " + playerId); 454 return; 455 } 456 457 // If we removed the active player, set no player as active until the Media Framework 458 // tells us otherwise 459 if (playerId == mActivePlayerId && playerId != NO_ACTIVE_PLAYER) { 460 getActivePlayer().unregisterCallback(); 461 mActivePlayerId = NO_ACTIVE_PLAYER; 462 } 463 464 final MediaPlayerWrapper wrapper = mMediaPlayers.get(playerId); 465 d("Removing media player " + wrapper.getPackageName()); 466 mMediaPlayerIds.remove(wrapper.getPackageName()); 467 mMediaPlayers.remove(playerId); 468 wrapper.cleanup(); 469 } 470 setActivePlayer(int playerId)471 void setActivePlayer(int playerId) { 472 if (!mMediaPlayers.containsKey(playerId)) { 473 e("Player doesn't exist in list(): " + playerId); 474 return; 475 } 476 477 if (playerId == mActivePlayerId) { 478 Log.w(TAG, getActivePlayer().getPackageName() + " is already the active player"); 479 return; 480 } 481 482 if (mActivePlayerId != NO_ACTIVE_PLAYER) getActivePlayer().unregisterCallback(); 483 484 mActivePlayerId = playerId; 485 getActivePlayer().registerCallback(mMediaPlayerCallback); 486 Log.i(TAG, "setActivePlayer(): setting player to " + getActivePlayer().getPackageName()); 487 488 // Ensure that metadata is synced on the new player 489 if (!getActivePlayer().isMetadataSynced()) { 490 Log.w(TAG, "setActivePlayer(): Metadata not synced on new player"); 491 return; 492 } 493 494 if (Utils.isPtsTestMode()) { 495 sendFolderUpdate(true, true, false); 496 } 497 498 sendMediaUpdate(getActivePlayer().getCurrentMediaData()); 499 } 500 501 // TODO (apanicke): Add logging for media key events in dumpsys sendMediaKeyEvent(int key, boolean pushed)502 void sendMediaKeyEvent(int key, boolean pushed) { 503 d("sendMediaKeyEvent: key=" + key + " pushed=" + pushed); 504 int action = pushed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP; 505 KeyEvent event = new KeyEvent(action, AvrcpPassthrough.toKeyCode(key)); 506 mMediaSessionManager.dispatchMediaKeyEvent(event); 507 } 508 sendFolderUpdate(boolean availablePlayers, boolean addressedPlayers, boolean uids)509 private void sendFolderUpdate(boolean availablePlayers, boolean addressedPlayers, 510 boolean uids) { 511 d("sendFolderUpdate"); 512 if (mCallback == null) { 513 return; 514 } 515 516 mCallback.run(availablePlayers, addressedPlayers, uids); 517 } 518 sendMediaUpdate(MediaData data)519 private void sendMediaUpdate(MediaData data) { 520 d("sendMediaUpdate"); 521 if (mCallback == null) { 522 return; 523 } 524 525 // Always have items in the queue 526 if (data.queue.size() == 0) { 527 Log.i(TAG, "sendMediaUpdate: Creating a one item queue for a player with no queue"); 528 data.queue.add(data.metadata); 529 } 530 531 mCallback.run(data); 532 } 533 534 private final MediaSessionManager.OnActiveSessionsChangedListener 535 mActiveSessionsChangedListener = 536 new MediaSessionManager.OnActiveSessionsChangedListener() { 537 @Override 538 public void onActiveSessionsChanged( 539 List<android.media.session.MediaController> newControllers) { 540 synchronized (MediaPlayerList.this) { 541 Log.v(TAG, "onActiveSessionsChanged: number of controllers: " 542 + newControllers.size()); 543 if (newControllers.size() == 0) return; 544 545 // Apps are allowed to have multiple MediaControllers. If an app does have 546 // multiple controllers then newControllers contains them in highest 547 // priority order. Since we only want to keep the highest priority one, 548 // we keep track of which controllers we updated and skip over ones 549 // we've already looked at. 550 HashSet<String> addedPackages = new HashSet<String>(); 551 552 for (int i = 0; i < newControllers.size(); i++) { 553 Log.d(TAG, "onActiveSessionsChanged: controller: " 554 + newControllers.get(i).getPackageName()); 555 if (addedPackages.contains(newControllers.get(i).getPackageName())) { 556 continue; 557 } 558 559 addedPackages.add(newControllers.get(i).getPackageName()); 560 addMediaPlayer(newControllers.get(i)); 561 } 562 } 563 } 564 }; 565 566 // TODO (apanicke): Write a test that tests uninstalling the active session 567 private final BroadcastReceiver mPackageChangedBroadcastReceiver = new BroadcastReceiver() { 568 @Override 569 public void onReceive(Context context, Intent intent) { 570 String action = intent.getAction(); 571 Log.v(TAG, "mPackageChangedBroadcastReceiver: action: " + action); 572 573 if (action.equals(Intent.ACTION_PACKAGE_REMOVED) 574 || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) { 575 if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return; 576 577 String packageName = intent.getData().getSchemeSpecificPart(); 578 if (packageName != null && mMediaPlayerIds.containsKey(packageName)) { 579 removeMediaPlayer(mMediaPlayerIds.get(packageName)); 580 } 581 } else if (action.equals(Intent.ACTION_PACKAGE_ADDED) 582 || action.equals(Intent.ACTION_PACKAGE_CHANGED)) { 583 String packageName = intent.getData().getSchemeSpecificPart(); 584 if (packageName != null) { 585 if (DEBUG) Log.d(TAG, "Name of package changed: " + packageName); 586 // TODO (apanicke): Handle either updating or adding the new package. 587 // Check if its browsable and send the UIDS changed to update the 588 // root folder 589 } 590 } 591 } 592 }; 593 594 private final MediaPlayerWrapper.Callback mMediaPlayerCallback = 595 new MediaPlayerWrapper.Callback() { 596 @Override 597 public void mediaUpdatedCallback(MediaData data) { 598 if (data.metadata == null) { 599 Log.d(TAG, "mediaUpdatedCallback(): metadata is null"); 600 return; 601 } 602 603 if (data.state == null) { 604 Log.w(TAG, "mediaUpdatedCallback(): Tried to update with null state"); 605 return; 606 } 607 608 sendMediaUpdate(data); 609 } 610 }; 611 612 private final MediaSessionManager.Callback mButtonDispatchCallback = 613 new MediaSessionManager.Callback() { 614 @Override 615 public void onMediaKeyEventDispatched(KeyEvent event, MediaSession.Token token) { 616 // TODO (apanicke): Add logging for these 617 } 618 619 @Override 620 public void onMediaKeyEventDispatched(KeyEvent event, ComponentName receiver) { 621 // TODO (apanicke): Add logging for these 622 } 623 624 @Override 625 public void onAddressedPlayerChanged(MediaSession.Token token) { 626 android.media.session.MediaController controller = 627 new android.media.session.MediaController(mContext, token); 628 629 if (!mMediaPlayerIds.containsKey(controller.getPackageName())) { 630 // Since we have a controller, we can try to to recover by adding the 631 // player and then setting it as active. 632 Log.w(TAG, "onAddressedPlayerChanged(Token): Addressed Player " 633 + "changed to a player we didn't have a session for"); 634 addMediaPlayer(controller); 635 } 636 637 Log.i(TAG, "onAddressedPlayerChanged: token=" + controller.getPackageName()); 638 setActivePlayer(mMediaPlayerIds.get(controller.getPackageName())); 639 } 640 641 @Override 642 public void onAddressedPlayerChanged(ComponentName receiver) { 643 if (receiver == null) { 644 return; 645 } 646 647 if (!mMediaPlayerIds.containsKey(receiver.getPackageName())) { 648 e("onAddressedPlayerChanged(Component): Addressed Player " 649 + "changed to a player we don't have a session for"); 650 return; 651 } 652 653 Log.i(TAG, "onAddressedPlayerChanged: component=" + receiver.getPackageName()); 654 setActivePlayer(mMediaPlayerIds.get(receiver.getPackageName())); 655 } 656 }; 657 658 dump(StringBuilder sb)659 void dump(StringBuilder sb) { 660 sb.append("List of MediaControllers: size=" + mMediaPlayers.size() + "\n"); 661 for (int id : mMediaPlayers.keySet()) { 662 if (id == mActivePlayerId) { 663 sb.append("<Active> "); 664 } 665 MediaPlayerWrapper player = mMediaPlayers.get(id); 666 sb.append(" Media Player " + id + ": " + player.getPackageName() + "\n"); 667 sb.append(player.toString().replaceAll("(?m)^", " ")); 668 sb.append("\n"); 669 } 670 671 sb.append("List of Browsers: size=" + mBrowsablePlayers.size() + "\n"); 672 for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { 673 sb.append(player.toString().replaceAll("(?m)^", " ")); 674 sb.append("\n"); 675 } 676 // TODO (apanicke): Add media key events 677 // TODO (apanicke): Add last sent data 678 // TODO (apanicke): Add addressed player history 679 } 680 e(String message)681 private static void e(String message) { 682 if (sTesting) { 683 Log.wtfStack(TAG, message); 684 } else { 685 Log.e(TAG, message); 686 } 687 } 688 d(String message)689 private static void d(String message) { 690 if (DEBUG) { 691 Log.d(TAG, message); 692 } 693 } 694 } 695