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