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.Utils; 40 import com.android.internal.annotations.VisibleForTesting; 41 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.HashMap; 45 import java.util.HashSet; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.regex.Matcher; 49 import java.util.regex.Pattern; 50 51 /** 52 * This class is directly responsible of maintaining the list of Browsable Players as well as 53 * the list of Addressable Players. This variation of the list doesn't actually list all the 54 * available players for a getAvailableMediaPlayers request. Instead it only reports one media 55 * player with ID=0 and all the other browsable players are folders in the root of that player. 56 * 57 * Changing the directory to a browsable player will allow you to traverse that player as normal. 58 * By only having one root player, we never have to send Addressed Player Changed notifications, 59 * UIDs Changed notifications, or Available Players Changed notifications. 60 * 61 * TODO (apanicke): Add non-browsable players as song items to the root folder. Selecting that 62 * player would effectively cause player switch by sending a play command to that player. 63 */ 64 public class MediaPlayerList { 65 private static final String TAG = "MediaPlayerList"; 66 private static final boolean DEBUG = true; 67 static boolean sTesting = false; 68 69 private static final String PACKAGE_SCHEME = "package"; 70 private static final int NO_ACTIVE_PLAYER = 0; 71 private static final int BLUETOOTH_PLAYER_ID = 0; 72 private static final String BLUETOOTH_PLAYER_NAME = "Bluetooth Player"; 73 private static final int ACTIVE_PLAYER_LOGGER_SIZE = 5; 74 private static final String ACTIVE_PLAYER_LOGGER_TITLE = "Active Player Events"; 75 private static final int AUDIO_PLAYBACK_STATE_LOGGER_SIZE = 15; 76 private static final String AUDIO_PLAYBACK_STATE_LOGGER_TITLE = "Audio Playback State Events"; 77 78 // mediaId's for the now playing list will be in the form of "NowPlayingId[XX]" where [XX] 79 // is the Queue ID for the requested item. 80 private static final String NOW_PLAYING_ID_PATTERN = Util.NOW_PLAYING_PREFIX + "([0-9]*)"; 81 82 // mediaId's for folder browsing will be in the form of [XX][mediaid], where [XX] is a 83 // two digit representation of the player id and [mediaid] is the original media id as a 84 // string. 85 private static final String BROWSE_ID_PATTERN = "\\d\\d.*"; 86 87 private Context mContext; 88 private Looper mLooper; // Thread all media player callbacks and timeouts happen on 89 private PackageManager mPackageManager; 90 private MediaSessionManager mMediaSessionManager; 91 private MediaData mCurrMediaData = null; 92 private final AudioManager mAudioManager; 93 94 private final BTAudioEventLogger mActivePlayerLogger = new BTAudioEventLogger( 95 ACTIVE_PLAYER_LOGGER_SIZE, ACTIVE_PLAYER_LOGGER_TITLE); 96 private final BTAudioEventLogger mAudioPlaybackStateLogger = new BTAudioEventLogger( 97 AUDIO_PLAYBACK_STATE_LOGGER_SIZE, AUDIO_PLAYBACK_STATE_LOGGER_TITLE); 98 99 private Map<Integer, MediaPlayerWrapper> mMediaPlayers = 100 Collections.synchronizedMap(new HashMap<Integer, MediaPlayerWrapper>()); 101 private Map<String, Integer> mMediaPlayerIds = 102 Collections.synchronizedMap(new HashMap<String, Integer>()); 103 private Map<Integer, BrowsedPlayerWrapper> mBrowsablePlayers = 104 Collections.synchronizedMap(new HashMap<Integer, BrowsedPlayerWrapper>()); 105 private int mActivePlayerId = NO_ACTIVE_PLAYER; 106 107 private MediaUpdateCallback mCallback; 108 private boolean mAudioPlaybackIsActive = false; 109 110 private BrowsablePlayerConnector mBrowsablePlayerConnector; 111 112 public interface MediaUpdateCallback { run(MediaData data)113 void run(MediaData data); run(boolean availablePlayers, boolean addressedPlayers, boolean uids)114 void run(boolean availablePlayers, boolean addressedPlayers, boolean uids); 115 } 116 117 public interface GetPlayerRootCallback { run(int playerId, boolean success, String rootId, int numItems)118 void run(int playerId, boolean success, String rootId, int numItems); 119 } 120 121 public interface GetFolderItemsCallback { run(String parentId, List<ListItem> items)122 void run(String parentId, List<ListItem> items); 123 } 124 MediaPlayerList(Looper looper, Context context)125 public MediaPlayerList(Looper looper, Context context) { 126 Log.v(TAG, "Creating MediaPlayerList"); 127 128 mLooper = looper; 129 mContext = context; 130 131 // Register for intents where available players might have changed 132 IntentFilter pkgFilter = new IntentFilter(); 133 pkgFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 134 pkgFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED); 135 pkgFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 136 pkgFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); 137 pkgFilter.addDataScheme(PACKAGE_SCHEME); 138 context.registerReceiver(mPackageChangedBroadcastReceiver, pkgFilter); 139 140 mAudioManager = context.getSystemService(AudioManager.class); 141 mAudioManager.registerAudioPlaybackCallback(mAudioPlaybackCallback, new Handler(mLooper)); 142 143 mMediaSessionManager = context.getSystemService(MediaSessionManager.class); 144 mMediaSessionManager.addOnActiveSessionsChangedListener( 145 mActiveSessionsChangedListener, null, new Handler(looper)); 146 mMediaSessionManager.addOnMediaKeyEventSessionChangedListener( 147 mContext.getMainExecutor(), mMediaKeyEventSessionChangedListener); 148 } 149 constructCurrentPlayers()150 private void constructCurrentPlayers() { 151 // Construct the list of current players 152 d("Initializing list of current media players"); 153 List<android.media.session.MediaController> controllers = 154 mMediaSessionManager.getActiveSessions(null); 155 156 for (android.media.session.MediaController controller : controllers) { 157 addMediaPlayer(controller); 158 } 159 160 // If there were any active players and we don't already have one due to the Media 161 // Framework Callbacks then set the highest priority one to active 162 if (mActivePlayerId == 0 && mMediaPlayers.size() > 0) { 163 String packageName = mMediaSessionManager.getMediaKeyEventSessionPackageName(); 164 if (!TextUtils.isEmpty(packageName) && haveMediaPlayer(packageName)) { 165 Log.i(TAG, "Set active player to MediaKeyEvent session = " + packageName); 166 setActivePlayer(mMediaPlayerIds.get(packageName)); 167 } else { 168 Log.i(TAG, "Set active player to first default"); 169 setActivePlayer(1); 170 } 171 } 172 } 173 init(MediaUpdateCallback callback)174 public void init(MediaUpdateCallback callback) { 175 Log.v(TAG, "Initializing MediaPlayerList"); 176 mCallback = callback; 177 178 if (!SystemProperties.getBoolean("bluetooth.avrcp.browsable_media_player.enabled", true)) { 179 // Allow to disable BrowsablePlayerConnector with systemproperties. 180 // This is useful when for watches because: 181 // 1. It is not a regular use case 182 // 2. Registering to all players is a very loading task 183 184 Log.i(TAG, "init: without Browsable Player"); 185 constructCurrentPlayers(); 186 return; 187 } 188 189 // Build the list of browsable players and afterwards, build the list of media players 190 Intent intent = new Intent(android.service.media.MediaBrowserService.SERVICE_INTERFACE); 191 List<ResolveInfo> playerList = 192 mContext 193 .getApplicationContext() 194 .getPackageManager() 195 .queryIntentServices(intent, PackageManager.MATCH_ALL); 196 197 mBrowsablePlayerConnector = BrowsablePlayerConnector.connectToPlayers(mContext, mLooper, 198 playerList, (List<BrowsedPlayerWrapper> players) -> { 199 Log.i(TAG, "init: Browsable Player list size is " + players.size()); 200 201 // Check to see if the list has been cleaned up before this completed 202 if (mMediaSessionManager == null) { 203 return; 204 } 205 206 for (BrowsedPlayerWrapper wrapper : players) { 207 // Generate new id and add the browsable player 208 if (!havePlayerId(wrapper.getPackageName())) { 209 mMediaPlayerIds.put(wrapper.getPackageName(), getFreeMediaPlayerId()); 210 } 211 212 d("Adding Browser Wrapper for " + wrapper.getPackageName() + " with id " 213 + mMediaPlayerIds.get(wrapper.getPackageName())); 214 215 mBrowsablePlayers.put(mMediaPlayerIds.get(wrapper.getPackageName()), wrapper); 216 217 wrapper.getFolderItems(wrapper.getRootId(), 218 (int status, String mediaId, List<ListItem> results) -> { 219 d("Got the contents for: " + mediaId + " : num results=" 220 + results.size()); 221 }); 222 } 223 224 constructCurrentPlayers(); 225 }); 226 } 227 cleanup()228 public void cleanup() { 229 mContext.unregisterReceiver(mPackageChangedBroadcastReceiver); 230 231 mActivePlayerId = NO_ACTIVE_PLAYER; 232 233 mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveSessionsChangedListener); 234 mMediaSessionManager.removeOnMediaKeyEventSessionChangedListener( 235 mMediaKeyEventSessionChangedListener); 236 mMediaSessionManager = null; 237 238 mAudioManager.unregisterAudioPlaybackCallback(mAudioPlaybackCallback); 239 240 mMediaPlayerIds.clear(); 241 242 for (MediaPlayerWrapper player : mMediaPlayers.values()) { 243 player.cleanup(); 244 } 245 mMediaPlayers.clear(); 246 247 if (mBrowsablePlayerConnector != null) { 248 mBrowsablePlayerConnector.cleanup(); 249 } 250 for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { 251 player.disconnect(); 252 } 253 mBrowsablePlayers.clear(); 254 } 255 getCurrentPlayerId()256 public int getCurrentPlayerId() { 257 return BLUETOOTH_PLAYER_ID; 258 } 259 getFreeMediaPlayerId()260 int getFreeMediaPlayerId() { 261 int id = 1; 262 while (mMediaPlayerIds.containsValue(id)) { 263 id++; 264 } 265 return id; 266 } 267 getActivePlayer()268 public MediaPlayerWrapper getActivePlayer() { 269 return mMediaPlayers.get(mActivePlayerId); 270 } 271 272 // In this case the displayed player is the Bluetooth Player, the number of items is equal 273 // to the number of players. The root ID will always be empty string in this case as well. getPlayerRoot(int playerId, GetPlayerRootCallback cb)274 public void getPlayerRoot(int playerId, GetPlayerRootCallback cb) { 275 /** M: Fix PTS AVRCP/TG/MCN/CB/BI-02-C fail @{ */ 276 if (Utils.isPtsTestMode()) { 277 d("PTS test mode: getPlayerRoot"); 278 BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(BLUETOOTH_PLAYER_ID + 1); 279 String itemId = wrapper.getRootId(); 280 281 wrapper.getFolderItems(itemId, (status, id, results) -> { 282 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { 283 cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", 0); 284 return; 285 } 286 cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", results.size()); 287 }); 288 return; 289 } 290 /** @} */ 291 cb.run(playerId, playerId == BLUETOOTH_PLAYER_ID, "", mBrowsablePlayers.size()); 292 } 293 294 // Return the "Bluetooth Player" as the only player always getMediaPlayerList()295 public List<PlayerInfo> getMediaPlayerList() { 296 PlayerInfo info = new PlayerInfo(); 297 info.id = BLUETOOTH_PLAYER_ID; 298 info.name = BLUETOOTH_PLAYER_NAME; 299 info.browsable = true; 300 if (mBrowsablePlayers.size() == 0) { 301 // Set Bluetooth Player as non-browable if there is not browsable player exist. 302 info.browsable = false; 303 } 304 List<PlayerInfo> ret = new ArrayList<PlayerInfo>(); 305 ret.add(info); 306 307 return ret; 308 } 309 310 @NonNull getCurrentMediaId()311 public String getCurrentMediaId() { 312 final MediaPlayerWrapper player = getActivePlayer(); 313 if (player == null) return ""; 314 315 final PlaybackState state = player.getPlaybackState(); 316 final List<Metadata> queue = player.getCurrentQueue(); 317 318 // Disable the now playing list if the player doesn't have a queue or provide an active 319 // queue ID that can be used to determine the active song in the queue. 320 if (state == null 321 || state.getActiveQueueItemId() == MediaSession.QueueItem.UNKNOWN_ID 322 || queue.size() == 0) { 323 d("getCurrentMediaId: No active queue item Id sending empty mediaId: PlaybackState=" 324 + state); 325 return ""; 326 } 327 328 return Util.NOW_PLAYING_PREFIX + state.getActiveQueueItemId(); 329 } 330 331 @NonNull getCurrentSongInfo()332 public Metadata getCurrentSongInfo() { 333 final MediaPlayerWrapper player = getActivePlayer(); 334 if (player == null) return Util.empty_data(); 335 336 return player.getCurrentMetadata(); 337 } 338 getCurrentPlayStatus()339 public PlaybackState getCurrentPlayStatus() { 340 final MediaPlayerWrapper player = getActivePlayer(); 341 if (player == null) return null; 342 343 PlaybackState state = player.getPlaybackState(); 344 if (mAudioPlaybackIsActive 345 && (state == null || state.getState() != PlaybackState.STATE_PLAYING)) { 346 return new PlaybackState.Builder() 347 .setState(PlaybackState.STATE_PLAYING, 348 state == null ? 0 : state.getPosition(), 349 1.0f) 350 .build(); 351 } 352 return state; 353 } 354 355 @NonNull getNowPlayingList()356 public List<Metadata> getNowPlayingList() { 357 // Only send the current song for the now playing if there is no active song. See 358 // |getCurrentMediaId()| for reasons why there might be no active song. 359 if (getCurrentMediaId().equals("")) { 360 List<Metadata> ret = new ArrayList<Metadata>(); 361 Metadata data = getCurrentSongInfo(); 362 data.mediaId = ""; 363 ret.add(data); 364 return ret; 365 } 366 367 return getActivePlayer().getCurrentQueue(); 368 } 369 playItem(int playerId, boolean nowPlaying, String mediaId)370 public void playItem(int playerId, boolean nowPlaying, String mediaId) { 371 if (nowPlaying) { 372 playNowPlayingItem(mediaId); 373 } else { 374 playFolderItem(mediaId); 375 } 376 } 377 playNowPlayingItem(String mediaId)378 private void playNowPlayingItem(String mediaId) { 379 d("playNowPlayingItem: mediaId=" + mediaId); 380 381 Pattern regex = Pattern.compile(NOW_PLAYING_ID_PATTERN); 382 Matcher m = regex.matcher(mediaId); 383 if (!m.find()) { 384 // This should never happen since we control the media ID's reported 385 Log.wtf(TAG, "playNowPlayingItem: Couldn't match mediaId to pattern: mediaId=" 386 + mediaId); 387 } 388 389 long queueItemId = Long.parseLong(m.group(1)); 390 if (getActivePlayer() != null) { 391 getActivePlayer().playItemFromQueue(queueItemId); 392 } 393 } 394 playFolderItem(String mediaId)395 private void playFolderItem(String mediaId) { 396 d("playFolderItem: mediaId=" + mediaId); 397 398 if (!mediaId.matches(BROWSE_ID_PATTERN)) { 399 // This should never happen since we control the media ID's reported 400 Log.wtf(TAG, "playFolderItem: mediaId didn't match pattern: mediaId=" + mediaId); 401 } 402 403 int playerIndex = Integer.parseInt(mediaId.substring(0, 2)); 404 if (!haveMediaBrowser(playerIndex)) { 405 e("playFolderItem: Do not have the a browsable player with ID " + playerIndex); 406 return; 407 } 408 409 BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(playerIndex); 410 String itemId = mediaId.substring(2); 411 if (TextUtils.isEmpty(itemId)) { 412 itemId = wrapper.getRootId(); 413 if (TextUtils.isEmpty(itemId)) { 414 e("playFolderItem: Failed to start playback with an empty media id."); 415 return; 416 } 417 Log.i(TAG, "playFolderItem: Empty media id, trying with the root id for " 418 + wrapper.getPackageName()); 419 } 420 wrapper.playItem(itemId); 421 } 422 getFolderItemsMediaPlayerList(GetFolderItemsCallback cb)423 void getFolderItemsMediaPlayerList(GetFolderItemsCallback cb) { 424 d("getFolderItemsMediaPlayerList: Sending Media Player list for root directory"); 425 426 ArrayList<ListItem> playerList = new ArrayList<ListItem>(); 427 for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { 428 429 String displayName = Util.getDisplayName(mContext, player.getPackageName()); 430 int id = mMediaPlayerIds.get(player.getPackageName()); 431 432 d("getFolderItemsMediaPlayerList: Adding player " + displayName); 433 Folder playerFolder = new Folder(String.format("%02d", id), false, displayName); 434 playerList.add(new ListItem(playerFolder)); 435 } 436 cb.run("", playerList); 437 return; 438 } 439 getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb)440 public void getFolderItems(int playerId, String mediaId, GetFolderItemsCallback cb) { 441 // The playerId is unused since we always assume the remote device is using the 442 // Bluetooth Player. 443 d("getFolderItems(): playerId=" + playerId + ", mediaId=" + mediaId); 444 /** M: Fix PTS AVRCP/TG/MCN/CB/BI-02-C fail @{ */ 445 if (Utils.isPtsTestMode()) { 446 d("PTS test mode: getFolderItems"); 447 BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(BLUETOOTH_PLAYER_ID + 1); 448 String itemId = mediaId; 449 if (mediaId.equals("")) { 450 itemId = wrapper.getRootId(); 451 } 452 453 wrapper.getFolderItems(itemId, (status, id, results) -> { 454 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { 455 cb.run(mediaId, new ArrayList<ListItem>()); 456 return; 457 } 458 cb.run(mediaId, results); 459 }); 460 return; 461 } 462 /** @} */ 463 464 // The device is requesting the content of the root folder. This folder contains a list of 465 // Browsable Media Players displayed as folders with their contents contained within. 466 if (mediaId.equals("")) { 467 getFolderItemsMediaPlayerList(cb); 468 return; 469 } 470 471 if (!mediaId.matches(BROWSE_ID_PATTERN)) { 472 // This should never happen since we control the media ID's reported 473 Log.wtf(TAG, "getFolderItems: mediaId didn't match pattern: mediaId=" + mediaId); 474 } 475 476 int playerIndex = Integer.parseInt(mediaId.substring(0, 2)); 477 String itemId = mediaId.substring(2); 478 479 // TODO (apanicke): Add timeouts for looking up folder items since media browsers don't 480 // have to respond. 481 if (haveMediaBrowser(playerIndex)) { 482 BrowsedPlayerWrapper wrapper = mBrowsablePlayers.get(playerIndex); 483 if (itemId.equals("")) { 484 Log.i(TAG, "Empty media id, getting the root for " 485 + wrapper.getPackageName()); 486 itemId = wrapper.getRootId(); 487 } 488 489 wrapper.getFolderItems(itemId, (status, id, results) -> { 490 if (status != BrowsedPlayerWrapper.STATUS_SUCCESS) { 491 cb.run(mediaId, new ArrayList<ListItem>()); 492 return; 493 } 494 495 String playerPrefix = String.format("%02d", playerIndex); 496 for (ListItem item : results) { 497 if (item.isFolder) { 498 item.folder.mediaId = playerPrefix.concat(item.folder.mediaId); 499 } else { 500 item.song.mediaId = playerPrefix.concat(item.song.mediaId); 501 } 502 } 503 cb.run(mediaId, results); 504 }); 505 return; 506 } else { 507 cb.run(mediaId, new ArrayList<ListItem>()); 508 } 509 } 510 511 @VisibleForTesting addMediaPlayer(MediaController controller)512 int addMediaPlayer(MediaController controller) { 513 // Each new player has an ID of 1 plus the highest ID. The ID 0 is reserved to signify that 514 // there is no active player. If we already have a browsable player for the package, reuse 515 // that key. 516 String packageName = controller.getPackageName(); 517 if (!havePlayerId(packageName)) { 518 mMediaPlayerIds.put(packageName, getFreeMediaPlayerId()); 519 } 520 521 int playerId = mMediaPlayerIds.get(packageName); 522 523 // If we already have a controller for the package, then update it with this new controller 524 // as the old controller has probably gone stale. 525 if (haveMediaPlayer(playerId)) { 526 d("Already have a controller for the player: " + packageName + ", updating instead"); 527 MediaPlayerWrapper player = mMediaPlayers.get(playerId); 528 player.updateMediaController(controller); 529 530 // If the media controller we updated was the active player check if the media updated 531 if (playerId == mActivePlayerId) { 532 sendMediaUpdate(getActivePlayer().getCurrentMediaData()); 533 } 534 535 return playerId; 536 } 537 538 MediaPlayerWrapper newPlayer = MediaPlayerWrapperFactory.wrap( 539 mContext, 540 controller, 541 mLooper); 542 543 Log.i(TAG, "Adding wrapped media player: " + packageName + " at key: " 544 + mMediaPlayerIds.get(controller.getPackageName())); 545 546 mMediaPlayers.put(playerId, newPlayer); 547 return playerId; 548 } 549 550 // Adds the controller to the MediaPlayerList or updates the controller if we already had 551 // a controller for a package. Returns the new ID of the controller where its added or its 552 // previous value if it already existed. Returns -1 if the controller passed in is invalid addMediaPlayer(android.media.session.MediaController controller)553 int addMediaPlayer(android.media.session.MediaController controller) { 554 if (controller == null) { 555 e("Trying to add a null MediaController"); 556 return -1; 557 } 558 559 return addMediaPlayer(MediaControllerFactory.wrap(controller)); 560 } 561 havePlayerId(String packageName)562 boolean havePlayerId(String packageName) { 563 if (packageName == null) return false; 564 return mMediaPlayerIds.containsKey(packageName); 565 } 566 haveMediaPlayer(String packageName)567 boolean haveMediaPlayer(String packageName) { 568 if (!havePlayerId(packageName)) return false; 569 int playerId = mMediaPlayerIds.get(packageName); 570 return mMediaPlayers.containsKey(playerId); 571 } 572 haveMediaPlayer(int playerId)573 boolean haveMediaPlayer(int playerId) { 574 return mMediaPlayers.containsKey(playerId); 575 } 576 haveMediaBrowser(int playerId)577 boolean haveMediaBrowser(int playerId) { 578 return mBrowsablePlayers.containsKey(playerId); 579 } 580 removeMediaPlayer(int playerId)581 void removeMediaPlayer(int playerId) { 582 if (!haveMediaPlayer(playerId)) { 583 e("Trying to remove nonexistent media player: " + playerId); 584 return; 585 } 586 587 // If we removed the active player, set no player as active until the Media Framework 588 // tells us otherwise 589 if (playerId == mActivePlayerId && playerId != NO_ACTIVE_PLAYER) { 590 getActivePlayer().unregisterCallback(); 591 mActivePlayerId = NO_ACTIVE_PLAYER; 592 List<Metadata> queue = new ArrayList<Metadata>(); 593 queue.add(Util.empty_data()); 594 MediaData newData = new MediaData( 595 Util.empty_data(), 596 null, 597 queue 598 ); 599 600 sendMediaUpdate(newData); 601 } 602 603 final MediaPlayerWrapper wrapper = mMediaPlayers.get(playerId); 604 d("Removing media player " + wrapper.getPackageName()); 605 mMediaPlayers.remove(playerId); 606 if (!haveMediaBrowser(playerId)) { 607 d(wrapper.getPackageName() + " doesn't have a browse service. Recycle player ID."); 608 mMediaPlayerIds.remove(wrapper.getPackageName()); 609 } 610 wrapper.cleanup(); 611 } 612 setActivePlayer(int playerId)613 void setActivePlayer(int playerId) { 614 if (!haveMediaPlayer(playerId)) { 615 e("Player doesn't exist in list(): " + playerId); 616 return; 617 } 618 619 if (playerId == mActivePlayerId) { 620 Log.w(TAG, getActivePlayer().getPackageName() + " is already the active player"); 621 return; 622 } 623 624 if (mActivePlayerId != NO_ACTIVE_PLAYER) getActivePlayer().unregisterCallback(); 625 626 mActivePlayerId = playerId; 627 getActivePlayer().registerCallback(mMediaPlayerCallback); 628 mActivePlayerLogger.logd(TAG, "setActivePlayer(): setting player to " 629 + getActivePlayer().getPackageName()); 630 631 // Ensure that metadata is synced on the new player 632 if (!getActivePlayer().isMetadataSynced()) { 633 Log.w(TAG, "setActivePlayer(): Metadata not synced on new player"); 634 return; 635 } 636 637 if (Utils.isPtsTestMode()) { 638 sendFolderUpdate(true, true, false); 639 } 640 641 MediaData data = getActivePlayer().getCurrentMediaData(); 642 if (mAudioPlaybackIsActive) { 643 data.state = mCurrMediaData.state; 644 Log.d(TAG, "setActivePlayer mAudioPlaybackIsActive=true, state=" + data.state); 645 } 646 sendMediaUpdate(data); 647 } 648 649 // TODO (apanicke): Add logging for media key events in dumpsys sendMediaKeyEvent(int key, boolean pushed)650 public void sendMediaKeyEvent(int key, boolean pushed) { 651 d("sendMediaKeyEvent: key=" + key + " pushed=" + pushed); 652 int action = pushed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP; 653 KeyEvent event = new KeyEvent(action, AvrcpPassthrough.toKeyCode(key)); 654 mAudioManager.dispatchMediaKeyEvent(event); 655 } 656 sendFolderUpdate(boolean availablePlayers, boolean addressedPlayers, boolean uids)657 private void sendFolderUpdate(boolean availablePlayers, boolean addressedPlayers, 658 boolean uids) { 659 d("sendFolderUpdate"); 660 if (mCallback == null) { 661 return; 662 } 663 664 mCallback.run(availablePlayers, addressedPlayers, uids); 665 } 666 sendMediaUpdate(MediaData data)667 private void sendMediaUpdate(MediaData data) { 668 d("sendMediaUpdate"); 669 if (mCallback == null) { 670 return; 671 } 672 673 // Always have items in the queue 674 if (data.queue.size() == 0) { 675 Log.i(TAG, "sendMediaUpdate: Creating a one item queue for a player with no queue"); 676 data.queue.add(data.metadata); 677 } 678 679 Log.d(TAG, "sendMediaUpdate state=" + data.state); 680 mCurrMediaData = data; 681 mCallback.run(data); 682 } 683 684 private final MediaSessionManager.OnActiveSessionsChangedListener 685 mActiveSessionsChangedListener = 686 new MediaSessionManager.OnActiveSessionsChangedListener() { 687 @Override 688 public void onActiveSessionsChanged( 689 List<android.media.session.MediaController> newControllers) { 690 synchronized (MediaPlayerList.this) { 691 Log.v(TAG, "onActiveSessionsChanged: number of controllers: " 692 + newControllers.size()); 693 if (newControllers.size() == 0) return; 694 695 // Apps are allowed to have multiple MediaControllers. If an app does have 696 // multiple controllers then newControllers contains them in highest 697 // priority order. Since we only want to keep the highest priority one, 698 // we keep track of which controllers we updated and skip over ones 699 // we've already looked at. 700 HashSet<String> addedPackages = new HashSet<String>(); 701 702 for (int i = 0; i < newControllers.size(); i++) { 703 Log.d(TAG, "onActiveSessionsChanged: controller: " 704 + newControllers.get(i).getPackageName()); 705 if (addedPackages.contains(newControllers.get(i).getPackageName())) { 706 continue; 707 } 708 709 addedPackages.add(newControllers.get(i).getPackageName()); 710 addMediaPlayer(newControllers.get(i)); 711 } 712 } 713 } 714 }; 715 716 // TODO (apanicke): Write a test that tests uninstalling the active session 717 private final BroadcastReceiver mPackageChangedBroadcastReceiver = new BroadcastReceiver() { 718 @Override 719 public void onReceive(Context context, Intent intent) { 720 String action = intent.getAction(); 721 Log.v(TAG, "mPackageChangedBroadcastReceiver: action: " + action); 722 723 if (action.equals(Intent.ACTION_PACKAGE_REMOVED) 724 || action.equals(Intent.ACTION_PACKAGE_DATA_CLEARED)) { 725 if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return; 726 727 String packageName = intent.getData().getSchemeSpecificPart(); 728 if (haveMediaPlayer(packageName)) { 729 removeMediaPlayer(mMediaPlayerIds.get(packageName)); 730 } 731 } else if (action.equals(Intent.ACTION_PACKAGE_ADDED) 732 || action.equals(Intent.ACTION_PACKAGE_CHANGED)) { 733 String packageName = intent.getData().getSchemeSpecificPart(); 734 if (packageName != null) { 735 if (DEBUG) Log.d(TAG, "Name of package changed: " + packageName); 736 // TODO (apanicke): Handle either updating or adding the new package. 737 // Check if its browsable and send the UIDS changed to update the 738 // root folder 739 } 740 } 741 } 742 }; 743 updateMediaForAudioPlayback()744 void updateMediaForAudioPlayback() { 745 MediaData currMediaData = null; 746 PlaybackState currState = null; 747 if (getActivePlayer() == null) { 748 Log.d(TAG, "updateMediaForAudioPlayback: no active player"); 749 PlaybackState.Builder builder = new PlaybackState.Builder() 750 .setState(PlaybackState.STATE_STOPPED, 0L, 0L); 751 List<Metadata> queue = new ArrayList<Metadata>(); 752 queue.add(Util.empty_data()); 753 currMediaData = new MediaData( 754 Util.empty_data(), 755 builder.build(), 756 queue 757 ); 758 } else { 759 currMediaData = getActivePlayer().getCurrentMediaData(); 760 currState = currMediaData.state; 761 } 762 763 if (currState != null 764 && currState.getState() == PlaybackState.STATE_PLAYING) { 765 Log.i(TAG, "updateMediaForAudioPlayback: Active player is playing, drop it"); 766 return; 767 } 768 769 if (mAudioPlaybackIsActive) { 770 PlaybackState.Builder builder = new PlaybackState.Builder() 771 .setState(PlaybackState.STATE_PLAYING, 772 currState == null ? 0 : currState.getPosition(), 773 1.0f); 774 currMediaData.state = builder.build(); 775 } 776 mAudioPlaybackStateLogger.logd(TAG, "updateMediaForAudioPlayback: update state=" 777 + currMediaData.state); 778 sendMediaUpdate(currMediaData); 779 } 780 781 @VisibleForTesting injectAudioPlaybacActive(boolean isActive)782 void injectAudioPlaybacActive(boolean isActive) { 783 mAudioPlaybackIsActive = isActive; 784 updateMediaForAudioPlayback(); 785 } 786 787 private final AudioManager.AudioPlaybackCallback mAudioPlaybackCallback = 788 new AudioManager.AudioPlaybackCallback() { 789 @Override 790 public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) { 791 if (configs == null) { 792 return; 793 } 794 boolean isActive = false; 795 AudioPlaybackConfiguration activeConfig = null; 796 for (AudioPlaybackConfiguration config : configs) { 797 if (config.isActive() && (config.getAudioAttributes().getUsage() 798 == AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) 799 && (config.getAudioAttributes().getContentType() 800 == AudioAttributes.CONTENT_TYPE_SPEECH)) { 801 activeConfig = config; 802 isActive = true; 803 } 804 } 805 if (isActive != mAudioPlaybackIsActive) { 806 mAudioPlaybackStateLogger.logd(DEBUG, TAG, "onPlaybackConfigChanged: " 807 + (mAudioPlaybackIsActive ? "Active" : "Non-active") + " -> " 808 + (isActive ? "Active" : "Non-active")); 809 if (isActive) { 810 mAudioPlaybackStateLogger.logd(DEBUG, TAG, "onPlaybackConfigChanged: " 811 + "active config: " + activeConfig); 812 } 813 mAudioPlaybackIsActive = isActive; 814 updateMediaForAudioPlayback(); 815 } 816 } 817 }; 818 819 private final MediaPlayerWrapper.Callback mMediaPlayerCallback = 820 new MediaPlayerWrapper.Callback() { 821 @Override 822 public void mediaUpdatedCallback(MediaData data) { 823 if (data.metadata == null) { 824 Log.d(TAG, "mediaUpdatedCallback(): metadata is null"); 825 return; 826 } 827 828 if (data.state == null) { 829 Log.w(TAG, "mediaUpdatedCallback(): Tried to update with null state"); 830 return; 831 } 832 833 if (mAudioPlaybackIsActive && (data.state.getState() != PlaybackState.STATE_PLAYING)) { 834 Log.d(TAG, "Some audio playbacks are still active, drop it"); 835 return; 836 } 837 sendMediaUpdate(data); 838 } 839 840 @Override 841 public void sessionUpdatedCallback(String packageName) { 842 if (haveMediaPlayer(packageName)) { 843 Log.d(TAG, "sessionUpdatedCallback(): packageName: " + packageName); 844 removeMediaPlayer(mMediaPlayerIds.get(packageName)); 845 } 846 } 847 }; 848 849 private final MediaSessionManager.OnMediaKeyEventSessionChangedListener 850 mMediaKeyEventSessionChangedListener = 851 new MediaSessionManager.OnMediaKeyEventSessionChangedListener() { 852 @Override 853 public void onMediaKeyEventSessionChanged(String packageName, 854 MediaSession.Token token) { 855 if (mMediaSessionManager == null) { 856 Log.w(TAG, "onMediaKeyEventSessionChanged(): Unexpected callback " 857 + "from the MediaSessionManager, pkg" + packageName + ", token=" 858 + token); 859 return; 860 } 861 if (TextUtils.isEmpty(packageName)) { 862 return; 863 } 864 if (token != null) { 865 android.media.session.MediaController controller = 866 new android.media.session.MediaController(mContext, token); 867 if (!haveMediaPlayer(controller.getPackageName())) { 868 // Since we have a controller, we can try to to recover by adding the 869 // player and then setting it as active. 870 Log.w(TAG, "onMediaKeyEventSessionChanged(Token): Addressed Player " 871 + "changed to a player we didn't have a session for"); 872 addMediaPlayer(controller); 873 } 874 875 Log.i(TAG, "onMediaKeyEventSessionChanged: token=" 876 + controller.getPackageName()); 877 setActivePlayer(mMediaPlayerIds.get(controller.getPackageName())); 878 } else { 879 if (!haveMediaPlayer(packageName)) { 880 e("onMediaKeyEventSessionChanged(PackageName): Media key event session " 881 + "changed to a player we don't have a session for"); 882 return; 883 } 884 885 Log.i(TAG, "onMediaKeyEventSessionChanged: packageName=" + packageName); 886 setActivePlayer(mMediaPlayerIds.get(packageName)); 887 } 888 } 889 }; 890 891 dump(StringBuilder sb)892 public void dump(StringBuilder sb) { 893 sb.append("List of MediaControllers: size=" + mMediaPlayers.size() + "\n"); 894 for (int id : mMediaPlayers.keySet()) { 895 if (id == mActivePlayerId) { 896 sb.append("<Active> "); 897 } 898 MediaPlayerWrapper player = mMediaPlayers.get(id); 899 sb.append(" Media Player " + id + ": " + player.getPackageName() + "\n"); 900 sb.append(player.toString().replaceAll("(?m)^", " ")); 901 sb.append("\n"); 902 } 903 904 sb.append("List of Browsers: size=" + mBrowsablePlayers.size() + "\n"); 905 for (BrowsedPlayerWrapper player : mBrowsablePlayers.values()) { 906 sb.append(player.toString().replaceAll("(?m)^", " ")); 907 sb.append("\n"); 908 } 909 910 mActivePlayerLogger.dump(sb); 911 sb.append("\n"); 912 mAudioPlaybackStateLogger.dump(sb); 913 sb.append("\n"); 914 // TODO (apanicke): Add last sent data 915 } 916 e(String message)917 private static void e(String message) { 918 if (sTesting) { 919 Log.wtf(TAG, message); 920 } else { 921 Log.e(TAG, message); 922 } 923 } 924 d(String message)925 private static void d(String message) { 926 if (DEBUG) { 927 Log.d(TAG, message); 928 } 929 } 930 } 931