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