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