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