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_HDMI_ARC; 25 import static android.media.MediaRoute2Info.TYPE_HDMI_EARC; 26 import static android.media.MediaRoute2Info.TYPE_HEARING_AID; 27 import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER; 28 import static android.media.MediaRoute2Info.TYPE_REMOTE_CAR; 29 import static android.media.MediaRoute2Info.TYPE_REMOTE_COMPUTER; 30 import static android.media.MediaRoute2Info.TYPE_REMOTE_GAME_CONSOLE; 31 import static android.media.MediaRoute2Info.TYPE_REMOTE_SMARTPHONE; 32 import static android.media.MediaRoute2Info.TYPE_REMOTE_SMARTWATCH; 33 import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; 34 import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET; 35 import static android.media.MediaRoute2Info.TYPE_REMOTE_TABLET_DOCKED; 36 import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; 37 import static android.media.MediaRoute2Info.TYPE_UNKNOWN; 38 import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY; 39 import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; 40 import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; 41 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; 42 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; 43 import static android.media.session.MediaController.PlaybackInfo; 44 45 import static com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_SELECTED; 46 47 import android.annotation.TargetApi; 48 import android.bluetooth.BluetoothAdapter; 49 import android.bluetooth.BluetoothDevice; 50 import android.content.ComponentName; 51 import android.content.Context; 52 import android.media.MediaRoute2Info; 53 import android.media.RouteListingPreference; 54 import android.media.RoutingSessionInfo; 55 import android.media.session.MediaController; 56 import android.media.session.MediaSession; 57 import android.os.Build; 58 import android.os.UserHandle; 59 import android.text.TextUtils; 60 import android.util.Log; 61 62 import androidx.annotation.DoNotInline; 63 import androidx.annotation.NonNull; 64 import androidx.annotation.Nullable; 65 import androidx.annotation.RequiresApi; 66 67 import com.android.internal.annotations.VisibleForTesting; 68 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 69 import com.android.settingslib.bluetooth.LocalBluetoothManager; 70 import com.android.settingslib.media.flags.Flags; 71 72 import java.util.ArrayList; 73 import java.util.Collection; 74 import java.util.Collections; 75 import java.util.HashSet; 76 import java.util.LinkedHashSet; 77 import java.util.List; 78 import java.util.Map; 79 import java.util.Set; 80 import java.util.concurrent.ConcurrentHashMap; 81 import java.util.concurrent.CopyOnWriteArrayList; 82 import java.util.function.Function; 83 import java.util.stream.Collectors; 84 import java.util.stream.Stream; 85 86 /** InfoMediaManager provide interface to get InfoMediaDevice list. */ 87 @RequiresApi(Build.VERSION_CODES.R) 88 public abstract class InfoMediaManager { 89 /** Callback for notifying device is added, removed and attributes changed. */ 90 public interface MediaDeviceCallback { 91 92 /** 93 * Callback for notifying MediaDevice list is added. 94 * 95 * @param devices the MediaDevice list 96 */ onDeviceListAdded(@onNull List<MediaDevice> devices)97 void onDeviceListAdded(@NonNull List<MediaDevice> devices); 98 99 /** 100 * Callback for notifying MediaDevice list is removed. 101 * 102 * @param devices the MediaDevice list 103 */ onDeviceListRemoved(@onNull List<MediaDevice> devices)104 void onDeviceListRemoved(@NonNull List<MediaDevice> devices); 105 106 /** 107 * Callback for notifying connected MediaDevice is changed. 108 * 109 * @param id the id of MediaDevice 110 */ onConnectedDeviceChanged(@ullable String id)111 void onConnectedDeviceChanged(@Nullable String id); 112 113 /** 114 * Callback for notifying that transferring is failed. 115 * 116 * @param reason the reason that the request has failed. Can be one of followings: {@link 117 * android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR}, {@link 118 * android.media.MediaRoute2ProviderService#REASON_REJECTED}, {@link 119 * android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR}, {@link 120 * android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE}, {@link 121 * android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND}, 122 */ onRequestFailed(int reason)123 void onRequestFailed(int reason); 124 } 125 126 /** Checked exception that signals the specified package is not present in the system. */ 127 public static class PackageNotAvailableException extends Exception { PackageNotAvailableException(String message)128 public PackageNotAvailableException(String message) { 129 super(message); 130 } 131 } 132 133 private static final String TAG = "InfoMediaManager"; 134 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 135 protected final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>(); 136 @NonNull protected final Context mContext; 137 @NonNull protected final String mPackageName; 138 @NonNull protected final UserHandle mUserHandle; 139 private final Collection<MediaDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>(); 140 private MediaDevice mCurrentConnectedDevice; 141 private MediaController mMediaController; 142 private PlaybackInfo mLastKnownPlaybackInfo; 143 private final LocalBluetoothManager mBluetoothManager; 144 private final Map<String, RouteListingPreference.Item> mPreferenceItemMap = 145 new ConcurrentHashMap<>(); 146 147 private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); 148 InfoMediaManager( @onNull Context context, @NonNull String packageName, @NonNull UserHandle userHandle, @NonNull LocalBluetoothManager localBluetoothManager, @Nullable MediaController mediaController)149 /* package */ InfoMediaManager( 150 @NonNull Context context, 151 @NonNull String packageName, 152 @NonNull UserHandle userHandle, 153 @NonNull LocalBluetoothManager localBluetoothManager, 154 @Nullable MediaController mediaController) { 155 mContext = context; 156 mBluetoothManager = localBluetoothManager; 157 mPackageName = packageName; 158 mUserHandle = userHandle; 159 mMediaController = mediaController; 160 if (mediaController != null) { 161 mLastKnownPlaybackInfo = mediaController.getPlaybackInfo(); 162 } 163 } 164 165 /** 166 * Creates an instance of InfoMediaManager. 167 * 168 * @param context The {@link Context}. 169 * @param packageName The package name of the app for which to control routing, or null if the 170 * caller is interested in system-level routing only (for example, headsets, built-in 171 * speakers, as opposed to app-specific routing (for example, casting to another device). 172 * @param userHandle The {@link UserHandle} of the user on which the app to control is running, 173 * or null if the caller does not need app-specific routing (see {@code packageName}). 174 * @param token The token of the associated {@link MediaSession} for which to do media routing. 175 */ createInstance( Context context, @Nullable String packageName, @Nullable UserHandle userHandle, LocalBluetoothManager localBluetoothManager, @Nullable MediaSession.Token token)176 public static InfoMediaManager createInstance( 177 Context context, 178 @Nullable String packageName, 179 @Nullable UserHandle userHandle, 180 LocalBluetoothManager localBluetoothManager, 181 @Nullable MediaSession.Token token) { 182 MediaController mediaController = null; 183 184 if (Flags.usePlaybackInfoForRoutingControls() && token != null) { 185 mediaController = new MediaController(context, token); 186 } 187 188 // The caller is only interested in system routes (headsets, built-in speakers, etc), and is 189 // not interested in a specific app's routing. The media routing APIs still require a 190 // package name, so we use the package name of the calling app. 191 if (TextUtils.isEmpty(packageName)) { 192 packageName = context.getPackageName(); 193 } 194 195 if (userHandle == null) { 196 userHandle = android.os.Process.myUserHandle(); 197 } 198 199 if (Flags.useMediaRouter2ForInfoMediaManager()) { 200 try { 201 return new RouterInfoMediaManager( 202 context, packageName, userHandle, localBluetoothManager, mediaController); 203 } catch (PackageNotAvailableException ex) { 204 // TODO: b/293578081 - Propagate this exception to callers for proper handling. 205 Log.w(TAG, "Returning a no-op InfoMediaManager for package " + packageName); 206 return new NoOpInfoMediaManager( 207 context, packageName, userHandle, localBluetoothManager, mediaController); 208 } 209 } else { 210 return new ManagerInfoMediaManager( 211 context, packageName, userHandle, localBluetoothManager, mediaController); 212 } 213 } 214 startScan()215 public void startScan() { 216 startScanOnRouter(); 217 } 218 updateRouteListingPreference()219 private void updateRouteListingPreference() { 220 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 221 RouteListingPreference routeListingPreference = 222 getRouteListingPreference(); 223 Api34Impl.onRouteListingPreferenceUpdated(routeListingPreference, 224 mPreferenceItemMap); 225 } 226 } 227 stopScan()228 public final void stopScan() { 229 stopScanOnRouter(); 230 } 231 stopScanOnRouter()232 protected abstract void stopScanOnRouter(); 233 startScanOnRouter()234 protected abstract void startScanOnRouter(); 235 registerRouter()236 protected abstract void registerRouter(); 237 unregisterRouter()238 protected abstract void unregisterRouter(); 239 transferToRoute(@onNull MediaRoute2Info route)240 protected abstract void transferToRoute(@NonNull MediaRoute2Info route); 241 selectRoute( @onNull MediaRoute2Info route, @NonNull RoutingSessionInfo info)242 protected abstract void selectRoute( 243 @NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info); 244 deselectRoute( @onNull MediaRoute2Info route, @NonNull RoutingSessionInfo info)245 protected abstract void deselectRoute( 246 @NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo info); 247 releaseSession(@onNull RoutingSessionInfo sessionInfo)248 protected abstract void releaseSession(@NonNull RoutingSessionInfo sessionInfo); 249 250 @NonNull getSelectableRoutes(@onNull RoutingSessionInfo info)251 protected abstract List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo info); 252 253 @NonNull getDeselectableRoutes( @onNull RoutingSessionInfo info)254 protected abstract List<MediaRoute2Info> getDeselectableRoutes( 255 @NonNull RoutingSessionInfo info); 256 257 @NonNull getSelectedRoutes(@onNull RoutingSessionInfo info)258 protected abstract List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo info); 259 setSessionVolume(@onNull RoutingSessionInfo info, int volume)260 protected abstract void setSessionVolume(@NonNull RoutingSessionInfo info, int volume); 261 setRouteVolume(@onNull MediaRoute2Info route, int volume)262 protected abstract void setRouteVolume(@NonNull MediaRoute2Info route, int volume); 263 264 @Nullable getRouteListingPreference()265 protected abstract RouteListingPreference getRouteListingPreference(); 266 267 /** 268 * Returns the list of remote {@link RoutingSessionInfo routing sessions} known to the system. 269 */ 270 @NonNull getRemoteSessions()271 protected abstract List<RoutingSessionInfo> getRemoteSessions(); 272 273 /** 274 * Returns a non-empty list containing the routing sessions associated to the target media app. 275 * 276 * <p> The first item of the list is always the {@link RoutingSessionInfo#isSystemSession() 277 * system session}, followed other remote sessions linked to the target media app. 278 */ 279 @NonNull getRoutingSessionsForPackage()280 protected abstract List<RoutingSessionInfo> getRoutingSessionsForPackage(); 281 282 @Nullable getRoutingSessionById(@onNull String sessionId)283 protected abstract RoutingSessionInfo getRoutingSessionById(@NonNull String sessionId); 284 285 @NonNull getAvailableRoutesFromRouter()286 protected abstract List<MediaRoute2Info> getAvailableRoutesFromRouter(); 287 288 @NonNull getTransferableRoutes(@onNull String packageName)289 protected abstract List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName); 290 rebuildDeviceList()291 protected final void rebuildDeviceList() { 292 buildAvailableRoutes(); 293 } 294 notifyCurrentConnectedDeviceChanged()295 protected final void notifyCurrentConnectedDeviceChanged() { 296 final String id = mCurrentConnectedDevice != null ? mCurrentConnectedDevice.getId() : null; 297 dispatchConnectedDeviceChanged(id); 298 } 299 300 @RequiresApi(34) notifyRouteListingPreferenceUpdated( RouteListingPreference routeListingPreference)301 protected final void notifyRouteListingPreferenceUpdated( 302 RouteListingPreference routeListingPreference) { 303 Api34Impl.onRouteListingPreferenceUpdated(routeListingPreference, mPreferenceItemMap); 304 } 305 findMediaDevice(@onNull String id)306 protected final MediaDevice findMediaDevice(@NonNull String id) { 307 for (MediaDevice mediaDevice : mMediaDevices) { 308 if (mediaDevice.getId().equals(id)) { 309 return mediaDevice; 310 } 311 } 312 Log.e(TAG, "findMediaDevice() can't find device with id: " + id); 313 return null; 314 } 315 316 /** 317 * Registers the specified {@code callback} to receive state updates about routing information. 318 * 319 * <p>As long as there is a registered {@link MediaDeviceCallback}, {@link InfoMediaManager} 320 * will receive state updates from the platform. 321 * 322 * <p>Call {@link #unregisterCallback(MediaDeviceCallback)} once you no longer need platform 323 * updates. 324 */ registerCallback(@onNull MediaDeviceCallback callback)325 public final void registerCallback(@NonNull MediaDeviceCallback callback) { 326 boolean wasEmpty = mCallbacks.isEmpty(); 327 if (!mCallbacks.contains(callback)) { 328 mCallbacks.add(callback); 329 if (wasEmpty) { 330 mMediaDevices.clear(); 331 registerRouter(); 332 if (mMediaController != null) { 333 mMediaController.registerCallback(mMediaControllerCallback); 334 } 335 updateRouteListingPreference(); 336 refreshDevices(); 337 } 338 } 339 } 340 341 /** 342 * Unregisters the specified {@code callback}. 343 * 344 * @see #registerCallback(MediaDeviceCallback) 345 */ unregisterCallback(@onNull MediaDeviceCallback callback)346 public final void unregisterCallback(@NonNull MediaDeviceCallback callback) { 347 if (mCallbacks.remove(callback) && mCallbacks.isEmpty()) { 348 if (mMediaController != null) { 349 mMediaController.unregisterCallback(mMediaControllerCallback); 350 } 351 unregisterRouter(); 352 } 353 } 354 dispatchDeviceListAdded(@onNull List<MediaDevice> devices)355 private void dispatchDeviceListAdded(@NonNull List<MediaDevice> devices) { 356 for (MediaDeviceCallback callback : getCallbacks()) { 357 callback.onDeviceListAdded(new ArrayList<>(devices)); 358 } 359 } 360 dispatchConnectedDeviceChanged(String id)361 private void dispatchConnectedDeviceChanged(String id) { 362 for (MediaDeviceCallback callback : getCallbacks()) { 363 callback.onConnectedDeviceChanged(id); 364 } 365 } 366 dispatchOnRequestFailed(int reason)367 protected void dispatchOnRequestFailed(int reason) { 368 for (MediaDeviceCallback callback : getCallbacks()) { 369 callback.onRequestFailed(reason); 370 } 371 } 372 getCallbacks()373 private Collection<MediaDeviceCallback> getCallbacks() { 374 return new CopyOnWriteArrayList<>(mCallbacks); 375 } 376 377 /** 378 * Get current device that played media. 379 * @return MediaDevice 380 */ getCurrentConnectedDevice()381 MediaDevice getCurrentConnectedDevice() { 382 return mCurrentConnectedDevice; 383 } 384 connectToDevice(MediaDevice device)385 /* package */ void connectToDevice(MediaDevice device) { 386 if (device.mRouteInfo == null) { 387 Log.w(TAG, "Unable to connect. RouteInfo is empty"); 388 return; 389 } 390 391 device.setConnectedRecord(); 392 transferToRoute(device.mRouteInfo); 393 } 394 395 /** 396 * Add a MediaDevice to let it play current media. 397 * 398 * @param device MediaDevice 399 * @return If add device successful return {@code true}, otherwise return {@code false} 400 */ addDeviceToPlayMedia(MediaDevice device)401 boolean addDeviceToPlayMedia(MediaDevice device) { 402 final RoutingSessionInfo info = getActiveRoutingSession(); 403 if (!info.getSelectableRoutes().contains(device.mRouteInfo.getId())) { 404 Log.w(TAG, "addDeviceToPlayMedia() Ignoring selecting a non-selectable device : " 405 + device.getName()); 406 return false; 407 } 408 409 selectRoute(device.mRouteInfo, info); 410 return true; 411 } 412 413 @NonNull getActiveRoutingSession()414 private RoutingSessionInfo getActiveRoutingSession() { 415 // List is never empty. 416 final List<RoutingSessionInfo> sessions = getRoutingSessionsForPackage(); 417 RoutingSessionInfo activeSession = sessions.get(sessions.size() - 1); 418 419 // Logic from MediaRouter2Manager#getRoutingSessionForMediaController 420 if (!Flags.usePlaybackInfoForRoutingControls() || mMediaController == null) { 421 return activeSession; 422 } 423 424 PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); 425 if (playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_LOCAL) { 426 // Return system session. 427 return sessions.get(0); 428 } 429 430 // For PLAYBACK_TYPE_REMOTE. 431 String volumeControlId = playbackInfo.getVolumeControlId(); 432 for (RoutingSessionInfo session : sessions) { 433 if (TextUtils.equals(volumeControlId, session.getId())) { 434 return session; 435 } 436 // Workaround for provider not being able to know the unique session ID. 437 if (TextUtils.equals(volumeControlId, session.getOriginalId()) 438 && TextUtils.equals( 439 mMediaController.getPackageName(), session.getOwnerPackageName())) { 440 return session; 441 } 442 } 443 444 return activeSession; 445 } 446 isRoutingSessionAvailableForVolumeControl()447 boolean isRoutingSessionAvailableForVolumeControl() { 448 List<RoutingSessionInfo> sessions = getRoutingSessionsForPackage(); 449 450 for (RoutingSessionInfo session : sessions) { 451 if (!session.isSystemSession() 452 && session.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { 453 return true; 454 } 455 } 456 457 Log.d(TAG, "No routing session for " + mPackageName); 458 return false; 459 } 460 preferRouteListingOrdering()461 boolean preferRouteListingOrdering() { 462 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE 463 && Api34Impl.preferRouteListingOrdering(getRouteListingPreference()); 464 } 465 466 @Nullable getLinkedItemComponentName()467 ComponentName getLinkedItemComponentName() { 468 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE && TextUtils.isEmpty( 469 mPackageName)) { 470 return null; 471 } 472 return Api34Impl.getLinkedItemComponentName(getRouteListingPreference()); 473 } 474 475 /** 476 * Remove a {@code device} from current media. 477 * 478 * @param device MediaDevice 479 * @return If device stop successful return {@code true}, otherwise return {@code false} 480 */ removeDeviceFromPlayMedia(MediaDevice device)481 boolean removeDeviceFromPlayMedia(MediaDevice device) { 482 final RoutingSessionInfo info = getActiveRoutingSession(); 483 if (!info.getSelectedRoutes().contains(device.mRouteInfo.getId())) { 484 Log.w(TAG, "removeDeviceFromMedia() Ignoring deselecting a non-deselectable device : " 485 + device.getName()); 486 return false; 487 } 488 489 deselectRoute(device.mRouteInfo, info); 490 return true; 491 } 492 493 /** 494 * Release session to stop playing media on MediaDevice. 495 */ releaseSession()496 boolean releaseSession() { 497 releaseSession(getActiveRoutingSession()); 498 return true; 499 } 500 501 /** 502 * Returns the list of {@link MediaDevice media devices} that can be added to the current {@link 503 * RoutingSessionInfo routing session}. 504 */ 505 @NonNull getSelectableMediaDevices()506 List<MediaDevice> getSelectableMediaDevices() { 507 final RoutingSessionInfo info = getActiveRoutingSession(); 508 509 final List<MediaDevice> deviceList = new ArrayList<>(); 510 for (MediaRoute2Info route : getSelectableRoutes(info)) { 511 deviceList.add( 512 new InfoMediaDevice( 513 mContext, route, mPreferenceItemMap.get(route.getId()))); 514 } 515 return deviceList; 516 } 517 518 /** 519 * Returns the list of {@link MediaDevice media devices} that can be deselected from the current 520 * {@link RoutingSessionInfo routing session}. 521 */ 522 @NonNull getDeselectableMediaDevices()523 List<MediaDevice> getDeselectableMediaDevices() { 524 final RoutingSessionInfo info = getActiveRoutingSession(); 525 526 final List<MediaDevice> deviceList = new ArrayList<>(); 527 for (MediaRoute2Info route : getDeselectableRoutes(info)) { 528 deviceList.add( 529 new InfoMediaDevice( 530 mContext, route, mPreferenceItemMap.get(route.getId()))); 531 Log.d(TAG, route.getName() + " is deselectable for " + mPackageName); 532 } 533 return deviceList; 534 } 535 536 /** 537 * Returns the list of {@link MediaDevice media devices} that are selected in the current {@link 538 * RoutingSessionInfo routing session}. 539 */ 540 @NonNull getSelectedMediaDevices()541 List<MediaDevice> getSelectedMediaDevices() { 542 RoutingSessionInfo info = getActiveRoutingSession(); 543 544 final List<MediaDevice> deviceList = new ArrayList<>(); 545 for (MediaRoute2Info route : getSelectedRoutes(info)) { 546 deviceList.add( 547 new InfoMediaDevice( 548 mContext, route, mPreferenceItemMap.get(route.getId()))); 549 } 550 return deviceList; 551 } 552 adjustDeviceVolume(MediaDevice device, int volume)553 /* package */ void adjustDeviceVolume(MediaDevice device, int volume) { 554 if (device.mRouteInfo == null) { 555 Log.w(TAG, "Unable to set volume. RouteInfo is empty"); 556 return; 557 } 558 setRouteVolume(device.mRouteInfo, volume); 559 } 560 adjustSessionVolume(RoutingSessionInfo info, int volume)561 void adjustSessionVolume(RoutingSessionInfo info, int volume) { 562 if (info == null) { 563 Log.w(TAG, "Unable to adjust session volume. RoutingSessionInfo is empty"); 564 return; 565 } 566 567 setSessionVolume(info, volume); 568 } 569 570 /** 571 * Adjust the volume of {@link android.media.RoutingSessionInfo}. 572 * 573 * @param volume the value of volume 574 */ adjustSessionVolume(int volume)575 void adjustSessionVolume(int volume) { 576 Log.d(TAG, "adjustSessionVolume() adjust volume: " + volume + ", with : " + mPackageName); 577 setSessionVolume(getActiveRoutingSession(), volume); 578 } 579 580 /** 581 * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}. 582 * 583 * @return maximum volume of the session, and return -1 if not found. 584 */ getSessionVolumeMax()585 public int getSessionVolumeMax() { 586 return getActiveRoutingSession().getVolumeMax(); 587 } 588 589 /** 590 * Gets the current volume of the {@link android.media.RoutingSessionInfo}. 591 * 592 * @return current volume of the session, and return -1 if not found. 593 */ getSessionVolume()594 public int getSessionVolume() { 595 return getActiveRoutingSession().getVolume(); 596 } 597 getSessionName()598 CharSequence getSessionName() { 599 return getActiveRoutingSession().getName(); 600 } 601 602 @TargetApi(Build.VERSION_CODES.R) shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo)603 boolean shouldEnableVolumeSeekBar(RoutingSessionInfo sessionInfo) { 604 return sessionInfo.isSystemSession() // System sessions are not remote 605 || sessionInfo.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED; 606 } 607 refreshDevices()608 protected final synchronized void refreshDevices() { 609 rebuildDeviceList(); 610 dispatchDeviceListAdded(mMediaDevices); 611 } 612 613 // MediaRoute2Info.getType was made public on API 34, but exists since API 30. 614 @SuppressWarnings("NewApi") buildAvailableRoutes()615 private synchronized void buildAvailableRoutes() { 616 mMediaDevices.clear(); 617 RoutingSessionInfo activeSession = getActiveRoutingSession(); 618 619 for (MediaRoute2Info route : getAvailableRoutes(activeSession)) { 620 if (DEBUG) { 621 Log.d(TAG, "buildAvailableRoutes() route : " + route.getName() + ", volume : " 622 + route.getVolume() + ", type : " + route.getType()); 623 } 624 addMediaDevice(route, activeSession); 625 } 626 627 // In practice, mMediaDevices should always have at least one route. 628 if (!mMediaDevices.isEmpty()) { 629 // First device on the list is always the first selected route. 630 mCurrentConnectedDevice = mMediaDevices.get(0); 631 } 632 } 633 getAvailableRoutes( RoutingSessionInfo activeSession)634 private synchronized List<MediaRoute2Info> getAvailableRoutes( 635 RoutingSessionInfo activeSession) { 636 List<MediaRoute2Info> availableRoutes = new ArrayList<>(); 637 638 List<MediaRoute2Info> selectedRoutes = getSelectedRoutes(activeSession); 639 availableRoutes.addAll(selectedRoutes); 640 availableRoutes.addAll(getSelectableRoutes(activeSession)); 641 642 final List<MediaRoute2Info> transferableRoutes = getTransferableRoutes(mPackageName); 643 for (MediaRoute2Info transferableRoute : transferableRoutes) { 644 boolean alreadyAdded = false; 645 for (MediaRoute2Info mediaRoute2Info : availableRoutes) { 646 if (TextUtils.equals(transferableRoute.getId(), mediaRoute2Info.getId())) { 647 alreadyAdded = true; 648 break; 649 } 650 } 651 if (!alreadyAdded) { 652 availableRoutes.add(transferableRoute); 653 } 654 } 655 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 656 RouteListingPreference routeListingPreference = getRouteListingPreference(); 657 if (routeListingPreference != null) { 658 final List<RouteListingPreference.Item> preferenceRouteListing = 659 Api34Impl.composePreferenceRouteListing( 660 routeListingPreference); 661 availableRoutes = Api34Impl.arrangeRouteListByPreference(selectedRoutes, 662 getAvailableRoutesFromRouter(), 663 preferenceRouteListing); 664 } 665 return Api34Impl.filterDuplicatedIds(availableRoutes); 666 } else { 667 return availableRoutes; 668 } 669 } 670 671 // MediaRoute2Info.getType was made public on API 34, but exists since API 30. 672 @SuppressWarnings("NewApi") 673 @VisibleForTesting addMediaDevice(@onNull MediaRoute2Info route, @NonNull RoutingSessionInfo activeSession)674 void addMediaDevice(@NonNull MediaRoute2Info route, @NonNull RoutingSessionInfo activeSession) { 675 final int deviceType = route.getType(); 676 MediaDevice mediaDevice = null; 677 switch (deviceType) { 678 case TYPE_UNKNOWN: 679 case TYPE_REMOTE_TV: 680 case TYPE_REMOTE_SPEAKER: 681 case TYPE_GROUP: 682 case TYPE_REMOTE_TABLET: 683 case TYPE_REMOTE_TABLET_DOCKED: 684 case TYPE_REMOTE_COMPUTER: 685 case TYPE_REMOTE_GAME_CONSOLE: 686 case TYPE_REMOTE_CAR: 687 case TYPE_REMOTE_SMARTWATCH: 688 case TYPE_REMOTE_SMARTPHONE: 689 mediaDevice = 690 new InfoMediaDevice( 691 mContext, 692 route, 693 mPreferenceItemMap.get(route.getId())); 694 break; 695 case TYPE_BUILTIN_SPEAKER: 696 case TYPE_USB_DEVICE: 697 case TYPE_USB_HEADSET: 698 case TYPE_USB_ACCESSORY: 699 case TYPE_DOCK: 700 case TYPE_HDMI: 701 case TYPE_HDMI_ARC: 702 case TYPE_HDMI_EARC: 703 case TYPE_WIRED_HEADSET: 704 case TYPE_WIRED_HEADPHONES: 705 mediaDevice = 706 new PhoneMediaDevice( 707 mContext, 708 route, 709 mPreferenceItemMap.getOrDefault(route.getId(), null)); 710 break; 711 case TYPE_HEARING_AID: 712 case TYPE_BLUETOOTH_A2DP: 713 case TYPE_BLE_HEADSET: 714 if (route.getAddress() == null) { 715 Log.e(TAG, "Ignoring bluetooth route with no set address: " + route); 716 break; 717 } 718 final BluetoothDevice device = 719 BluetoothAdapter.getDefaultAdapter() 720 .getRemoteDevice(route.getAddress()); 721 final CachedBluetoothDevice cachedDevice = 722 mBluetoothManager.getCachedDeviceManager().findDevice(device); 723 if (cachedDevice != null) { 724 mediaDevice = 725 new BluetoothMediaDevice( 726 mContext, 727 cachedDevice, 728 route, 729 mPreferenceItemMap.getOrDefault(route.getId(), null)); 730 } 731 break; 732 case TYPE_REMOTE_AUDIO_VIDEO_RECEIVER: 733 mediaDevice = 734 new ComplexMediaDevice( 735 mContext, 736 route, 737 mPreferenceItemMap.get(route.getId())); 738 break; 739 default: 740 Log.w(TAG, "addMediaDevice() unknown device type : " + deviceType); 741 break; 742 } 743 744 if (mediaDevice != null) { 745 if (activeSession.getSelectedRoutes().contains(route.getId())) { 746 mediaDevice.setState(STATE_SELECTED); 747 } 748 mMediaDevices.add(mediaDevice); 749 } 750 } 751 752 @RequiresApi(34) 753 static class Api34Impl { 754 @DoNotInline composePreferenceRouteListing( RouteListingPreference routeListingPreference)755 static List<RouteListingPreference.Item> composePreferenceRouteListing( 756 RouteListingPreference routeListingPreference) { 757 List<RouteListingPreference.Item> finalizedItemList = new ArrayList<>(); 758 List<RouteListingPreference.Item> itemList = routeListingPreference.getItems(); 759 for (RouteListingPreference.Item item : itemList) { 760 // Put suggested devices on the top first before further organization 761 if ((item.getFlags() & RouteListingPreference.Item.FLAG_SUGGESTED) != 0) { 762 finalizedItemList.add(0, item); 763 } else { 764 finalizedItemList.add(item); 765 } 766 } 767 return finalizedItemList; 768 } 769 770 @DoNotInline filterDuplicatedIds(List<MediaRoute2Info> infos)771 static synchronized List<MediaRoute2Info> filterDuplicatedIds(List<MediaRoute2Info> infos) { 772 List<MediaRoute2Info> filteredInfos = new ArrayList<>(); 773 Set<String> foundDeduplicationIds = new HashSet<>(); 774 for (MediaRoute2Info mediaRoute2Info : infos) { 775 if (!Collections.disjoint(mediaRoute2Info.getDeduplicationIds(), 776 foundDeduplicationIds)) { 777 continue; 778 } 779 filteredInfos.add(mediaRoute2Info); 780 foundDeduplicationIds.addAll(mediaRoute2Info.getDeduplicationIds()); 781 } 782 return filteredInfos; 783 } 784 785 /** 786 * Returns an ordered list of available devices based on the provided {@code 787 * routeListingPreferenceItems}. 788 * 789 * <p>The result has the following order: 790 * 791 * <ol> 792 * <li>Selected routes. 793 * <li>Not-selected system routes. 794 * <li>Not-selected, non-system, available routes sorted by route listing preference. 795 * </ol> 796 * 797 * @param selectedRoutes List of currently selected routes. 798 * @param availableRoutes List of available routes that match the app's requested route 799 * features. 800 * @param routeListingPreferenceItems Ordered list of {@link RouteListingPreference.Item} to 801 * sort routes with. 802 */ 803 @DoNotInline arrangeRouteListByPreference( List<MediaRoute2Info> selectedRoutes, List<MediaRoute2Info> availableRoutes, List<RouteListingPreference.Item> routeListingPreferenceItems)804 static List<MediaRoute2Info> arrangeRouteListByPreference( 805 List<MediaRoute2Info> selectedRoutes, 806 List<MediaRoute2Info> availableRoutes, 807 List<RouteListingPreference.Item> routeListingPreferenceItems) { 808 Set<String> sortedRouteIds = new LinkedHashSet<>(); 809 810 // Add selected routes first. 811 for (MediaRoute2Info selectedRoute : selectedRoutes) { 812 sortedRouteIds.add(selectedRoute.getId()); 813 } 814 815 // Add not-yet-added system routes. 816 for (MediaRoute2Info availableRoute : availableRoutes) { 817 if (availableRoute.isSystemRoute()) { 818 sortedRouteIds.add(availableRoute.getId()); 819 } 820 } 821 822 // Create a mapping from id to route to avoid a quadratic search. 823 Map<String, MediaRoute2Info> idToRouteMap = 824 Stream.concat(selectedRoutes.stream(), availableRoutes.stream()) 825 .collect( 826 Collectors.toMap( 827 MediaRoute2Info::getId, 828 Function.identity(), 829 (route1, route2) -> route1)); 830 831 // Add not-selected routes that match RLP items. All system routes have already been 832 // added at this point. 833 for (RouteListingPreference.Item item : routeListingPreferenceItems) { 834 MediaRoute2Info route = idToRouteMap.get(item.getRouteId()); 835 if (route != null) { 836 sortedRouteIds.add(route.getId()); 837 } 838 } 839 840 return sortedRouteIds.stream().map(idToRouteMap::get).collect(Collectors.toList()); 841 } 842 843 @DoNotInline preferRouteListingOrdering(RouteListingPreference routeListingPreference)844 static boolean preferRouteListingOrdering(RouteListingPreference routeListingPreference) { 845 return routeListingPreference != null 846 && !routeListingPreference.getUseSystemOrdering(); 847 } 848 849 @DoNotInline 850 @Nullable getLinkedItemComponentName( RouteListingPreference routeListingPreference)851 static ComponentName getLinkedItemComponentName( 852 RouteListingPreference routeListingPreference) { 853 return routeListingPreference == null ? null 854 : routeListingPreference.getLinkedItemComponentName(); 855 } 856 857 @DoNotInline onRouteListingPreferenceUpdated( RouteListingPreference routeListingPreference, Map<String, RouteListingPreference.Item> preferenceItemMap)858 static void onRouteListingPreferenceUpdated( 859 RouteListingPreference routeListingPreference, 860 Map<String, RouteListingPreference.Item> preferenceItemMap) { 861 preferenceItemMap.clear(); 862 if (routeListingPreference != null) { 863 routeListingPreference.getItems().forEach((item) -> 864 preferenceItemMap.put(item.getRouteId(), item)); 865 } 866 } 867 } 868 869 private final class MediaControllerCallback extends MediaController.Callback { 870 @Override onSessionDestroyed()871 public void onSessionDestroyed() { 872 mMediaController = null; 873 refreshDevices(); 874 } 875 876 @Override onAudioInfoChanged(@onNull PlaybackInfo info)877 public void onAudioInfoChanged(@NonNull PlaybackInfo info) { 878 if (info.getPlaybackType() != mLastKnownPlaybackInfo.getPlaybackType() 879 || !TextUtils.equals( 880 info.getVolumeControlId(), 881 mLastKnownPlaybackInfo.getVolumeControlId())) { 882 refreshDevices(); 883 } 884 mLastKnownPlaybackInfo = info; 885 } 886 } 887 } 888