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 package com.android.settingslib.media; 17 18 import static android.media.MediaRoute2Info.TYPE_BLE_HEADSET; 19 import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; 20 import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; 21 import static android.media.MediaRoute2Info.TYPE_DOCK; 22 import static android.media.MediaRoute2Info.TYPE_GROUP; 23 import static android.media.MediaRoute2Info.TYPE_HDMI; 24 import static android.media.MediaRoute2Info.TYPE_HEARING_AID; 25 import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER; 26 import static android.media.MediaRoute2Info.TYPE_REMOTE_CAR; 27 import static android.media.MediaRoute2Info.TYPE_REMOTE_COMPUTER; 28 import static android.media.MediaRoute2Info.TYPE_REMOTE_GAME_CONSOLE; 29 import static android.media.MediaRoute2Info.TYPE_REMOTE_SMARTPHONE; 30 import static android.media.MediaRoute2Info.TYPE_REMOTE_SMARTWATCH; 31 import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; 32 import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET; 33 import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET_DOCKED; 34 import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; 35 import static android.media.MediaRoute2Info.TYPE_UNKNOWN; 36 import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY; 37 import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; 38 import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; 39 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; 40 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; 41 42 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED; 43 44 import android.annotation.Nullable; 45 import android.annotation.TargetApi; 46 import android.app.Notification; 47 import android.bluetooth.BluetoothAdapter; 48 import android.bluetooth.BluetoothDevice; 49 import android.content.ComponentName; 50 import android.content.Context; 51 import android.media.MediaRoute2Info; 52 import android.media.MediaRouter2Manager; 53 import android.media.RouteListingPreference; 54 import android.media.RoutingSessionInfo; 55 import android.os.Build; 56 import android.text.TextUtils; 57 import android.util.Log; 58 59 import androidx.annotation.DoNotInline; 60 import androidx.annotation.NonNull; 61 import androidx.annotation.RequiresApi; 62 63 import com.android.internal.annotations.VisibleForTesting; 64 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 65 import com.android.settingslib.bluetooth.LocalBluetoothManager; 66 67 import java.util.ArrayList; 68 import java.util.Collections; 69 import java.util.HashSet; 70 import java.util.List; 71 import java.util.Map; 72 import java.util.Set; 73 import java.util.concurrent.ConcurrentHashMap; 74 import java.util.concurrent.Executor; 75 import java.util.concurrent.Executors; 76 import java.util.stream.Collectors; 77 78 /** 79 * InfoMediaManager provide interface to get InfoMediaDevice list. 80 */ 81 @RequiresApi(Build.VERSION_CODES.R) 82 public class InfoMediaManager extends MediaManager { 83 84 private static final String TAG = "InfoMediaManager"; 85 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 86 @VisibleForTesting 87 final RouterManagerCallback mMediaRouterCallback = new RouterManagerCallback(); 88 @VisibleForTesting 89 final Executor mExecutor = Executors.newSingleThreadExecutor(); 90 @VisibleForTesting 91 MediaRouter2Manager mRouterManager; 92 @VisibleForTesting 93 String mPackageName; 94 boolean mIsScanning = false; 95 96 private MediaDevice mCurrentConnectedDevice; 97 private LocalBluetoothManager mBluetoothManager; 98 private final Map<String, RouteListingPreference.Item> mPreferenceItemMap = 99 new ConcurrentHashMap<>(); 100 InfoMediaManager(Context context, String packageName, Notification notification, LocalBluetoothManager localBluetoothManager)101 public InfoMediaManager(Context context, String packageName, Notification notification, 102 LocalBluetoothManager localBluetoothManager) { 103 super(context, notification); 104 105 mRouterManager = MediaRouter2Manager.getInstance(context); 106 mBluetoothManager = localBluetoothManager; 107 if (!TextUtils.isEmpty(packageName)) { 108 mPackageName = packageName; 109 } 110 } 111 112 @Override startScan()113 public void startScan() { 114 if (!mIsScanning) { 115 mMediaDevices.clear(); 116 mRouterManager.registerCallback(mExecutor, mMediaRouterCallback); 117 mRouterManager.registerScanRequest(); 118 mIsScanning = true; 119 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 120 && !TextUtils.isEmpty(mPackageName)) { 121 RouteListingPreference routeListingPreference = 122 mRouterManager.getRouteListingPreference(mPackageName); 123 Api34Impl.onRouteListingPreferenceUpdated(routeListingPreference, 124 mPreferenceItemMap); 125 } 126 refreshDevices(); 127 } 128 } 129 130 @Override stopScan()131 public void stopScan() { 132 if (mIsScanning) { 133 mRouterManager.unregisterCallback(mMediaRouterCallback); 134 mRouterManager.unregisterScanRequest(); 135 mIsScanning = false; 136 } 137 } 138 139 /** 140 * Get current device that played media. 141 * @return MediaDevice 142 */ getCurrentConnectedDevice()143 MediaDevice getCurrentConnectedDevice() { 144 return mCurrentConnectedDevice; 145 } 146 147 /** 148 * Transfer MediaDevice for media without package name. 149 */ connectDeviceWithoutPackageName(MediaDevice device)150 boolean connectDeviceWithoutPackageName(MediaDevice device) { 151 boolean isConnected = false; 152 final RoutingSessionInfo info = mRouterManager.getSystemRoutingSession(null); 153 if (info != null) { 154 mRouterManager.transfer(info, device.mRouteInfo); 155 isConnected = true; 156 } 157 return isConnected; 158 } 159 160 /** 161 * Add a MediaDevice to let it play current media. 162 * 163 * @param device MediaDevice 164 * @return If add device successful return {@code true}, otherwise return {@code false} 165 */ addDeviceToPlayMedia(MediaDevice device)166 boolean addDeviceToPlayMedia(MediaDevice device) { 167 if (TextUtils.isEmpty(mPackageName)) { 168 Log.w(TAG, "addDeviceToPlayMedia() package name is null or empty!"); 169 return false; 170 } 171 172 final RoutingSessionInfo info = getRoutingSessionInfo(); 173 if (info != null && info.getSelectableRoutes().contains(device.mRouteInfo.getId())) { 174 mRouterManager.selectRoute(info, device.mRouteInfo); 175 return true; 176 } 177 178 Log.w(TAG, "addDeviceToPlayMedia() Ignoring selecting a non-selectable device : " 179 + device.getName()); 180 181 return false; 182 } 183 getRoutingSessionInfo()184 private RoutingSessionInfo getRoutingSessionInfo() { 185 return getRoutingSessionInfo(mPackageName); 186 } 187 getRoutingSessionInfo(String packageName)188 private RoutingSessionInfo getRoutingSessionInfo(String packageName) { 189 final List<RoutingSessionInfo> sessionInfos = 190 mRouterManager.getRoutingSessions(packageName); 191 192 if (sessionInfos == null || sessionInfos.isEmpty()) { 193 return null; 194 } 195 return sessionInfos.get(sessionInfos.size() - 1); 196 } 197 isRoutingSessionAvailableForVolumeControl()198 boolean isRoutingSessionAvailableForVolumeControl() { 199 List<RoutingSessionInfo> sessions = 200 mRouterManager.getRoutingSessions(mPackageName); 201 202 for (RoutingSessionInfo session : sessions) { 203 if (!session.isSystemSession() 204 && session.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { 205 return true; 206 } 207 } 208 209 Log.d(TAG, "No routing session for " + mPackageName); 210 return false; 211 } 212 preferRouteListingOrdering()213 boolean preferRouteListingOrdering() { 214 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 215 && Api34Impl.preferRouteListingOrdering(mRouterManager, mPackageName); 216 } 217 218 @Nullable getLinkedItemComponentName()219 ComponentName getLinkedItemComponentName() { 220 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 221 return null; 222 } 223 return Api34Impl.getLinkedItemComponentName(mRouterManager, mPackageName); 224 } 225 226 /** 227 * Remove a {@code device} from current media. 228 * 229 * @param device MediaDevice 230 * @return If device stop successful return {@code true}, otherwise return {@code false} 231 */ removeDeviceFromPlayMedia(MediaDevice device)232 boolean removeDeviceFromPlayMedia(MediaDevice device) { 233 if (TextUtils.isEmpty(mPackageName)) { 234 Log.w(TAG, "removeDeviceFromMedia() package name is null or empty!"); 235 return false; 236 } 237 238 final RoutingSessionInfo info = getRoutingSessionInfo(); 239 if (info != null && info.getSelectedRoutes().contains(device.mRouteInfo.getId())) { 240 mRouterManager.deselectRoute(info, device.mRouteInfo); 241 return true; 242 } 243 244 Log.w(TAG, "removeDeviceFromMedia() Ignoring deselecting a non-deselectable device : " 245 + device.getName()); 246 247 return false; 248 } 249 250 /** 251 * Release session to stop playing media on MediaDevice. 252 */ releaseSession()253 boolean releaseSession() { 254 if (TextUtils.isEmpty(mPackageName)) { 255 Log.w(TAG, "releaseSession() package name is null or empty!"); 256 return false; 257 } 258 259 final RoutingSessionInfo sessionInfo = getRoutingSessionInfo(); 260 261 if (sessionInfo != null) { 262 mRouterManager.releaseSession(sessionInfo); 263 return true; 264 } 265 266 Log.w(TAG, "releaseSession() Ignoring release session : " + mPackageName); 267 268 return false; 269 } 270 271 /** 272 * Get the MediaDevice list that can be added to current media. 273 * 274 * @return list of MediaDevice 275 */ getSelectableMediaDevice()276 List<MediaDevice> getSelectableMediaDevice() { 277 final List<MediaDevice> deviceList = new ArrayList<>(); 278 if (TextUtils.isEmpty(mPackageName)) { 279 Log.w(TAG, "getSelectableMediaDevice() package name is null or empty!"); 280 return deviceList; 281 } 282 283 final RoutingSessionInfo info = getRoutingSessionInfo(); 284 if (info != null) { 285 for (MediaRoute2Info route : mRouterManager.getSelectableRoutes(info)) { 286 deviceList.add(new InfoMediaDevice(mContext, mRouterManager, 287 route, mPackageName, mPreferenceItemMap.get(route.getId()))); 288 } 289 return deviceList; 290 } 291 292 Log.w(TAG, "getSelectableMediaDevice() cannot found selectable MediaDevice from : " 293 + mPackageName); 294 295 return deviceList; 296 } 297 298 /** 299 * Get the MediaDevice list that can be removed from current media session. 300 * 301 * @return list of MediaDevice 302 */ getDeselectableMediaDevice()303 List<MediaDevice> getDeselectableMediaDevice() { 304 final List<MediaDevice> deviceList = new ArrayList<>(); 305 if (TextUtils.isEmpty(mPackageName)) { 306 Log.d(TAG, "getDeselectableMediaDevice() package name is null or empty!"); 307 return deviceList; 308 } 309 310 final RoutingSessionInfo info = getRoutingSessionInfo(); 311 if (info != null) { 312 for (MediaRoute2Info route : mRouterManager.getDeselectableRoutes(info)) { 313 deviceList.add(new InfoMediaDevice(mContext, mRouterManager, 314 route, mPackageName, mPreferenceItemMap.get(route.getId()))); 315 Log.d(TAG, route.getName() + " is deselectable for " + mPackageName); 316 } 317 return deviceList; 318 } 319 Log.d(TAG, "getDeselectableMediaDevice() cannot found deselectable MediaDevice from : " 320 + mPackageName); 321 322 return deviceList; 323 } 324 325 /** 326 * Get the MediaDevice list that has been selected to current media. 327 * 328 * @return list of MediaDevice 329 */ getSelectedMediaDevice()330 List<MediaDevice> getSelectedMediaDevice() { 331 final List<MediaDevice> deviceList = new ArrayList<>(); 332 if (TextUtils.isEmpty(mPackageName)) { 333 Log.w(TAG, "getSelectedMediaDevice() package name is null or empty!"); 334 return deviceList; 335 } 336 337 final RoutingSessionInfo info = getRoutingSessionInfo(); 338 if (info != null) { 339 for (MediaRoute2Info route : mRouterManager.getSelectedRoutes(info)) { 340 deviceList.add(new InfoMediaDevice(mContext, mRouterManager, 341 route, mPackageName, mPreferenceItemMap.get(route.getId()))); 342 } 343 return deviceList; 344 } 345 346 Log.w(TAG, "getSelectedMediaDevice() cannot found selectable MediaDevice from : " 347 + mPackageName); 348 349 return deviceList; 350 } 351 adjustSessionVolume(RoutingSessionInfo info, int volume)352 void adjustSessionVolume(RoutingSessionInfo info, int volume) { 353 if (info == null) { 354 Log.w(TAG, "Unable to adjust session volume. RoutingSessionInfo is empty"); 355 return; 356 } 357 358 mRouterManager.setSessionVolume(info, volume); 359 } 360 361 /** 362 * Adjust the volume of {@link android.media.RoutingSessionInfo}. 363 * 364 * @param volume the value of volume 365 */ adjustSessionVolume(int volume)366 void adjustSessionVolume(int volume) { 367 if (TextUtils.isEmpty(mPackageName)) { 368 Log.w(TAG, "adjustSessionVolume() package name is null or empty!"); 369 return; 370 } 371 372 final RoutingSessionInfo info = getRoutingSessionInfo(); 373 if (info != null) { 374 Log.d(TAG, "adjustSessionVolume() adjust volume : " + volume + ", with : " 375 + mPackageName); 376 mRouterManager.setSessionVolume(info, volume); 377 return; 378 } 379 380 Log.w(TAG, "adjustSessionVolume() can't found corresponding RoutingSession with : " 381 + mPackageName); 382 } 383 384 /** 385 * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}. 386 * 387 * @return maximum volume of the session, and return -1 if not found. 388 */ getSessionVolumeMax()389 public int getSessionVolumeMax() { 390 if (TextUtils.isEmpty(mPackageName)) { 391 Log.w(TAG, "getSessionVolumeMax() package name is null or empty!"); 392 return -1; 393 } 394 395 final RoutingSessionInfo info = getRoutingSessionInfo(); 396 if (info != null) { 397 return info.getVolumeMax(); 398 } 399 400 Log.w(TAG, "getSessionVolumeMax() can't found corresponding RoutingSession with : " 401 + mPackageName); 402 return -1; 403 } 404 405 /** 406 * Gets the current volume of the {@link android.media.RoutingSessionInfo}. 407 * 408 * @return current volume of the session, and return -1 if not found. 409 */ getSessionVolume()410 public int getSessionVolume() { 411 if (TextUtils.isEmpty(mPackageName)) { 412 Log.w(TAG, "getSessionVolume() package name is null or empty!"); 413 return -1; 414 } 415 416 final RoutingSessionInfo info = getRoutingSessionInfo(); 417 if (info != null) { 418 return info.getVolume(); 419 } 420 421 Log.w(TAG, "getSessionVolume() can't found corresponding RoutingSession with : " 422 + mPackageName); 423 return -1; 424 } 425 getSessionName()426 CharSequence getSessionName() { 427 if (TextUtils.isEmpty(mPackageName)) { 428 Log.w(TAG, "Unable to get session name. The package name is null or empty!"); 429 return null; 430 } 431 432 final RoutingSessionInfo info = getRoutingSessionInfo(); 433 if (info != null) { 434 return info.getName(); 435 } 436 437 Log.w(TAG, "Unable to get session name for package: " + mPackageName); 438 return null; 439 } 440 shouldDisableMediaOutput(String packageName)441 boolean shouldDisableMediaOutput(String packageName) { 442 if (TextUtils.isEmpty(packageName)) { 443 Log.w(TAG, "shouldDisableMediaOutput() package name is null or empty!"); 444 return true; 445 } 446 447 // Disable when there is no transferable route 448 return mRouterManager.getTransferableRoutes(packageName).isEmpty(); 449 } 450 451 @TargetApi(Build.VERSION_CODES.R) shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo)452 boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) { 453 return sessionInfo.isSystemSession() // System sessions are not remote 454 || sessionInfo.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED; 455 } 456 refreshDevices()457 private synchronized void refreshDevices() { 458 mMediaDevices.clear(); 459 mCurrentConnectedDevice = null; 460 if (TextUtils.isEmpty(mPackageName)) { 461 buildAllRoutes(); 462 } else { 463 buildAvailableRoutes(); 464 } 465 dispatchDeviceListAdded(); 466 } 467 468 // MediaRoute2Info.getType was made public on API 34, but exists since API 30. 469 @SuppressWarnings("NewApi") buildAllRoutes()470 private void buildAllRoutes() { 471 for (MediaRoute2Info route : mRouterManager.getAllRoutes()) { 472 if (DEBUG) { 473 Log.d(TAG, "buildAllRoutes() route : " + route.getName() + ", volume : " 474 + route.getVolume() + ", type : " + route.getType()); 475 } 476 if (route.isSystemRoute()) { 477 addMediaDevice(route); 478 } 479 } 480 } 481 getActiveMediaSession()482 List<RoutingSessionInfo> getActiveMediaSession() { 483 List<RoutingSessionInfo> infos = new ArrayList<>(); 484 infos.add(mRouterManager.getSystemRoutingSession(null)); 485 infos.addAll(mRouterManager.getRemoteSessions()); 486 return infos; 487 } 488 489 // MediaRoute2Info.getType was made public on API 34, but exists since API 30. 490 @SuppressWarnings("NewApi") buildAvailableRoutes()491 private synchronized void buildAvailableRoutes() { 492 for (MediaRoute2Info route : getAvailableRoutes(mPackageName)) { 493 if (DEBUG) { 494 Log.d(TAG, "buildAvailableRoutes() route : " + route.getName() + ", volume : " 495 + route.getVolume() + ", type : " + route.getType()); 496 } 497 addMediaDevice(route); 498 } 499 } 500 getAvailableRoutes(String packageName)501 private synchronized List<MediaRoute2Info> getAvailableRoutes(String packageName) { 502 List<MediaRoute2Info> infos = new ArrayList<>(); 503 RoutingSessionInfo routingSessionInfo = getRoutingSessionInfo(packageName); 504 List<MediaRoute2Info> selectedRouteInfos = new ArrayList<>(); 505 if (routingSessionInfo != null) { 506 selectedRouteInfos = mRouterManager.getSelectedRoutes(routingSessionInfo); 507 infos.addAll(selectedRouteInfos); 508 infos.addAll(mRouterManager.getSelectableRoutes(routingSessionInfo)); 509 } 510 final List<MediaRoute2Info> transferableRoutes = 511 mRouterManager.getTransferableRoutes(packageName); 512 for (MediaRoute2Info transferableRoute : transferableRoutes) { 513 boolean alreadyAdded = false; 514 for (MediaRoute2Info mediaRoute2Info : infos) { 515 if (TextUtils.equals(transferableRoute.getId(), mediaRoute2Info.getId())) { 516 alreadyAdded = true; 517 break; 518 } 519 } 520 if (!alreadyAdded) { 521 infos.add(transferableRoute); 522 } 523 } 524 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 525 && !TextUtils.isEmpty(mPackageName)) { 526 RouteListingPreference routeListingPreference = 527 mRouterManager.getRouteListingPreference(mPackageName); 528 if (routeListingPreference != null) { 529 final List<RouteListingPreference.Item> preferenceRouteListing = 530 Api34Impl.composePreferenceRouteListing( 531 routeListingPreference); 532 infos = Api34Impl.arrangeRouteListByPreference(selectedRouteInfos, 533 mRouterManager.getAvailableRoutes(packageName), 534 preferenceRouteListing); 535 } 536 return Api34Impl.filterDuplicatedIds(infos); 537 } else { 538 return infos; 539 } 540 } 541 542 // MediaRoute2Info.getType was made public on API 34, but exists since API 30. 543 @SuppressWarnings("NewApi") 544 @VisibleForTesting addMediaDevice(MediaRoute2Info route)545 void addMediaDevice(MediaRoute2Info route) { 546 final int deviceType = route.getType(); 547 MediaDevice mediaDevice = null; 548 switch (deviceType) { 549 case TYPE_UNKNOWN: 550 case TYPE_REMOTE_TV: 551 case TYPE_REMOTE_SPEAKER: 552 case TYPE_GROUP: 553 case TYPE_REMOTE_TABLET: 554 case TYPE_REMOTE_TABLET_DOCKED: 555 case TYPE_REMOTE_COMPUTER: 556 case TYPE_REMOTE_GAME_CONSOLE: 557 case TYPE_REMOTE_CAR: 558 case TYPE_REMOTE_SMARTWATCH: 559 case TYPE_REMOTE_SMARTPHONE: 560 mediaDevice = new InfoMediaDevice(mContext, mRouterManager, route, 561 mPackageName, mPreferenceItemMap.get(route.getId())); 562 break; 563 case TYPE_BUILTIN_SPEAKER: 564 case TYPE_USB_DEVICE: 565 case TYPE_USB_HEADSET: 566 case TYPE_USB_ACCESSORY: 567 case TYPE_DOCK: 568 case TYPE_HDMI: 569 case TYPE_WIRED_HEADSET: 570 case TYPE_WIRED_HEADPHONES: 571 mediaDevice = mPreferenceItemMap.containsKey(route.getId()) ? new PhoneMediaDevice( 572 mContext, mRouterManager, route, mPackageName, 573 mPreferenceItemMap.get(route.getId())) : new PhoneMediaDevice(mContext, 574 mRouterManager, route, mPackageName); 575 break; 576 case TYPE_HEARING_AID: 577 case TYPE_BLUETOOTH_A2DP: 578 case TYPE_BLE_HEADSET: 579 final BluetoothDevice device = 580 BluetoothAdapter.getDefaultAdapter().getRemoteDevice(route.getAddress()); 581 final CachedBluetoothDevice cachedDevice = 582 mBluetoothManager.getCachedDeviceManager().findDevice(device); 583 if (cachedDevice != null) { 584 mediaDevice = mPreferenceItemMap.containsKey(route.getId()) 585 ? new BluetoothMediaDevice(mContext, cachedDevice, mRouterManager, 586 route, mPackageName, mPreferenceItemMap.get(route.getId())) 587 : new BluetoothMediaDevice(mContext, cachedDevice, mRouterManager, 588 route, mPackageName); 589 } 590 break; 591 case TYPE_REMOTE_AUDIO_VIDEO_RECEIVER: 592 mediaDevice = new ComplexMediaDevice(mContext, mRouterManager, route, 593 mPackageName, mPreferenceItemMap.get(route.getId())); 594 default: 595 Log.w(TAG, "addMediaDevice() unknown device type : " + deviceType); 596 break; 597 598 } 599 if (mediaDevice != null && !TextUtils.isEmpty(mPackageName) 600 && getRoutingSessionInfo().getSelectedRoutes().contains(route.getId())) { 601 mediaDevice.setState(STATE_SELECTED); 602 if (mCurrentConnectedDevice == null) { 603 mCurrentConnectedDevice = mediaDevice; 604 } 605 } 606 if (mediaDevice != null) { 607 mMediaDevices.add(mediaDevice); 608 } 609 } 610 611 class RouterManagerCallback implements MediaRouter2Manager.Callback { 612 613 @Override onRoutesUpdated()614 public void onRoutesUpdated() { 615 refreshDevices(); 616 } 617 618 @Override onPreferredFeaturesChanged(String packageName, List<String> preferredFeatures)619 public void onPreferredFeaturesChanged(String packageName, List<String> preferredFeatures) { 620 if (TextUtils.equals(mPackageName, packageName)) { 621 refreshDevices(); 622 } 623 } 624 625 @Override onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession)626 public void onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) { 627 if (DEBUG) { 628 Log.d(TAG, "onTransferred() oldSession : " + oldSession.getName() 629 + ", newSession : " + newSession.getName()); 630 } 631 mMediaDevices.clear(); 632 mCurrentConnectedDevice = null; 633 if (TextUtils.isEmpty(mPackageName)) { 634 buildAllRoutes(); 635 } else { 636 buildAvailableRoutes(); 637 } 638 639 final String id = mCurrentConnectedDevice != null 640 ? mCurrentConnectedDevice.getId() 641 : null; 642 dispatchConnectedDeviceChanged(id); 643 } 644 645 /** 646 * Ignore callback here since we'll also receive {@link #onRequestFailed} with reason code. 647 */ 648 @Override onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route)649 public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) { 650 } 651 652 @Override onRequestFailed(int reason)653 public void onRequestFailed(int reason) { 654 dispatchOnRequestFailed(reason); 655 } 656 657 @Override onSessionUpdated(RoutingSessionInfo sessionInfo)658 public void onSessionUpdated(RoutingSessionInfo sessionInfo) { 659 refreshDevices(); 660 } 661 662 @Override onSessionReleased(@onNull RoutingSessionInfo session)663 public void onSessionReleased(@NonNull RoutingSessionInfo session) { 664 refreshDevices(); 665 } 666 667 @Override onRouteListingPreferenceUpdated( String packageName, RouteListingPreference routeListingPreference)668 public void onRouteListingPreferenceUpdated( 669 String packageName, 670 RouteListingPreference routeListingPreference) { 671 if (TextUtils.equals(mPackageName, packageName)) { 672 Api34Impl.onRouteListingPreferenceUpdated( 673 routeListingPreference, mPreferenceItemMap); 674 refreshDevices(); 675 } 676 } 677 } 678 679 @RequiresApi(34) 680 private static class Api34Impl { 681 @DoNotInline composePreferenceRouteListing( RouteListingPreference routeListingPreference)682 static List<RouteListingPreference.Item> composePreferenceRouteListing( 683 RouteListingPreference routeListingPreference) { 684 List<RouteListingPreference.Item> finalizedItemList = new ArrayList<>(); 685 List<RouteListingPreference.Item> itemList = routeListingPreference.getItems(); 686 for (RouteListingPreference.Item item : itemList) { 687 // Put suggested devices on the top first before further organization 688 if ((item.getFlags() & RouteListingPreference.Item.FLAG_SUGGESTED) != 0) { 689 finalizedItemList.add(0, item); 690 } else { 691 finalizedItemList.add(item); 692 } 693 } 694 return finalizedItemList; 695 } 696 697 @DoNotInline filterDuplicatedIds(List<MediaRoute2Info> infos)698 static synchronized List<MediaRoute2Info> filterDuplicatedIds(List<MediaRoute2Info> infos) { 699 List<MediaRoute2Info> filteredInfos = new ArrayList<>(); 700 Set<String> foundDeduplicationIds = new HashSet<>(); 701 for (MediaRoute2Info mediaRoute2Info : infos) { 702 if (!Collections.disjoint(mediaRoute2Info.getDeduplicationIds(), 703 foundDeduplicationIds)) { 704 continue; 705 } 706 filteredInfos.add(mediaRoute2Info); 707 foundDeduplicationIds.addAll(mediaRoute2Info.getDeduplicationIds()); 708 } 709 return filteredInfos; 710 } 711 712 @DoNotInline arrangeRouteListByPreference( List<MediaRoute2Info> selectedRouteInfos, List<MediaRoute2Info> infolist, List<RouteListingPreference.Item> preferenceRouteListing)713 static List<MediaRoute2Info> arrangeRouteListByPreference( 714 List<MediaRoute2Info> selectedRouteInfos, List<MediaRoute2Info> infolist, 715 List<RouteListingPreference.Item> preferenceRouteListing) { 716 final List<MediaRoute2Info> sortedInfoList = new ArrayList<>(selectedRouteInfos); 717 infolist.removeAll(selectedRouteInfos); 718 sortedInfoList.addAll(infolist.stream().filter( 719 MediaRoute2Info::isSystemRoute).collect(Collectors.toList())); 720 for (RouteListingPreference.Item item : preferenceRouteListing) { 721 for (MediaRoute2Info info : infolist) { 722 if (item.getRouteId().equals(info.getId()) 723 && !selectedRouteInfos.contains(info) 724 && !info.isSystemRoute()) { 725 sortedInfoList.add(info); 726 break; 727 } 728 } 729 } 730 return sortedInfoList; 731 } 732 733 @DoNotInline preferRouteListingOrdering(MediaRouter2Manager mediaRouter2Manager, String packageName)734 static boolean preferRouteListingOrdering(MediaRouter2Manager mediaRouter2Manager, 735 String packageName) { 736 if (TextUtils.isEmpty(packageName)) { 737 return false; 738 } 739 RouteListingPreference routeListingPreference = 740 mediaRouter2Manager.getRouteListingPreference(packageName); 741 return routeListingPreference != null 742 && !routeListingPreference.getUseSystemOrdering(); 743 } 744 745 @DoNotInline 746 @Nullable getLinkedItemComponentName( MediaRouter2Manager mediaRouter2Manager, String packageName)747 static ComponentName getLinkedItemComponentName( 748 MediaRouter2Manager mediaRouter2Manager, String packageName) { 749 if (TextUtils.isEmpty(packageName)) { 750 return null; 751 } 752 RouteListingPreference routeListingPreference = 753 mediaRouter2Manager.getRouteListingPreference(packageName); 754 return routeListingPreference == null ? null 755 : routeListingPreference.getLinkedItemComponentName(); 756 } 757 758 @DoNotInline onRouteListingPreferenceUpdated( RouteListingPreference routeListingPreference, Map<String, RouteListingPreference.Item> preferenceItemMap)759 static void onRouteListingPreferenceUpdated( 760 RouteListingPreference routeListingPreference, 761 Map<String, RouteListingPreference.Item> preferenceItemMap) { 762 preferenceItemMap.clear(); 763 if (routeListingPreference != null) { 764 routeListingPreference.getItems().forEach((item) -> { 765 preferenceItemMap.put(item.getRouteId(), item); 766 }); 767 } 768 } 769 } 770 } 771