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