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.audio_util; 18 19 import android.annotation.NonNull; 20 import android.content.BroadcastReceiver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.IntentFilter; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ResolveInfo; 26 import android.media.AudioAttributes; 27 import android.media.AudioManager; 28 import android.media.AudioPlaybackConfiguration; 29 import android.media.session.MediaSession; 30 import android.media.session.MediaSessionManager; 31 import android.media.session.PlaybackState; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.os.SystemProperties; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.view.KeyEvent; 38 39 import com.android.bluetooth.BluetoothEventLogger; 40 import com.android.bluetooth.Utils; 41 import com.android.bluetooth.avrcp.AvrcpPassthrough; 42 import com.android.bluetooth.flags.Flags; 43 import com.android.internal.annotations.VisibleForTesting; 44 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.HashMap; 48 import java.util.HashSet; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.regex.Matcher; 52 import java.util.regex.Pattern; 53 54 /** 55 * This class is directly responsible of maintaining the list of Browsable Players as well as the 56 * list of Addressable Players. This variation of the list doesn't actually list all the available 57 * players for a getAvailableMediaPlayers request. Instead it only reports one media player with 58 * ID=0 and all the other browsable players are folders in the root of that player. 59 * 60 * <p>Changing the directory to a browsable player will allow you to traverse that player as normal. 61 * By only having one root player, we never have to send Addressed Player Changed notifications, 62 * UIDs Changed notifications, or Available Players Changed notifications. 63 * 64 * <p>TODO (apanicke): Add non-browsable players as song items to the root folder. Selecting that 65 * player would effectively cause player switch by sending a play command to that player. 66 */ 67 public class MediaPlayerList { 68 private static final String TAG = MediaPlayerList.class.getSimpleName(); 69 static boolean sTesting = false; 70 71 private static final String PACKAGE_SCHEME = "package"; 72 private static final int NO_ACTIVE_PLAYER = 0; 73 private static final int BLUETOOTH_PLAYER_ID = 0; 74 private static final String BLUETOOTH_PLAYER_NAME = "Bluetooth Player"; 75 private static final int ACTIVE_PLAYER_LOGGER_SIZE = 5; 76 private static final String ACTIVE_PLAYER_LOGGER_TITLE = "BTAudio Active Player Events"; 77 private static final int AUDIO_PLAYBACK_STATE_LOGGER_SIZE = 15; 78 private static final String AUDIO_PLAYBACK_STATE_LOGGER_TITLE = 79 "BTAudio Audio Playback State Events"; 80 81 // mediaId's for the now playing list will be in the form of "NowPlayingId[XX]" where [XX] 82 // is the Queue ID for the requested item. 83 private static final String NOW_PLAYING_ID_PATTERN = Util.NOW_PLAYING_PREFIX + "([0-9]*)"; 84 85 // mediaId's for folder browsing will be in the form of [XX][mediaid], where [XX] is a 86 // two digit representation of the player id and [mediaid] is the original media id as a 87 // string. 88 private static final String BROWSE_ID_PATTERN = "\\d\\d.*"; 89 90 private Context mContext; 91 private Looper mLooper; // Thread all media player callbacks and timeouts happen on 92 private MediaSessionManager mMediaSessionManager; 93 private MediaData mCurrMediaData = null; 94 private final AudioManager mAudioManager; 95 96 private final BluetoothEventLogger mActivePlayerLogger = 97 new BluetoothEventLogger(ACTIVE_PLAYER_LOGGER_SIZE, ACTIVE_PLAYER_LOGGER_TITLE); 98 private final BluetoothEventLogger mAudioPlaybackStateLogger = 99 new BluetoothEventLogger( 100 AUDIO_PLAYBACK_STATE_LOGGER_SIZE, AUDIO_PLAYBACK_STATE_LOGGER_TITLE); 101 102 private Map<Integer, MediaPlayerWrapper> mMediaPlayers = 103 Collections.synchronizedMap(new HashMap<Integer, MediaPlayerWrapper>()); 104 private Map<String, Integer> mMediaPlayerIds = 105 Collections.synchronizedMap(new HashMap<String, Integer>()); 106 private Map<Integer, BrowsedPlayerWrapper> mBrowsablePlayers = 107 Collections.synchronizedMap(new HashMap<Integer, BrowsedPlayerWrapper>()); 108 private int mActivePlayerId = NO_ACTIVE_PLAYER; 109 110 private MediaUpdateCallback mCallback; 111 private boolean mAudioPlaybackIsActive = false; 112 113 private BrowsablePlayerConnector mBrowsablePlayerConnector; 114 115 private MediaPlayerSettingsEventListener mPlayerSettingsListener; 116 117 public interface MediaUpdateCallback { run(MediaData data)118 void run(MediaData data); 119 run(boolean availablePlayers, boolean addressedPlayers, boolean uids)120 void run(boolean availablePlayers, boolean addressedPlayers, boolean uids); 121 } 122 123 public interface GetPlayerRootCallback { run(int playerId, boolean success, String rootId, int numItems)124 void run(int playerId, boolean success, String rootId, int numItems); 125 } 126 127 public interface GetFolderItemsCallback { run(String parentId, List<ListItem> items)128 void run(String parentId, List<ListItem> items); 129 } 130 131 /** Listener for PlayerSettingsManager. */ 132 public interface MediaPlayerSettingsEventListener { 133 /** Called when the active player has changed. */ onActivePlayerChanged(MediaPlayerWrapper player)134 void onActivePlayerChanged(MediaPlayerWrapper player); 135 } 136 MediaPlayerList(Looper looper, Context context)137 public MediaPlayerList(Looper looper, Context context) { 138 Log.v(TAG, "Creating MediaPlayerList"); 139 140 mLooper = looper; 141 mContext = context; 142 143 // Register for intents where available players might have changed 144 IntentFilter pkgFilter = new IntentFilter(); 145 pkgFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); 146 pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 147 pkgFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED); 148 pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 149 pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); 150 pkgFilter.addDataScheme(PACKAGE_SCHEME); 151 context.registerReceiver(mPackageChangedBroadcastReceiver, pkgFilter); 152 153 mAudioManager = context.getSystemService(AudioManager.class); 154 mAudioManager.registerAudioPlaybackCallback(mAudioPlaybackCallback, new Handler(mLooper)); 155 156 mMediaSessionManager = context.getSystemService(MediaSessionManager.class); 157 mMediaSessionManager.addOnActiveSessionsChangedListener( 158 mActiveSessionsChangedListener, null, new Handler(looper)); 159 mMediaSessionManager.addOnMediaKeyEventSessionChangedListener( 160 mContext.getMainExecutor(), mMediaKeyEventSessionChangedListener); 161 } 162 163 /** 164 * Retrieves the list of active {@link android.media.session.MediaController}, convert them to 165 * local {@link MediaController}, converts them again to {@link MediaPlayerWrapper} and add them 166 * to the local list ({@link #mMediaPlayers}). 167 * 168 * <p>If we already received an onActiveSessionsChanged callback, set this player as active, 169 * otherwise set the highest priority one as active (first in the list). 170 */ constructCurrentPlayers()171 private void constructCurrentPlayers() { 172 // Construct the list of current players 173 d("Initializing list of current media players"); 174 List<android.media.session.MediaController> controllers = 175 mMediaSessionManager.getActiveSessions(null); 176 177 for (android.media.session.MediaController controller : controllers) { 178 if ((controller.getFlags() & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) != 0) { 179 // GLOBAL_PRIORITY session is created by Telecom to handle call control key events 180 // but Bluetooth Headset profile handles the key events for calls so we don't have 181 // to handle these sessions in AVRCP. 182 continue; 183 } 184 addMediaPlayer(controller); 185 } 186 187 // If there were any active players and we don't already have one due to the Media 188 // Framework Callbacks then set the highest priority one to active 189 if (mActivePlayerId == 0 && mMediaPlayers.size() > 0) { 190 String packageName = mMediaSessionManager.getMediaKeyEventSessionPackageName(); 191 if (!TextUtils.isEmpty(packageName) && haveMediaPlayer(packageName)) { 192 Log.i(TAG, "Set active player to MediaKeyEvent session = " + packageName); 193 setActivePlayer(mMediaPlayerIds.get(packageName)); 194 } else { 195 Log.i(TAG, "Set active player to first default"); 196 setActivePlayer(1); 197 } 198 } 199 } 200 201 /** Initiates browsable players and calls {@link #constructCurrentPlayers}. */ init(MediaUpdateCallback callback)202 public void init(MediaUpdateCallback callback) { 203 Log.v(TAG, "Initializing MediaPlayerList"); 204 mCallback = callback; 205 206 if (!SystemProperties.getBoolean("bluetooth.avrcp.browsable_media_player.enabled", true)) { 207 // Allow to disable BrowsablePlayerConnector with systemproperties. 208 // This is useful when for watches because: 209 // 1. It is not a regular use case 210 // 2. Registering to all players is a very loading task 211 212 Log.i(TAG, "init: without Browsable Player"); 213 constructCurrentPlayers(); 214 return; 215 } 216 217 // Build the list of browsable players and afterwards, build the list of media players 218 Intent intent = new Intent(android.service.media.MediaBrowserService.SERVICE_INTERFACE); 219 if (Flags.keepStoppedMediaBrowserService()) { 220 // Don't query stopped apps, that would end up unstopping them 221 intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES); 222 } 223 List<ResolveInfo> playerList = 224 mContext.getApplicationContext() 225 .getPackageManager() 226 .queryIntentServices(intent, PackageManager.MATCH_ALL); 227 228 mBrowsablePlayerConnector = 229 BrowsablePlayerConnector.connectToPlayers( 230 mContext, 231 mLooper, 232 playerList, 233 (List<BrowsedPlayerWrapper> players) -> { 234 Log.i(TAG, "init: Browsable Player list size is " + players.size()); 235 236 // Check to see if the list has been cleaned up before this completed 237 if (mMediaSessionManager == null) { 238 return; 239 } 240 241 for (BrowsedPlayerWrapper wrapper : players) { 242 // Generate new id and add the browsable player 243 if (!havePlayerId(wrapper.getPackageName())) { 244 mMediaPlayerIds.put( 245 wrapper.getPackageName(), getFreeMediaPlayerId()); 246 } 247 248 d( 249 "Adding Browser Wrapper for " 250 + wrapper.getPackageName() 251 + " with id " 252 + mMediaPlayerIds.get(wrapper.getPackageName())); 253 254 mBrowsablePlayers.put( 255 mMediaPlayerIds.get(wrapper.getPackageName()), wrapper); 256 257 wrapper.getFolderItems( 258 wrapper.getRootId(), 259 (int status, String mediaId, List<ListItem> results) -> { 260 d( 261 "Got the contents for: " 262 + mediaId 263 + " : num results=" 264 + results.size()); 265 }); 266 } 267 268 constructCurrentPlayers(); 269 }); 270 } 271 cleanup()272 public void cleanup() { 273 mCallback = null; 274 mContext.unregisterReceiver(mPackageChangedBroadcastReceiver); 275 276 mActivePlayerId = NO_ACTIVE_PLAYER; 277 278 mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveSessionsChangedListener); 279 mMediaSessionManager.removeOnMediaKeyEventSessionChangedListener( 280 mMediaKeyEventSessionChangedListener); 281 mMediaSessionManager = null; 282 283 mAudioManager.unregisterAudioPlaybackCallback(mAudioPlaybackCallback); 284 285 mMediaPlayerIds.clear(); 286 287 for (MediaPlayerWrapper player : mMediaPlayers.values()) { 288 player.cleanup(); 289 } 290 mMediaPlayers.clear(); 291 292 if (mBrowsablePlayerConnector != null) { 293 mBrowsablePlayerConnector.cleanup(); 294 } 295 for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { 296 player.disconnect(); 297 } 298 mBrowsablePlayers.clear(); 299 } 300 301 /** 302 * Current player ID is always Bluetooth player ID. 303 * 304 * <p>All browsable players are subdirectories of Bluetooth player. 305 */ getCurrentPlayerId()306 public int getCurrentPlayerId() { 307 return BLUETOOTH_PLAYER_ID; 308 } 309 310 /** Get the next ID available in the IDs map. */ getFreeMediaPlayerId()311 int getFreeMediaPlayerId() { 312 int id = 1; 313 while (mMediaPlayerIds.containsValue(id)) { 314 id++; 315 } 316 return id; 317 } 318 319 /** Returns the {@link #MediaPlayerWrapper} with ID matching {@link #mActivePlayerId}. */ getActivePlayer()320 public MediaPlayerWrapper getActivePlayer() { 321 return mMediaPlayers.get(mActivePlayerId); 322 } 323 324 /** This is used to send passthrough command to media session */ sendMediaKeyEvent(int key, boolean pushed)325 public void sendMediaKeyEvent(int key, boolean pushed) { 326 if (mMediaSessionManager == null) { 327 Log.d(TAG, "Bluetooth is turning off, ignore it"); 328 return; 329 } 330 int action = pushed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP; 331 KeyEvent event = new KeyEvent(action, AvrcpPassthrough.toKeyCode(key)); 332 mMediaSessionManager.dispatchMediaKeyEvent(event, false); 333 } 334 335 /** 336 * This is used by setBrowsedPlayer as the browsed player is always the Bluetooth player. 337 * 338 * <p>If the requested player ID is not {@link #BLUETOOTH_PLAYER_ID}, success will be false. 339 * 340 * <p>The number of items will be the number of browsable players as they all are direct 341 * subdirectories of the Bluetooth player ID. 342 * 343 * <p>The root ID will always be an empty string to correspond to bluetooth player ID. 344 */ getPlayerRoot(int playerId, GetPlayerRootCallback cb)345 public void getPlayerRoot(int playerId, GetPlayerRootCallback cb) { 346 /** M: Fix PTS AVRCP/TG/MCN/CB/BI-02-C fail @{ */ 347 if (Utils.isPtsTestMode()) { 348 d("PTS test mode: getPlayerRoot"); 349 BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(BLUETOOTH_PLAYER_ID + 1); 350 String itemId = wrapper.getRootId(); 351 352 wrapper.getFolderItems( 353 itemId, 354 (status, id, results) -> { 355 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { 356 cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", 0); 357 return; 358 } 359 cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", results.size()); 360 }); 361 return; 362 } 363 /** 364 * @} 365 */ 366 cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", mBrowsablePlayers.size()); 367 } 368 369 /** 370 * Returns a list containing only the Bluetooth player. 371 * 372 * <p>See class documentation. 373 */ getMediaPlayerList()374 public List<PlayerInfo> getMediaPlayerList() { 375 PlayerInfo info = new PlayerInfo(); 376 info.id = BLUETOOTH_PLAYER_ID; 377 info.name = BLUETOOTH_PLAYER_NAME; 378 info.browsable = true; 379 if (mBrowsablePlayers.size() == 0) { 380 // Set Bluetooth Player as non-browable if there is not browsable player exist. 381 info.browsable = false; 382 } 383 List<PlayerInfo> ret = new ArrayList<PlayerInfo>(); 384 ret.add(info); 385 386 return ret; 387 } 388 389 /** 390 * Returns the active queue item id of the active player. 391 * 392 * <p>If the player's queue is empty, if the active queue item id is unknown or if the {@link 393 * android.media.session.PlaybackState} is null, returns an empty string. 394 */ 395 @NonNull getCurrentMediaId()396 public String getCurrentMediaId() { 397 final MediaPlayerWrapper player = getActivePlayer(); 398 if (player == null) return ""; 399 400 final PlaybackState state = player.getPlaybackState(); 401 final List<Metadata> queue = player.getCurrentQueue(); 402 403 // Disable the now playing list if the player doesn't have a queue or provide an active 404 // queue ID that can be used to determine the active song in the queue. 405 if (state == null 406 || state.getActiveQueueItemId() == MediaSession.QueueItem.UNKNOWN_ID 407 || queue.size() == 0) { 408 d( 409 "getCurrentMediaId: No active queue item Id sending empty mediaId:" 410 + " PlaybackState=" 411 + state); 412 return ""; 413 } 414 415 return Util.NOW_PLAYING_PREFIX + state.getActiveQueueItemId(); 416 } 417 418 /** 419 * Returns the active {@link android.media.session.MediaController}'s metadata, converted to 420 * {@link Metadata}. 421 */ 422 @NonNull getCurrentSongInfo()423 public Metadata getCurrentSongInfo() { 424 final MediaPlayerWrapper player = getActivePlayer(); 425 if (player == null) return Util.empty_data(); 426 427 return player.getCurrentMetadata(); 428 } 429 430 /** 431 * Returns the current playing state of the active player. 432 * 433 * <p>If {@link #mAudioPlaybackIsActive} is true and the returned state is different from {@link 434 * android.media.session.PlaybackState.STATE_PLAYING}, returns a copy of the state with playing 435 * state {@link android.media.session.PlaybackState.STATE_PLAYING}. 436 */ getCurrentPlayStatus()437 public PlaybackState getCurrentPlayStatus() { 438 final MediaPlayerWrapper player = getActivePlayer(); 439 if (player == null) return null; 440 441 PlaybackState state = player.getPlaybackState(); 442 if (mAudioPlaybackIsActive 443 && (state == null || state.getState() != PlaybackState.STATE_PLAYING)) { 444 return new PlaybackState.Builder() 445 .setState( 446 PlaybackState.STATE_PLAYING, 447 state == null ? 0 : state.getPosition(), 448 1.0f) 449 .build(); 450 } 451 return state; 452 } 453 454 /** 455 * Returns the current queue of the active player. 456 * 457 * <p>If there is no queue, returns a list containing only the active player's Metadata. 458 * 459 * <p>See {@link #getCurrentSongInfo} and {@link #getCurrentMediaId}. 460 */ 461 @NonNull getNowPlayingList()462 public List<Metadata> getNowPlayingList() { 463 // Only send the current song for the now playing if there is no active song. See 464 // |getCurrentMediaId()| for reasons why there might be no active song. 465 if (getCurrentMediaId().equals("")) { 466 List<Metadata> ret = new ArrayList<Metadata>(); 467 Metadata data = getCurrentSongInfo(); 468 data.mediaId = ""; 469 ret.add(data); 470 return ret; 471 } 472 473 return getActivePlayer().getCurrentQueue(); 474 } 475 476 /** 477 * Informs Media that there is a new request to play {@code mediaId}. 478 * 479 * <p>If the {@code nowPlaying} parameter is true, this will try to select the item from the 480 * current active player's queue. Otherwise this means that the item is from a browsable player 481 * and this calls {@link BrowsedPlayerWrapper} to handle the change. 482 */ playItem(int playerId, boolean nowPlaying, String mediaId)483 public void playItem(int playerId, boolean nowPlaying, String mediaId) { 484 if (nowPlaying) { 485 playNowPlayingItem(mediaId); 486 } else { 487 playFolderItem(mediaId); 488 } 489 } 490 491 /** 492 * Retrieves the active player and plays item from queue. 493 * 494 * <p>See {@link #playItem}. 495 */ playNowPlayingItem(String mediaId)496 private void playNowPlayingItem(String mediaId) { 497 d("playNowPlayingItem: mediaId=" + mediaId); 498 499 Pattern regex = Pattern.compile(NOW_PLAYING_ID_PATTERN); 500 Matcher m = regex.matcher(mediaId); 501 if (!m.find()) { 502 // This should never happen since we control the media ID's reported 503 Log.wtf( 504 TAG, 505 "playNowPlayingItem: Couldn't match mediaId to pattern: mediaId=" + mediaId); 506 } 507 508 long queueItemId = Long.parseLong(m.group(1)); 509 if (getActivePlayer() != null) { 510 getActivePlayer().playItemFromQueue(queueItemId); 511 } 512 } 513 514 /** 515 * Retrieves the {@link BrowsedPlayerWrapper} corresponding to the {@code mediaId} and plays it. 516 * 517 * <p>See {@link #playItem}. 518 */ playFolderItem(String mediaId)519 private void playFolderItem(String mediaId) { 520 d("playFolderItem: mediaId=" + mediaId); 521 522 if (!mediaId.matches(BROWSE_ID_PATTERN)) { 523 // This should never happen since we control the media ID's reported 524 Log.wtf(TAG, "playFolderItem: mediaId didn't match pattern: mediaId=" + mediaId); 525 } 526 527 int playerIndex = Integer.parseInt(mediaId.substring(0, 2)); 528 if (!haveMediaBrowser(playerIndex)) { 529 e("playFolderItem: Do not have the a browsable player with ID " + playerIndex); 530 return; 531 } 532 533 BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(playerIndex); 534 String itemId = mediaId.substring(2); 535 if (TextUtils.isEmpty(itemId)) { 536 itemId = wrapper.getRootId(); 537 if (TextUtils.isEmpty(itemId)) { 538 e("playFolderItem: Failed to start playback with an empty media id."); 539 return; 540 } 541 Log.i( 542 TAG, 543 "playFolderItem: Empty media id, trying with the root id for " 544 + wrapper.getPackageName()); 545 } 546 wrapper.playItem(itemId); 547 } 548 549 /** 550 * Calls {@code cb} with the list of browsable players as folder items. 551 * 552 * <p>As browsable players are subdirectories of the root Bluetooth player, the list always 553 * contains all the browsable players. 554 * 555 * <p>See {@link #getFolderItems}. 556 */ getFolderItemsMediaPlayerList(GetFolderItemsCallback cb)557 void getFolderItemsMediaPlayerList(GetFolderItemsCallback cb) { 558 d("getFolderItemsMediaPlayerList: Sending Media Player list for root directory"); 559 560 ArrayList<ListItem> playerList = new ArrayList<ListItem>(); 561 for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { 562 563 String displayName = Util.getDisplayName(mContext, player.getPackageName()); 564 int id = mMediaPlayerIds.get(player.getPackageName()); 565 566 d("getFolderItemsMediaPlayerList: Adding player " + displayName); 567 Folder playerFolder = new Folder(String.format("%02d", id), false, displayName); 568 playerList.add(new ListItem(playerFolder)); 569 } 570 cb.run("", playerList); 571 } 572 573 /** 574 * Calls {@code cb} with a list of browsable folders. 575 * 576 * <p>If {@code mediaId} is empty, {@code cb} will be called with all the browsable players as 577 * they are subdirectories of the root Bluetooth player. 578 * 579 * <p>If {@code mediaId} corresponds to a known {@link BrowsedPlayerWrapper}, {@code cb} will be 580 * called with the folder items list of the {@link BrowsedPlayerWrapper}. 581 */ getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb)582 public void getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb) { 583 // The playerId is unused since we always assume the remote device is using the 584 // Bluetooth Player. 585 d("getFolderItems(): playerId=" + playerId + ", mediaId=" + mediaId); 586 /** M: Fix PTS AVRCP/TG/MCN/CB/BI-02-C fail @{ */ 587 if (Utils.isPtsTestMode()) { 588 d("PTS test mode: getFolderItems"); 589 BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(BLUETOOTH_PLAYER_ID + 1); 590 String itemId = mediaId; 591 if (mediaId.equals("")) { 592 itemId = wrapper.getRootId(); 593 } 594 595 wrapper.getFolderItems( 596 itemId, 597 (status, id, results) -> { 598 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { 599 cb.run(mediaId, new ArrayList<ListItem>()); 600 return; 601 } 602 cb.run(mediaId, results); 603 }); 604 return; 605 } 606 /** 607 * @} 608 */ 609 610 // The device is requesting the content of the root folder. This folder contains a list of 611 // Browsable Media Players displayed as folders with their contents contained within. 612 if (mediaId.equals("")) { 613 getFolderItemsMediaPlayerList(cb); 614 return; 615 } 616 617 if (!mediaId.matches(BROWSE_ID_PATTERN)) { 618 // This should never happen since we control the media ID's reported 619 Log.wtf(TAG, "getFolderItems: mediaId didn't match pattern: mediaId=" + mediaId); 620 } 621 622 int playerIndex = Integer.parseInt(mediaId.substring(0, 2)); 623 String itemId = mediaId.substring(2); 624 625 // TODO (apanicke): Add timeouts for looking up folder items since media browsers don't 626 // have to respond. 627 if (haveMediaBrowser(playerIndex)) { 628 BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(playerIndex); 629 if (itemId.equals("")) { 630 Log.i(TAG, "Empty media id, getting the root for " + wrapper.getPackageName()); 631 itemId = wrapper.getRootId(); 632 } 633 634 wrapper.getFolderItems( 635 itemId, 636 (status, id, results) -> { 637 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { 638 cb.run(mediaId, new ArrayList<ListItem>()); 639 return; 640 } 641 642 String playerPrefix = String.format("%02d", playerIndex); 643 for (ListItem item : results) { 644 if (item.isFolder) { 645 item.folder.mediaId = playerPrefix.concat(item.folder.mediaId); 646 } else { 647 item.song.mediaId = playerPrefix.concat(item.song.mediaId); 648 } 649 } 650 cb.run(mediaId, results); 651 }); 652 return; 653 } else { 654 cb.run(mediaId, new ArrayList<ListItem>()); 655 } 656 } 657 658 /** 659 * Adds a {@link MediaController} to the {@link #mMediaPlayers} map and returns its ID. 660 * 661 * <p>Each {@link MediaController} is mapped to an ID and each ID is mapped to a package name. 662 * If the new {@link MediaController}'s package name is already present in {@link 663 * #mMediaPlayerIds}, we keep the ID and replace the {@link MediaController}. Otherwise, we add 664 * an entry in both {@link #mMediaPlayerIds} and {@link #mMediaPlayers} maps. 665 * 666 * <p>Also sends the new {@link MediaData} to the AVRCP service. 667 */ 668 @VisibleForTesting addMediaPlayer(MediaController controller)669 int addMediaPlayer(MediaController controller) { 670 // Each new player has an ID of 1 plus the highest ID. The ID 0 is reserved to signify that 671 // there is no active player. If we already have a browsable player for the package, reuse 672 // that key. 673 String packageName = controller.getPackageName(); 674 if (!havePlayerId(packageName)) { 675 mMediaPlayerIds.put(packageName, getFreeMediaPlayerId()); 676 } 677 678 int playerId = mMediaPlayerIds.get(packageName); 679 680 // If we already have a controller for the package, then update it with this new controller 681 // as the old controller has probably gone stale. 682 if (haveMediaPlayer(playerId)) { 683 d("Already have a controller for the player: " + packageName + ", updating instead"); 684 MediaPlayerWrapper player = mMediaPlayers.get(playerId); 685 player.updateMediaController(controller); 686 687 // If the media controller we updated was the active player check if the media updated 688 if (playerId == mActivePlayerId) { 689 sendMediaUpdate(getActivePlayer().getCurrentMediaData()); 690 } 691 692 return playerId; 693 } 694 695 MediaPlayerWrapper newPlayer = 696 MediaPlayerWrapperFactory.wrap(mContext, controller, mLooper); 697 698 Log.i( 699 TAG, 700 "Adding wrapped media player: " 701 + packageName 702 + " at key: " 703 + mMediaPlayerIds.get(controller.getPackageName())); 704 705 mMediaPlayers.put(playerId, newPlayer); 706 return playerId; 707 } 708 709 /** 710 * Adds a {@link android.media.session.MediaController} to the {@link #mMediaPlayers} map and 711 * returns its ID. If the {@link android.media.session.MediaController} is null, returns -1. 712 * 713 * <p>See {@link #addMediaPlayer(MediaController)}. 714 */ addMediaPlayer(android.media.session.MediaController controller)715 int addMediaPlayer(android.media.session.MediaController controller) { 716 if (controller == null) { 717 e("Trying to add a null MediaController"); 718 return -1; 719 } 720 721 return addMediaPlayer(MediaControllerFactory.wrap(controller)); 722 } 723 724 /** Returns true if {@code packageName} is present in {@link #mMediaPlayerIds}. */ havePlayerId(String packageName)725 boolean havePlayerId(String packageName) { 726 if (packageName == null) return false; 727 return mMediaPlayerIds.containsKey(packageName); 728 } 729 730 /** 731 * Returns true if {@code packageName} is present in {@link #mMediaPlayerIds} and the 732 * corresponding ID has a {@link MediaPlayerWrapper} set in {@link #mMediaPlayers}. 733 */ haveMediaPlayer(String packageName)734 boolean haveMediaPlayer(String packageName) { 735 if (!havePlayerId(packageName)) return false; 736 int playerId = mMediaPlayerIds.get(packageName); 737 return mMediaPlayers.containsKey(playerId); 738 } 739 740 /** Returns true if {@code playerId} is present in {@link #mMediaPlayers}. */ haveMediaPlayer(int playerId)741 boolean haveMediaPlayer(int playerId) { 742 return mMediaPlayers.containsKey(playerId); 743 } 744 745 /** Returns true if {@code playerId} is present in {@link #mBrowsablePlayers}. */ haveMediaBrowser(int playerId)746 boolean haveMediaBrowser(int playerId) { 747 return mBrowsablePlayers.containsKey(playerId); 748 } 749 750 /** 751 * Removes the entry corresponding to {@code playerId} from {@link #mMediaPlayers} and {@link 752 * #mMediaPlayerIds}. 753 * 754 * <p>If the removed player was the active one, we consider that there is no more active players 755 * until we receive a {@link MediaSessionManager.OnActiveSessionsChangedListener} callback 756 * saying otherwise. 757 * 758 * <p>Also sends empty {@link MediaData} to the AVRCP service. 759 */ removeMediaPlayer(int playerId)760 void removeMediaPlayer(int playerId) { 761 if (!haveMediaPlayer(playerId)) { 762 e("Trying to remove nonexistent media player: " + playerId); 763 return; 764 } 765 766 // If we removed the active player, set no player as active until the Media Framework 767 // tells us otherwise 768 if (playerId == mActivePlayerId && playerId != NO_ACTIVE_PLAYER) { 769 getActivePlayer().unregisterCallback(); 770 mActivePlayerId = NO_ACTIVE_PLAYER; 771 List<Metadata> queue = new ArrayList<Metadata>(); 772 queue.add(Util.empty_data()); 773 MediaData newData = new MediaData(Util.empty_data(), null, queue); 774 775 sendMediaUpdate(newData); 776 } 777 778 final MediaPlayerWrapper wrapper = mMediaPlayers.get(playerId); 779 d("Removing media player " + wrapper.getPackageName()); 780 mMediaPlayers.remove(playerId); 781 if (!haveMediaBrowser(playerId)) { 782 d(wrapper.getPackageName() + " doesn't have a browse service. Recycle player ID."); 783 mMediaPlayerIds.remove(wrapper.getPackageName()); 784 } 785 wrapper.cleanup(); 786 } 787 788 /** 789 * Sets {@code playerId} as the new active player and sends the new player's {@link Mediadata} 790 * to the AVRCP service. 791 * 792 * <p>Also informs the {@link #PlayerSettingsManager} about the change of active player. 793 */ setActivePlayer(int playerId)794 void setActivePlayer(int playerId) { 795 if (!haveMediaPlayer(playerId)) { 796 e("Player doesn't exist in list(): " + playerId); 797 return; 798 } 799 800 if (playerId == mActivePlayerId) { 801 Log.w(TAG, getActivePlayer().getPackageName() + " is already the active player"); 802 return; 803 } 804 805 if (mActivePlayerId != NO_ACTIVE_PLAYER) getActivePlayer().unregisterCallback(); 806 807 mActivePlayerId = playerId; 808 getActivePlayer().registerCallback(mMediaPlayerCallback); 809 mActivePlayerLogger.logd( 810 TAG, "setActivePlayer(): setting player to " + getActivePlayer().getPackageName()); 811 812 if (mPlayerSettingsListener != null) { 813 mPlayerSettingsListener.onActivePlayerChanged(getActivePlayer()); 814 } 815 816 // Ensure that metadata is synced on the new player 817 if (!getActivePlayer().isMetadataSynced()) { 818 Log.w(TAG, "setActivePlayer(): Metadata not synced on new player"); 819 return; 820 } 821 822 if (Utils.isPtsTestMode()) { 823 sendFolderUpdate(true, true, false); 824 } 825 826 MediaData data = getActivePlayer().getCurrentMediaData(); 827 if (mAudioPlaybackIsActive) { 828 data.state = mCurrMediaData.state; 829 Log.d(TAG, "setActivePlayer mAudioPlaybackIsActive=true, state=" + data.state); 830 } 831 sendMediaUpdate(data); 832 } 833 834 /** Informs AVRCP service that there has been an update in browsable players. */ sendFolderUpdate( boolean availablePlayers, boolean addressedPlayers, boolean uids)835 private void sendFolderUpdate( 836 boolean availablePlayers, boolean addressedPlayers, boolean uids) { 837 d("sendFolderUpdate"); 838 if (mCallback == null) { 839 return; 840 } 841 842 mCallback.run(availablePlayers, addressedPlayers, uids); 843 } 844 845 /** Indicates that there have been a {@link MediaData} update for the current active player. */ sendMediaUpdate(MediaData data)846 private void sendMediaUpdate(MediaData data) { 847 d("sendMediaUpdate"); 848 if (mCallback == null || data == null) { 849 return; 850 } 851 852 // Always have items in the queue 853 if (data.queue.size() == 0) { 854 Log.i(TAG, "sendMediaUpdate: Creating a one item queue for a player with no queue"); 855 data.queue.add(data.metadata); 856 } 857 858 Log.d(TAG, "sendMediaUpdate state=" + data.state); 859 mCurrMediaData = data; 860 mCallback.run(data); 861 } 862 863 /** 864 * Callback from Media Framework to indicate that the active session changed. 865 * 866 * <p>As sessions can have multiple {@link android.media.session.MediaController}, we add all 867 * the new players, keeping only the highest priority {@link 868 * android.media.session.MediaController} per package name (priority is defined by order in the 869 * list). 870 * 871 * <p>Note: This does not set the current active player, only adds the new {@link 872 * MediaController} to the {@link #mMediaPlayerIds} and {@link mMediaPlayers} maps. 873 * 874 * <p>See {@link #onMediaKeyEventSessionChanged}. 875 */ 876 @VisibleForTesting 877 final MediaSessionManager.OnActiveSessionsChangedListener mActiveSessionsChangedListener = 878 new MediaSessionManager.OnActiveSessionsChangedListener() { 879 @Override 880 public void onActiveSessionsChanged( 881 List<android.media.session.MediaController> newControllers) { 882 synchronized (MediaPlayerList.this) { 883 Log.v( 884 TAG, 885 "onActiveSessionsChanged: number of controllers: " 886 + newControllers.size()); 887 if (newControllers.size() == 0) { 888 if (mPlayerSettingsListener != null) { 889 mPlayerSettingsListener.onActivePlayerChanged(null); 890 } 891 return; 892 } 893 894 // Apps are allowed to have multiple MediaControllers. If an app does have 895 // multiple controllers then newControllers contains them in highest 896 // priority order. Since we only want to keep the highest priority one, 897 // we keep track of which controllers we updated and skip over ones 898 // we've already looked at. 899 HashSet<String> addedPackages = new HashSet<String>(); 900 901 for (int i = 0; i < newControllers.size(); i++) { 902 if ((newControllers.get(i).getFlags() 903 & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) 904 != 0) { 905 Log.d( 906 TAG, 907 "onActiveSessionsChanged: controller: " 908 + newControllers.get(i).getPackageName() 909 + " ignored due to global priority flag"); 910 continue; 911 } 912 Log.d( 913 TAG, 914 "onActiveSessionsChanged: controller: " 915 + newControllers.get(i).getPackageName()); 916 if (addedPackages.contains(newControllers.get(i).getPackageName())) { 917 continue; 918 } 919 920 addedPackages.add(newControllers.get(i).getPackageName()); 921 addMediaPlayer(newControllers.get(i)); 922 } 923 } 924 } 925 }; 926 927 /** 928 * {@link android.content.BroadcastReceiver} to catch intents indicating package add, change and 929 * removal. 930 * 931 * <p>If a package is removed while its corresponding {@link MediaPlayerWrapper} is present in 932 * the {@link #mMediaPlayerIds} and {@link mMediaPlayers} maps, remove it. 933 * 934 * <p>If a package is added or changed, currently nothing is done. Ideally, this should add it 935 * to the {@link #mMediaPlayerIds} and {@link mMediaPlayers} maps. 936 * 937 * <p>See {@link #removeMediaPlayer} and {@link 938 * #addMediaPlayer(android.media.session.MediaController)} 939 */ 940 private final BroadcastReceiver mPackageChangedBroadcastReceiver = 941 new BroadcastReceiver() { 942 @Override 943 public void onReceive(Context context, Intent intent) { 944 String action = intent.getAction(); 945 Log.v(TAG, "mPackageChangedBroadcastReceiver: action: " + action); 946 947 if (action.equals(Intent.ACTION_PACKAGE_REMOVED) 948 || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) { 949 if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return; 950 951 String packageName = intent.getData().getSchemeSpecificPart(); 952 if (haveMediaPlayer(packageName)) { 953 removeMediaPlayer(mMediaPlayerIds.get(packageName)); 954 } 955 } else if (action.equals(Intent.ACTION_PACKAGE_ADDED) 956 || action.equals(Intent.ACTION_PACKAGE_CHANGED)) { 957 String packageName = intent.getData().getSchemeSpecificPart(); 958 if (packageName != null) { 959 Log.d(TAG, "Name of package changed: " + packageName); 960 // TODO (apanicke): Handle either updating or adding the new package. 961 // Check if its browsable and send the UIDS changed to update the 962 // root folder 963 } 964 } 965 } 966 }; 967 968 /** 969 * Retrieves and sends the current {@link MediaData} of the active player (if present) to the 970 * AVRCP service or if there is no active player, sends an empty {@link MediaData}. 971 * 972 * <p>See {@link #sendMediaUpdate}. 973 */ updateMediaForAudioPlayback()974 void updateMediaForAudioPlayback() { 975 MediaData currMediaData = null; 976 PlaybackState currState = null; 977 if (getActivePlayer() == null) { 978 Log.d(TAG, "updateMediaForAudioPlayback: no active player"); 979 PlaybackState.Builder builder = 980 new PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f); 981 List<Metadata> queue = new ArrayList<Metadata>(); 982 queue.add(Util.empty_data()); 983 currMediaData = new MediaData(Util.empty_data(), builder.build(), queue); 984 } else { 985 currMediaData = getActivePlayer().getCurrentMediaData(); 986 currState = currMediaData.state; 987 } 988 989 if (currState != null && currState.getState() == PlaybackState.STATE_PLAYING) { 990 Log.i(TAG, "updateMediaForAudioPlayback: Active player is playing, drop it"); 991 return; 992 } 993 994 if (mAudioPlaybackIsActive) { 995 PlaybackState.Builder builder = 996 new PlaybackState.Builder() 997 .setState( 998 PlaybackState.STATE_PLAYING, 999 currState == null ? 0 : currState.getPosition(), 1000 1.0f); 1001 currMediaData.state = builder.build(); 1002 } 1003 mAudioPlaybackStateLogger.logd( 1004 TAG, "updateMediaForAudioPlayback: update state=" + currMediaData.state); 1005 sendMediaUpdate(currMediaData); 1006 } 1007 1008 /** For testing purposes only, sets the {@link #mAudioPlaybackIsActive} flag. */ 1009 @VisibleForTesting injectAudioPlaybacActive(boolean isActive)1010 void injectAudioPlaybacActive(boolean isActive) { 1011 mAudioPlaybackIsActive = isActive; 1012 updateMediaForAudioPlayback(); 1013 } 1014 1015 /** 1016 * Saves the reference to {@link MediaPlayerSettingsEventListener} to be called when the active 1017 * player changed, so that {@link #PlayerSettingsManager} always has the right player. 1018 */ setPlayerSettingsCallback(MediaPlayerSettingsEventListener listener)1019 void setPlayerSettingsCallback(MediaPlayerSettingsEventListener listener) { 1020 mPlayerSettingsListener = listener; 1021 } 1022 1023 /** 1024 * Listens to playback configurations changes, to set the {@link #mAudioPlaybackIsActive} flag. 1025 */ 1026 private final AudioManager.AudioPlaybackCallback mAudioPlaybackCallback = 1027 new AudioManager.AudioPlaybackCallback() { 1028 @Override 1029 public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) { 1030 if (configs == null) { 1031 return; 1032 } 1033 boolean isActive = false; 1034 AudioPlaybackConfiguration activeConfig = null; 1035 for (AudioPlaybackConfiguration config : configs) { 1036 if (config.isActive() 1037 && (config.getAudioAttributes().getUsage() 1038 == AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) 1039 && (config.getAudioAttributes().getContentType() 1040 == AudioAttributes.CONTENT_TYPE_SPEECH)) { 1041 activeConfig = config; 1042 isActive = true; 1043 } 1044 } 1045 if (isActive != mAudioPlaybackIsActive) { 1046 mAudioPlaybackStateLogger.logd( 1047 TAG, 1048 "onPlaybackConfigChanged: " 1049 + (mAudioPlaybackIsActive ? "Active" : "Non-active") 1050 + " -> " 1051 + (isActive ? "Active" : "Non-active")); 1052 if (isActive) { 1053 mAudioPlaybackStateLogger.logd( 1054 TAG, 1055 "onPlaybackConfigChanged: " + "active config: " + activeConfig); 1056 } 1057 mAudioPlaybackIsActive = isActive; 1058 updateMediaForAudioPlayback(); 1059 } 1060 } 1061 }; 1062 1063 /** 1064 * Callback from {@link MediaPlayerWrapper}. 1065 * 1066 * <p>{@link #mediaUpdatedCallback} listens for {@link #MediaData} changes on the active player. 1067 * 1068 * <p>{@link #sessionUpdatedCallback} is called when the active session is destroyed so we need 1069 * to remove the media player from the {@link #mMediaPlayerIds} and {@link mMediaPlayers} maps. 1070 */ 1071 private final MediaPlayerWrapper.Callback mMediaPlayerCallback = 1072 new MediaPlayerWrapper.Callback() { 1073 @Override 1074 public void mediaUpdatedCallback(MediaData data) { 1075 if (data.metadata == null) { 1076 Log.d(TAG, "mediaUpdatedCallback(): metadata is null"); 1077 return; 1078 } 1079 1080 if (data.state == null) { 1081 Log.w(TAG, "mediaUpdatedCallback(): Tried to update with null state"); 1082 return; 1083 } 1084 1085 if (mAudioPlaybackIsActive 1086 && (data.state.getState() != PlaybackState.STATE_PLAYING)) { 1087 Log.d(TAG, "Some audio playbacks are still active, drop it"); 1088 return; 1089 } 1090 sendMediaUpdate(data); 1091 } 1092 1093 @Override 1094 public void sessionUpdatedCallback(String packageName) { 1095 if (haveMediaPlayer(packageName)) { 1096 Log.d(TAG, "sessionUpdatedCallback(): packageName: " + packageName); 1097 removeMediaPlayer(mMediaPlayerIds.get(packageName)); 1098 } 1099 } 1100 }; 1101 1102 /** 1103 * Listens for Media key events session changes. 1104 * 1105 * <p>The Media session that listens to key events is considered the active session. 1106 * 1107 * <p>This will retrieve the {@link android.media.session.MediaController} for this session with 1108 * the {@code token} provided and set it as the active one. 1109 * 1110 * <p>If the {@link android.media.session.MediaController} flags include the {@link 1111 * MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY}, the session change shall be ignored as this 1112 * flag is used only by Telecom to handle wired headsets key events. 1113 * 1114 * <p>It can happen that {@code token} is null, in such case wecan still check if we have a 1115 * {@link MediaController} corresponding to {@code packageName} and set it as active. 1116 */ 1117 @VisibleForTesting 1118 final MediaSessionManager.OnMediaKeyEventSessionChangedListener 1119 mMediaKeyEventSessionChangedListener = 1120 new MediaSessionManager.OnMediaKeyEventSessionChangedListener() { 1121 @Override 1122 public void onMediaKeyEventSessionChanged( 1123 String packageName, MediaSession.Token token) { 1124 if (mMediaSessionManager == null) { 1125 Log.w( 1126 TAG, 1127 "onMediaKeyEventSessionChanged(): Unexpected callback " 1128 + "from the MediaSessionManager, pkg" 1129 + packageName); 1130 return; 1131 } 1132 if (TextUtils.isEmpty(packageName)) { 1133 return; 1134 } 1135 if (token != null) { 1136 android.media.session.MediaController controller = 1137 new android.media.session.MediaController(mContext, token); 1138 if ((controller.getFlags() 1139 & MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY) 1140 != 0) { 1141 // Skip adding controller for GLOBAL_PRIORITY session. 1142 Log.i( 1143 TAG, 1144 "onMediaKeyEventSessionChanged," 1145 + " ignoring global priority session"); 1146 return; 1147 } 1148 if (!haveMediaPlayer(controller.getPackageName())) { 1149 // Since we have a controller, we can try to to recover by 1150 // adding the 1151 // player and then setting it as active. 1152 Log.w( 1153 TAG, 1154 "onMediaKeyEventSessionChanged(Token): Addressed Player" 1155 + " changed to a player we didn't have a session" 1156 + " for"); 1157 addMediaPlayer(controller); 1158 } 1159 1160 Log.i( 1161 TAG, 1162 "onMediaKeyEventSessionChanged: token=" 1163 + controller.getPackageName()); 1164 setActivePlayer(mMediaPlayerIds.get(controller.getPackageName())); 1165 } else { 1166 if (!haveMediaPlayer(packageName)) { 1167 e( 1168 "onMediaKeyEventSessionChanged(PackageName): Media key" 1169 + " event session changed to a player we don't have" 1170 + " a session for"); 1171 return; 1172 } 1173 1174 Log.i( 1175 TAG, 1176 "onMediaKeyEventSessionChanged: packageName=" 1177 + packageName); 1178 setActivePlayer(mMediaPlayerIds.get(packageName)); 1179 } 1180 } 1181 }; 1182 1183 /** Dumps all players and browsable players currently listed in this class. */ dump(StringBuilder sb)1184 public void dump(StringBuilder sb) { 1185 sb.append("List of MediaControllers: size=" + mMediaPlayers.size() + "\n"); 1186 for (int id : mMediaPlayers.keySet()) { 1187 if (id == mActivePlayerId) { 1188 sb.append("<Active> "); 1189 } 1190 MediaPlayerWrapper player = mMediaPlayers.get(id); 1191 sb.append(" Media Player " + id + ": " + player.getPackageName() + "\n"); 1192 sb.append(player.toString().replaceAll("(?m)^", " ")); 1193 sb.append("\n"); 1194 } 1195 1196 sb.append("List of Browsers: size=" + mBrowsablePlayers.size() + "\n"); 1197 for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { 1198 sb.append(player.toString().replaceAll("(?m)^", " ")); 1199 sb.append("\n"); 1200 } 1201 1202 mActivePlayerLogger.dump(sb); 1203 sb.append("\n"); 1204 mAudioPlaybackStateLogger.dump(sb); 1205 sb.append("\n"); 1206 } 1207 e(String message)1208 private static void e(String message) { 1209 if (sTesting) { 1210 Log.wtf(TAG, message); 1211 } else { 1212 Log.e(TAG, message); 1213 } 1214 } 1215 d(String message)1216 private static void d(String message) { 1217 Log.d(TAG, message); 1218 } 1219 } 1220