1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.media.dialog; 18 19 import static android.media.RouteListingPreference.ACTION_TRANSFER_MEDIA; 20 import static android.media.RouteListingPreference.EXTRA_ROUTE_ID; 21 import static android.provider.Settings.ACTION_BLUETOOTH_SETTINGS; 22 23 import android.annotation.CallbackExecutor; 24 import android.app.AlertDialog; 25 import android.app.KeyguardManager; 26 import android.app.Notification; 27 import android.app.WallpaperColors; 28 import android.bluetooth.BluetoothDevice; 29 import android.bluetooth.BluetoothLeBroadcast; 30 import android.bluetooth.BluetoothLeBroadcastAssistant; 31 import android.bluetooth.BluetoothLeBroadcastMetadata; 32 import android.bluetooth.BluetoothLeBroadcastReceiveState; 33 import android.content.ComponentName; 34 import android.content.Context; 35 import android.content.DialogInterface; 36 import android.content.Intent; 37 import android.content.pm.ApplicationInfo; 38 import android.content.pm.PackageManager; 39 import android.graphics.Bitmap; 40 import android.graphics.drawable.Drawable; 41 import android.graphics.drawable.Icon; 42 import android.media.AudioManager; 43 import android.media.INearbyMediaDevicesUpdateCallback; 44 import android.media.MediaMetadata; 45 import android.media.MediaRoute2Info; 46 import android.media.NearbyDevice; 47 import android.media.RoutingSessionInfo; 48 import android.media.session.MediaController; 49 import android.media.session.MediaSession; 50 import android.media.session.MediaSessionManager; 51 import android.media.session.PlaybackState; 52 import android.os.IBinder; 53 import android.os.PowerExemptionManager; 54 import android.os.RemoteException; 55 import android.os.UserHandle; 56 import android.os.UserManager; 57 import android.provider.Settings; 58 import android.text.TextUtils; 59 import android.util.Log; 60 import android.view.View; 61 import android.view.WindowManager; 62 63 import androidx.annotation.NonNull; 64 import androidx.annotation.Nullable; 65 import androidx.annotation.VisibleForTesting; 66 import androidx.core.graphics.drawable.IconCompat; 67 68 import com.android.internal.annotations.GuardedBy; 69 import com.android.media.flags.Flags; 70 import com.android.settingslib.RestrictedLockUtilsInternal; 71 import com.android.settingslib.Utils; 72 import com.android.settingslib.bluetooth.BluetoothUtils; 73 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; 74 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 75 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata; 76 import com.android.settingslib.bluetooth.LocalBluetoothManager; 77 import com.android.settingslib.media.InfoMediaManager; 78 import com.android.settingslib.media.InputMediaDevice; 79 import com.android.settingslib.media.InputRouteManager; 80 import com.android.settingslib.media.LocalMediaManager; 81 import com.android.settingslib.media.MediaDevice; 82 import com.android.settingslib.utils.ThreadUtils; 83 import com.android.systemui.animation.ActivityTransitionAnimator; 84 import com.android.systemui.animation.DialogTransitionAnimator; 85 import com.android.systemui.broadcast.BroadcastSender; 86 import com.android.systemui.dagger.qualifiers.Background; 87 import com.android.systemui.dagger.qualifiers.Main; 88 import com.android.systemui.flags.FeatureFlags; 89 import com.android.systemui.media.dialog.MediaItem.MediaItemType; 90 import com.android.systemui.media.nearby.NearbyMediaDevicesManager; 91 import com.android.systemui.monet.ColorScheme; 92 import com.android.systemui.plugins.ActivityStarter; 93 import com.android.systemui.res.R; 94 import com.android.systemui.settings.UserTracker; 95 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 96 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; 97 import com.android.systemui.statusbar.phone.SystemUIDialog; 98 import com.android.systemui.volume.panel.domain.interactor.VolumePanelGlobalStateInteractor; 99 100 import dagger.assisted.Assisted; 101 import dagger.assisted.AssistedFactory; 102 import dagger.assisted.AssistedInject; 103 104 import java.nio.charset.StandardCharsets; 105 import java.util.ArrayList; 106 import java.util.Collection; 107 import java.util.Collections; 108 import java.util.Comparator; 109 import java.util.HashMap; 110 import java.util.List; 111 import java.util.Map; 112 import java.util.Set; 113 import java.util.concurrent.ConcurrentHashMap; 114 import java.util.concurrent.CopyOnWriteArrayList; 115 import java.util.concurrent.Executor; 116 import java.util.function.Function; 117 import java.util.stream.Collectors; 118 119 import javax.inject.Inject; 120 121 /** 122 * Controller for a dialog that allows users to switch media output and input devices, control 123 * volume, connect to new devices, etc. 124 */ 125 public class MediaSwitchingController 126 implements LocalMediaManager.DeviceCallback, INearbyMediaDevicesUpdateCallback { 127 128 private static final String TAG = "MediaSwitchingController"; 129 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 130 private static final String PAGE_CONNECTED_DEVICES_KEY = 131 "top_level_connected_devices"; 132 private static final long ALLOWLIST_DURATION_MS = 20000; 133 private static final String ALLOWLIST_REASON = "mediaoutput:remote_transfer"; 134 135 private final String mPackageName; 136 private final UserHandle mUserHandle; 137 private final Context mContext; 138 private final MediaSessionManager mMediaSessionManager; 139 private final LocalBluetoothManager mLocalBluetoothManager; 140 private final ActivityStarter mActivityStarter; 141 private final DialogTransitionAnimator mDialogTransitionAnimator; 142 private final CommonNotifCollection mNotifCollection; 143 protected final Object mMediaDevicesLock = new Object(); 144 protected final Object mInputMediaDevicesLock = new Object(); 145 @VisibleForTesting 146 final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>(); 147 final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>(); 148 private final OutputMediaItemListProxy mOutputMediaItemListProxy; 149 private final List<MediaItem> mInputMediaItemList = new CopyOnWriteArrayList<>(); 150 private final AudioManager mAudioManager; 151 private final PowerExemptionManager mPowerExemptionManager; 152 private final KeyguardManager mKeyGuardManager; 153 private final NearbyMediaDevicesManager mNearbyMediaDevicesManager; 154 private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>(); 155 private final MediaSession.Token mToken; 156 @Inject @Main Executor mMainExecutor; 157 @Inject @Background Executor mBackgroundExecutor; 158 @VisibleForTesting 159 boolean mIsRefreshing = false; 160 @VisibleForTesting 161 boolean mNeedRefresh = false; 162 private MediaController mMediaController; 163 @VisibleForTesting InputRouteManager mInputRouteManager; 164 @VisibleForTesting 165 Callback mCallback; 166 @VisibleForTesting 167 LocalMediaManager mLocalMediaManager; 168 @VisibleForTesting 169 MediaOutputMetricLogger mMetricLogger; 170 private int mCurrentState; 171 private FeatureFlags mFeatureFlags; 172 private UserTracker mUserTracker; 173 private VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor; 174 @NonNull private MediaOutputColorScheme mMediaOutputColorScheme; 175 @NonNull private MediaOutputColorSchemeLegacy mMediaOutputColorSchemeLegacy; 176 private boolean mIsGroupListCollapsed = true; 177 178 public enum BroadcastNotifyDialog { 179 ACTION_FIRST_LAUNCH, 180 ACTION_BROADCAST_INFO_ICON 181 } 182 183 @VisibleForTesting 184 final InputRouteManager.InputDeviceCallback mInputDeviceCallback = 185 new InputRouteManager.InputDeviceCallback() { 186 @Override 187 public void onInputDeviceListUpdated(@NonNull List<MediaDevice> devices) { 188 synchronized (mInputMediaDevicesLock) { 189 buildInputMediaItems(devices); 190 mCallback.onDeviceListChanged(); 191 } 192 } 193 }; 194 195 @AssistedInject MediaSwitchingController( Context context, @Assisted String packageName, @Assisted @Nullable UserHandle userHandle, @Assisted @Nullable MediaSession.Token token, MediaSessionManager mediaSessionManager, @Nullable LocalBluetoothManager lbm, ActivityStarter starter, CommonNotifCollection notifCollection, DialogTransitionAnimator dialogTransitionAnimator, NearbyMediaDevicesManager nearbyMediaDevicesManager, AudioManager audioManager, PowerExemptionManager powerExemptionManager, KeyguardManager keyGuardManager, FeatureFlags featureFlags, VolumePanelGlobalStateInteractor volumePanelGlobalStateInteractor, UserTracker userTracker)196 public MediaSwitchingController( 197 Context context, 198 @Assisted String packageName, 199 @Assisted @Nullable UserHandle userHandle, 200 @Assisted @Nullable MediaSession.Token token, 201 MediaSessionManager mediaSessionManager, 202 @Nullable LocalBluetoothManager lbm, 203 ActivityStarter starter, 204 CommonNotifCollection notifCollection, 205 DialogTransitionAnimator dialogTransitionAnimator, 206 NearbyMediaDevicesManager nearbyMediaDevicesManager, 207 AudioManager audioManager, 208 PowerExemptionManager powerExemptionManager, 209 KeyguardManager keyGuardManager, 210 FeatureFlags featureFlags, 211 VolumePanelGlobalStateInteractor volumePanelGlobalStateInteractor, 212 UserTracker userTracker) { 213 mContext = context; 214 mPackageName = packageName; 215 mUserHandle = userHandle; 216 mMediaSessionManager = mediaSessionManager; 217 mLocalBluetoothManager = lbm; 218 mActivityStarter = starter; 219 mNotifCollection = notifCollection; 220 mAudioManager = audioManager; 221 mPowerExemptionManager = powerExemptionManager; 222 mKeyGuardManager = keyGuardManager; 223 mFeatureFlags = featureFlags; 224 mUserTracker = userTracker; 225 mToken = token; 226 mVolumePanelGlobalStateInteractor = volumePanelGlobalStateInteractor; 227 InfoMediaManager imm = 228 InfoMediaManager.createInstance(mContext, packageName, userHandle, lbm, token); 229 mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName); 230 mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName); 231 mOutputMediaItemListProxy = new OutputMediaItemListProxy(context); 232 mDialogTransitionAnimator = dialogTransitionAnimator; 233 mNearbyMediaDevicesManager = nearbyMediaDevicesManager; 234 mMediaOutputColorScheme = MediaOutputColorScheme.fromSystemColors(mContext); 235 mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext); 236 237 if (enableInputRouting()) { 238 mInputRouteManager = new InputRouteManager(mContext, audioManager); 239 } 240 } 241 242 @AssistedFactory 243 public interface Factory { 244 /** Construct a MediaSwitchingController */ create( String packageName, UserHandle userHandle, MediaSession.Token token)245 MediaSwitchingController create( 246 String packageName, UserHandle userHandle, MediaSession.Token token); 247 } 248 start(@onNull Callback cb)249 protected void start(@NonNull Callback cb) { 250 synchronized (mMediaDevicesLock) { 251 mCachedMediaDevices.clear(); 252 mOutputMediaItemListProxy.clear(); 253 } 254 mNearbyDeviceInfoMap.clear(); 255 if (mNearbyMediaDevicesManager != null) { 256 mNearbyMediaDevicesManager.registerNearbyDevicesCallback(this); 257 } 258 if (!TextUtils.isEmpty(mPackageName)) { 259 mMediaController = getMediaController(); 260 if (mMediaController != null) { 261 mMediaController.unregisterCallback(mCb); 262 if (mMediaController.getPlaybackState() != null) { 263 mCurrentState = mMediaController.getPlaybackState().getState(); 264 } 265 mMediaController.registerCallback(mCb); 266 } 267 } 268 if (mMediaController == null) { 269 if (DEBUG) { 270 Log.d(TAG, "No media controller for " + mPackageName); 271 } 272 } 273 mCallback = cb; 274 mLocalMediaManager.registerCallback(this); 275 mLocalMediaManager.startScan(); 276 277 if (enableInputRouting()) { 278 mInputRouteManager.registerCallback(mInputDeviceCallback); 279 } 280 } 281 isRefreshing()282 public boolean isRefreshing() { 283 return mIsRefreshing; 284 } 285 setRefreshing(boolean refreshing)286 public void setRefreshing(boolean refreshing) { 287 mIsRefreshing = refreshing; 288 } 289 stop()290 protected void stop() { 291 if (mMediaController != null) { 292 mMediaController.unregisterCallback(mCb); 293 } 294 mLocalMediaManager.unregisterCallback(this); 295 mLocalMediaManager.stopScan(); 296 synchronized (mMediaDevicesLock) { 297 mCachedMediaDevices.clear(); 298 mOutputMediaItemListProxy.clear(); 299 } 300 if (mNearbyMediaDevicesManager != null) { 301 mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this); 302 } 303 mNearbyDeviceInfoMap.clear(); 304 305 if (enableInputRouting()) { 306 mInputRouteManager.unregisterCallback(mInputDeviceCallback); 307 synchronized (mInputMediaDevicesLock) { 308 mInputMediaItemList.clear(); 309 } 310 } 311 } 312 getMediaController()313 private MediaController getMediaController() { 314 if (mToken != null 315 && com.android.settingslib.media.flags.Flags.usePlaybackInfoForRoutingControls()) { 316 return new MediaController(mContext, mToken); 317 } else { 318 for (NotificationEntry entry : mNotifCollection.getAllNotifs()) { 319 final Notification notification = entry.getSbn().getNotification(); 320 if (notification.isMediaNotification() 321 && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) { 322 MediaSession.Token token = 323 notification.extras.getParcelable( 324 Notification.EXTRA_MEDIA_SESSION, MediaSession.Token.class); 325 return new MediaController(mContext, token); 326 } 327 } 328 for (MediaController controller : 329 mMediaSessionManager.getActiveSessionsForUser( 330 null, mUserTracker.getUserHandle())) { 331 if (TextUtils.equals(controller.getPackageName(), mPackageName)) { 332 return controller; 333 } 334 } 335 return null; 336 } 337 } 338 339 @Override onDeviceListUpdate(List<MediaDevice> devices)340 public void onDeviceListUpdate(List<MediaDevice> devices) { 341 boolean isListEmpty = mOutputMediaItemListProxy.isEmpty(); 342 if (isListEmpty || !mIsRefreshing) { 343 buildMediaItems(devices); 344 mCallback.onDeviceListChanged(); 345 } else { 346 synchronized (mMediaDevicesLock) { 347 mNeedRefresh = true; 348 mCachedMediaDevices.clear(); 349 mCachedMediaDevices.addAll(devices); 350 } 351 } 352 } 353 354 @Override onSelectedDeviceStateChanged( MediaDevice device, @LocalMediaManager.MediaDeviceState int state)355 public void onSelectedDeviceStateChanged( 356 MediaDevice device, @LocalMediaManager.MediaDeviceState int state) { 357 mCallback.onRouteChanged(); 358 mMetricLogger.logOutputItemSuccess( 359 device.toString(), 360 new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList())); 361 } 362 363 @Override onDeviceAttributesChanged()364 public void onDeviceAttributesChanged() { 365 mCallback.onRouteChanged(); 366 } 367 368 @Override onRequestFailed(int reason)369 public void onRequestFailed(int reason) { 370 mCallback.onRouteChanged(); 371 mMetricLogger.logOutputItemFailure( 372 new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList()), reason); 373 } 374 375 /** 376 * Checks if there's any muting expected device exist 377 */ hasMutingExpectedDevice()378 public boolean hasMutingExpectedDevice() { 379 return mAudioManager.getMutingExpectedDevice() != null; 380 } 381 382 /** 383 * Cancels mute await connection action in follow up request 384 */ cancelMuteAwaitConnection()385 public void cancelMuteAwaitConnection() { 386 if (mAudioManager.getMutingExpectedDevice() == null) { 387 return; 388 } 389 try { 390 synchronized (mMediaDevicesLock) { 391 mOutputMediaItemListProxy.removeMutingExpectedDevices(); 392 } 393 mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice()); 394 } catch (Exception e) { 395 Log.d(TAG, "Unable to cancel mute await connection"); 396 } 397 } 398 getAppSourceIconFromPackage()399 Drawable getAppSourceIconFromPackage() { 400 if (TextUtils.isEmpty(mPackageName)) { 401 return null; 402 } 403 try { 404 Log.d(TAG, "try to get app icon"); 405 return mContext.getPackageManager() 406 .getApplicationIcon(mPackageName); 407 } catch (PackageManager.NameNotFoundException e) { 408 Log.d(TAG, "icon not found"); 409 return null; 410 } 411 } 412 getAppSourceName()413 String getAppSourceName() { 414 if (TextUtils.isEmpty(mPackageName)) { 415 return null; 416 } 417 final PackageManager packageManager = mContext.getPackageManager(); 418 ApplicationInfo applicationInfo; 419 try { 420 applicationInfo = packageManager.getApplicationInfo(mPackageName, 421 PackageManager.ApplicationInfoFlags.of(0)); 422 } catch (PackageManager.NameNotFoundException e) { 423 applicationInfo = null; 424 } 425 final String applicationName = 426 (String) (applicationInfo != null ? packageManager.getApplicationLabel( 427 applicationInfo) 428 : mContext.getString(R.string.media_output_dialog_unknown_launch_app_name)); 429 return applicationName; 430 } 431 getAppLaunchIntent()432 Intent getAppLaunchIntent() { 433 if (TextUtils.isEmpty(mPackageName)) { 434 return null; 435 } 436 return mContext.getPackageManager().getLaunchIntentForPackage(mPackageName); 437 } 438 tryToLaunchInAppRoutingIntent(String routeId, View view)439 void tryToLaunchInAppRoutingIntent(String routeId, View view) { 440 ComponentName componentName = mLocalMediaManager.getLinkedItemComponentName(); 441 if (componentName != null) { 442 ActivityTransitionAnimator.Controller controller = 443 mDialogTransitionAnimator.createActivityTransitionController(view); 444 Intent launchIntent = new Intent(ACTION_TRANSFER_MEDIA); 445 launchIntent.setComponent(componentName); 446 launchIntent.putExtra(EXTRA_ROUTE_ID, routeId); 447 launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 448 mCallback.dismissDialog(); 449 startActivity(launchIntent, controller); 450 } 451 } 452 tryToLaunchMediaApplication(View view)453 void tryToLaunchMediaApplication(View view) { 454 ActivityTransitionAnimator.Controller controller = 455 mDialogTransitionAnimator.createActivityTransitionController(view); 456 Intent launchIntent = getAppLaunchIntent(); 457 if (launchIntent != null) { 458 launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 459 mCallback.dismissDialog(); 460 startActivity(launchIntent, controller); 461 } 462 } 463 getHeaderTitle()464 CharSequence getHeaderTitle() { 465 if (mMediaController != null) { 466 final MediaMetadata metadata = mMediaController.getMetadata(); 467 if (metadata != null) { 468 return metadata.getDescription().getTitle(); 469 } 470 } 471 return mContext.getText(R.string.controls_media_title); 472 } 473 getHeaderSubTitle()474 CharSequence getHeaderSubTitle() { 475 if (mMediaController == null) { 476 return null; 477 } 478 final MediaMetadata metadata = mMediaController.getMetadata(); 479 if (metadata == null) { 480 return null; 481 } 482 return metadata.getDescription().getSubtitle(); 483 } 484 getHeaderIcon()485 IconCompat getHeaderIcon() { 486 if (mMediaController == null) { 487 return null; 488 } 489 final MediaMetadata metadata = mMediaController.getMetadata(); 490 if (metadata != null) { 491 final Bitmap bitmap = metadata.getDescription().getIconBitmap(); 492 if (bitmap != null) { 493 final Bitmap roundBitmap = Utils.convertCornerRadiusBitmap(mContext, bitmap, 494 (float) mContext.getResources().getDimensionPixelSize( 495 R.dimen.media_output_dialog_icon_corner_radius)); 496 return IconCompat.createWithBitmap(roundBitmap); 497 } 498 } 499 if (DEBUG) { 500 Log.d(TAG, "Media meta data does not contain icon information"); 501 } 502 return getNotificationIcon(); 503 } 504 getDeviceIconDrawable(MediaDevice device)505 Drawable getDeviceIconDrawable(MediaDevice device) { 506 Drawable drawable = device.getIcon(); 507 if (drawable == null) { 508 if (DEBUG) { 509 Log.d(TAG, "getDeviceIconCompat() device : " + device.getName() 510 + ", drawable is null"); 511 } 512 // Use default Bluetooth device icon to handle getIcon() is null case. 513 drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp); 514 } 515 return drawable; 516 } 517 getDeviceIconCompat(MediaDevice device)518 IconCompat getDeviceIconCompat(MediaDevice device) { 519 return BluetoothUtils.createIconWithDrawable(getDeviceIconDrawable(device)); 520 } 521 setGroupListCollapsed(boolean isCollapsed)522 public void setGroupListCollapsed(boolean isCollapsed) { 523 mIsGroupListCollapsed = isCollapsed; 524 } 525 isGroupListCollapsed()526 public boolean isGroupListCollapsed() { 527 return mIsGroupListCollapsed; 528 } 529 isActiveItem(MediaDevice device)530 boolean isActiveItem(MediaDevice device) { 531 boolean isConnected = mLocalMediaManager.getCurrentConnectedDevice().getId().equals( 532 device.getId()); 533 boolean isSelectedDeviceInGroup = getSelectedMediaDevice().size() > 1 534 && getSelectedMediaDevice().contains(device); 535 return (!hasAdjustVolumeUserRestriction() && isConnected && !isAnyDeviceTransferring()) 536 || isSelectedDeviceInGroup; 537 } 538 getNotificationSmallIcon()539 IconCompat getNotificationSmallIcon() { 540 if (TextUtils.isEmpty(mPackageName)) { 541 return null; 542 } 543 for (NotificationEntry entry : mNotifCollection.getAllNotifs()) { 544 final Notification notification = entry.getSbn().getNotification(); 545 if (notification.isMediaNotification() 546 && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) { 547 final Icon icon = notification.getSmallIcon(); 548 if (icon == null) { 549 break; 550 } 551 return IconCompat.createFromIcon(icon); 552 } 553 } 554 return null; 555 } 556 getNotificationIcon()557 IconCompat getNotificationIcon() { 558 if (TextUtils.isEmpty(mPackageName)) { 559 return null; 560 } 561 for (NotificationEntry entry : mNotifCollection.getAllNotifs()) { 562 final Notification notification = entry.getSbn().getNotification(); 563 if (notification.isMediaNotification() 564 && TextUtils.equals(entry.getSbn().getPackageName(), mPackageName)) { 565 final Icon icon = notification.getLargeIcon(); 566 if (icon == null) { 567 break; 568 } 569 return IconCompat.createFromIcon(icon); 570 } 571 } 572 return null; 573 } 574 updateCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme)575 void updateCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) { 576 ColorScheme currentColorScheme = new ColorScheme(wallpaperColors, 577 isDarkTheme); 578 mMediaOutputColorScheme = MediaOutputColorScheme.fromDynamicColors( 579 currentColorScheme); 580 mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromDynamicColors( 581 currentColorScheme, isDarkTheme); 582 } 583 getColorScheme()584 MediaOutputColorScheme getColorScheme() { 585 return mMediaOutputColorScheme; 586 } 587 getColorSchemeLegacy()588 MediaOutputColorSchemeLegacy getColorSchemeLegacy() { 589 return mMediaOutputColorSchemeLegacy; 590 } 591 refreshDataSetIfNeeded()592 public void refreshDataSetIfNeeded() { 593 if (mNeedRefresh) { 594 buildMediaItems(mCachedMediaDevices); 595 mCallback.onDeviceListChanged(); 596 mNeedRefresh = false; 597 } 598 } 599 buildMediaItems(List<MediaDevice> devices)600 private void buildMediaItems(List<MediaDevice> devices) { 601 synchronized (mMediaDevicesLock) { 602 if (!mLocalMediaManager.isPreferenceRouteListingExist()) { 603 attachRangeInfo(devices); 604 if (Flags.enableOutputSwitcherDeviceGrouping()) { 605 List<MediaDevice> selectedDevices = new ArrayList<>(); 606 Set<String> selectedDeviceIds = 607 getSelectedMediaDevice().stream() 608 .map(MediaDevice::getId) 609 .collect(Collectors.toSet()); 610 for (MediaDevice device : devices) { 611 if (selectedDeviceIds.contains(device.getId())) { 612 selectedDevices.add(device); 613 } 614 } 615 devices.removeAll(selectedDevices); 616 Collections.sort(devices, Comparator.naturalOrder()); 617 devices.addAll(0, selectedDevices); 618 } else { 619 Collections.sort(devices, Comparator.naturalOrder()); 620 } 621 } 622 if (Flags.fixOutputMediaItemListIndexOutOfBoundsException()) { 623 // For the first time building list, to make sure the top device is the connected 624 // device. 625 boolean needToHandleMutingExpectedDevice = 626 hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote(); 627 final MediaDevice connectedMediaDevice = 628 needToHandleMutingExpectedDevice ? null : getCurrentConnectedMediaDevice(); 629 mOutputMediaItemListProxy.updateMediaDevices( 630 devices, 631 getSelectedMediaDevice(), 632 connectedMediaDevice, 633 needToHandleMutingExpectedDevice); 634 } else { 635 List<MediaItem> updatedMediaItems = 636 buildMediaItems( 637 mOutputMediaItemListProxy.getOutputMediaItemList(), devices); 638 mOutputMediaItemListProxy.clearAndAddAll(updatedMediaItems); 639 } 640 } 641 } 642 buildMediaItems( List<MediaItem> oldMediaItems, List<MediaDevice> devices)643 protected List<MediaItem> buildMediaItems( 644 List<MediaItem> oldMediaItems, List<MediaDevice> devices) { 645 synchronized (mMediaDevicesLock) { 646 // For the first time building list, to make sure the top device is the connected 647 // device. 648 boolean needToHandleMutingExpectedDevice = 649 hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote(); 650 final MediaDevice connectedMediaDevice = 651 needToHandleMutingExpectedDevice ? null 652 : getCurrentConnectedMediaDevice(); 653 if (oldMediaItems.isEmpty()) { 654 if (connectedMediaDevice == null) { 655 if (DEBUG) { 656 Log.d(TAG, "No connected media device or muting expected device exist."); 657 } 658 return categorizeMediaItemsLocked( 659 /* connectedMediaDevice */ null, 660 devices, 661 needToHandleMutingExpectedDevice); 662 } else { 663 // selected device exist 664 return categorizeMediaItemsLocked( 665 connectedMediaDevice, 666 devices, 667 /* needToHandleMutingExpectedDevice */ false); 668 } 669 } 670 // To keep the same list order 671 final List<MediaDevice> targetMediaDevices = new ArrayList<>(); 672 final Map<Integer, MediaItem> dividerItems = new HashMap<>(); 673 674 Map<String, MediaDevice> idToMediaDeviceMap = 675 devices.stream() 676 .collect(Collectors.toMap(MediaDevice::getId, Function.identity())); 677 678 for (MediaItem originalMediaItem : oldMediaItems) { 679 switch (originalMediaItem.getMediaItemType()) { 680 case MediaItemType.TYPE_GROUP_DIVIDER -> { 681 dividerItems.put( 682 oldMediaItems.indexOf(originalMediaItem), originalMediaItem); 683 } 684 case MediaItemType.TYPE_DEVICE -> { 685 String originalMediaItemId = 686 originalMediaItem.getMediaDevice().orElseThrow().getId(); 687 if (idToMediaDeviceMap.containsKey(originalMediaItemId)) { 688 targetMediaDevices.add(idToMediaDeviceMap.get(originalMediaItemId)); 689 } 690 } 691 case MediaItemType.TYPE_PAIR_NEW_DEVICE -> { 692 // Do nothing. 693 } 694 } 695 } 696 if (targetMediaDevices.size() != devices.size()) { 697 devices.removeAll(targetMediaDevices); 698 targetMediaDevices.addAll(devices); 699 } 700 List<MediaItem> finalMediaItems = targetMediaDevices.stream() 701 .map(MediaItem::createDeviceMediaItem) 702 .collect(Collectors.toList()); 703 704 boolean shouldAddFirstSeenSelectedDevice = Flags.enableOutputSwitcherDeviceGrouping(); 705 706 if (shouldAddFirstSeenSelectedDevice) { 707 finalMediaItems.clear(); 708 Set<String> selectedDevicesIds = getSelectedMediaDevice().stream() 709 .map(MediaDevice::getId) 710 .collect(Collectors.toSet()); 711 for (MediaDevice targetMediaDevice : targetMediaDevices) { 712 if (shouldAddFirstSeenSelectedDevice 713 && selectedDevicesIds.contains(targetMediaDevice.getId())) { 714 finalMediaItems.add(MediaItem.createDeviceMediaItem( 715 targetMediaDevice, /* isFirstDeviceInGroup */ true)); 716 shouldAddFirstSeenSelectedDevice = false; 717 } else { 718 finalMediaItems.add(MediaItem.createDeviceMediaItem( 719 targetMediaDevice, /* isFirstDeviceInGroup */ false)); 720 } 721 } 722 } 723 dividerItems.forEach(finalMediaItems::add); 724 return finalMediaItems; 725 } 726 } 727 enableInputRouting()728 private boolean enableInputRouting() { 729 return Flags.enableAudioInputDeviceRoutingAndVolumeControl(); 730 } 731 buildInputMediaItems(List<MediaDevice> devices)732 private void buildInputMediaItems(List<MediaDevice> devices) { 733 synchronized (mInputMediaDevicesLock) { 734 List<MediaItem> updatedInputMediaItems = 735 devices.stream().map(MediaItem::createDeviceMediaItem).toList(); 736 mInputMediaItemList.clear(); 737 mInputMediaItemList.addAll(updatedInputMediaItems); 738 } 739 } 740 741 /** 742 * Initial categorization of current devices, will not be called for updates to the devices 743 * list. 744 */ 745 @GuardedBy("mMediaDevicesLock") categorizeMediaItemsLocked( MediaDevice connectedMediaDevice, List<MediaDevice> devices, boolean needToHandleMutingExpectedDevice)746 private List<MediaItem> categorizeMediaItemsLocked( 747 MediaDevice connectedMediaDevice, 748 List<MediaDevice> devices, 749 boolean needToHandleMutingExpectedDevice) { 750 List<MediaItem> finalMediaItems = new ArrayList<>(); 751 Set<String> selectedDevicesIds = getSelectedMediaDevice().stream() 752 .map(MediaDevice::getId) 753 .collect(Collectors.toSet()); 754 if (connectedMediaDevice != null) { 755 selectedDevicesIds.add(connectedMediaDevice.getId()); 756 } 757 boolean groupSelectedDevices = Flags.enableOutputSwitcherDeviceGrouping(); 758 int nextSelectedItemIndex = 0; 759 boolean suggestedDeviceAdded = false; 760 boolean displayGroupAdded = false; 761 boolean selectedDeviceAdded = false; 762 for (MediaDevice device : devices) { 763 if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) { 764 finalMediaItems.add(0, MediaItem.createDeviceMediaItem(device)); 765 nextSelectedItemIndex++; 766 } else if (!needToHandleMutingExpectedDevice && selectedDevicesIds.contains( 767 device.getId())) { 768 if (groupSelectedDevices) { 769 finalMediaItems.add( 770 nextSelectedItemIndex++, 771 MediaItem.createDeviceMediaItem(device, !selectedDeviceAdded)); 772 selectedDeviceAdded = true; 773 } else { 774 finalMediaItems.add(0, MediaItem.createDeviceMediaItem(device)); 775 } 776 } else { 777 if (device.isSuggestedDevice() && !suggestedDeviceAdded) { 778 addSuggestedDeviceGroupDivider(finalMediaItems); 779 suggestedDeviceAdded = true; 780 } else if (!device.isSuggestedDevice() && !displayGroupAdded) { 781 addSpeakersAndDisplaysGroupDivider(finalMediaItems); 782 displayGroupAdded = true; 783 } 784 finalMediaItems.add(MediaItem.createDeviceMediaItem(device)); 785 } 786 } 787 return finalMediaItems; 788 } 789 addSuggestedDeviceGroupDivider(List<MediaItem> mediaItems)790 private void addSuggestedDeviceGroupDivider(List<MediaItem> mediaItems) { 791 mediaItems.add( 792 MediaItem.createGroupDividerMediaItem( 793 mContext.getString(R.string.media_output_group_title_suggested_device))); 794 } 795 addSpeakersAndDisplaysGroupDivider(List<MediaItem> mediaItems)796 private void addSpeakersAndDisplaysGroupDivider(List<MediaItem> mediaItems) { 797 mediaItems.add( 798 MediaItem.createGroupDividerMediaItem( 799 mContext.getString( 800 R.string.media_output_group_title_speakers_and_displays))); 801 } 802 attachConnectNewDeviceItemIfNeeded(List<MediaItem> mediaItems)803 private void attachConnectNewDeviceItemIfNeeded(List<MediaItem> mediaItems) { 804 MediaItem connectNewDeviceItem = getConnectNewDeviceItem(); 805 if (connectNewDeviceItem != null) { 806 mediaItems.add(connectNewDeviceItem); 807 } 808 } 809 810 @NonNull getConnectedSpeakersExpandableGroupDivider()811 MediaItem getConnectedSpeakersExpandableGroupDivider() { 812 return MediaItem.createExpandableGroupDividerMediaItem( 813 mContext.getString(R.string.media_output_group_title_connected_speakers)); 814 } 815 816 @Nullable getConnectNewDeviceItem()817 MediaItem getConnectNewDeviceItem() { 818 boolean isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() == 1; 819 if (enableInputRouting()) { 820 // When input routing is enabled, there are expected to be at least 2 total selected 821 // devices: one output device and one input device. 822 isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() <= 2; 823 } 824 825 // Attach "Connect a device" item only when current output is not remote and not a group 826 return (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup) 827 ? MediaItem.createPairNewDeviceMediaItem() 828 : null; 829 } 830 attachRangeInfo(List<MediaDevice> devices)831 private void attachRangeInfo(List<MediaDevice> devices) { 832 for (MediaDevice mediaDevice : devices) { 833 if (mNearbyDeviceInfoMap.containsKey(mediaDevice.getId())) { 834 mediaDevice.setRangeZone(mNearbyDeviceInfoMap.get(mediaDevice.getId())); 835 } 836 } 837 } 838 isCurrentConnectedDeviceRemote()839 boolean isCurrentConnectedDeviceRemote() { 840 MediaDevice currentConnectedMediaDevice = getCurrentConnectedMediaDevice(); 841 return currentConnectedMediaDevice != null && isActiveRemoteDevice( 842 currentConnectedMediaDevice); 843 } 844 isCurrentOutputDeviceHasSessionOngoing()845 boolean isCurrentOutputDeviceHasSessionOngoing() { 846 MediaDevice currentConnectedMediaDevice = getCurrentConnectedMediaDevice(); 847 return currentConnectedMediaDevice != null 848 && (currentConnectedMediaDevice.isHostForOngoingSession()); 849 } 850 getGroupMediaDevices()851 List<MediaDevice> getGroupMediaDevices() { 852 final List<MediaDevice> selectedDevices = getSelectedMediaDevice(); 853 final List<MediaDevice> selectableDevices = getSelectableMediaDevice(); 854 if (mGroupMediaDevices.isEmpty()) { 855 mGroupMediaDevices.addAll(selectedDevices); 856 mGroupMediaDevices.addAll(selectableDevices); 857 return mGroupMediaDevices; 858 } 859 // To keep the same list order 860 final Collection<MediaDevice> sourceDevices = new ArrayList<>(); 861 final Collection<MediaDevice> targetMediaDevices = new ArrayList<>(); 862 sourceDevices.addAll(selectedDevices); 863 sourceDevices.addAll(selectableDevices); 864 for (MediaDevice originalDevice : mGroupMediaDevices) { 865 for (MediaDevice newDevice : sourceDevices) { 866 if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) { 867 targetMediaDevices.add(newDevice); 868 sourceDevices.remove(newDevice); 869 break; 870 } 871 } 872 } 873 // Add new devices at the end of list if necessary 874 if (!sourceDevices.isEmpty()) { 875 targetMediaDevices.addAll(sourceDevices); 876 } 877 mGroupMediaDevices.clear(); 878 mGroupMediaDevices.addAll(targetMediaDevices); 879 880 return mGroupMediaDevices; 881 } 882 resetGroupMediaDevices()883 void resetGroupMediaDevices() { 884 mGroupMediaDevices.clear(); 885 } 886 connectDevice(MediaDevice device)887 protected void connectDevice(MediaDevice device) { 888 // If input routing is supported and the device is an input device, call mInputRouteManager 889 // to handle routing. 890 if (enableInputRouting() && device instanceof InputMediaDevice) { 891 var unused = 892 ThreadUtils.postOnBackgroundThread( 893 () -> { 894 mInputRouteManager.selectDevice(device); 895 }); 896 return; 897 } 898 899 mMetricLogger.updateOutputEndPoints(getCurrentConnectedMediaDevice(), device); 900 901 ThreadUtils.postOnBackgroundThread(() -> { 902 mLocalMediaManager.connectDevice(device); 903 }); 904 } 905 getOutputDeviceList(boolean addConnectDeviceButton)906 private List<MediaItem> getOutputDeviceList(boolean addConnectDeviceButton) { 907 List<MediaItem> mediaItems = new ArrayList<>( 908 mOutputMediaItemListProxy.getOutputMediaItemList()); 909 if (addConnectDeviceButton) { 910 attachConnectNewDeviceItemIfNeeded(mediaItems); 911 } 912 return mediaItems; 913 } 914 addInputDevices(List<MediaItem> mediaItems)915 private void addInputDevices(List<MediaItem> mediaItems) { 916 mediaItems.add( 917 MediaItem.createGroupDividerMediaItem( 918 mContext.getString(R.string.media_input_group_title))); 919 mediaItems.addAll(mInputMediaItemList); 920 } 921 addOutputDevices(List<MediaItem> mediaItems, boolean addConnectDeviceButton)922 private void addOutputDevices(List<MediaItem> mediaItems, boolean addConnectDeviceButton) { 923 mediaItems.add( 924 MediaItem.createGroupDividerMediaItem( 925 mContext.getString(R.string.media_output_group_title))); 926 mediaItems.addAll(getOutputDeviceList(addConnectDeviceButton)); 927 } 928 929 /** 930 * Returns a list of media items to be rendered in the device list. For backward compatibility 931 * reasons, adds a "Connect a device" button by default. 932 */ getMediaItemList()933 public List<MediaItem> getMediaItemList() { 934 return getMediaItemList(true /* addConnectDeviceButton */); 935 } 936 937 /** 938 * Returns a list of media items to be rendered in the device list. 939 * @param addConnectDeviceButton Whether to add a "Connect a device" button to the list. 940 */ getMediaItemList(boolean addConnectDeviceButton)941 public List<MediaItem> getMediaItemList(boolean addConnectDeviceButton) { 942 // If input routing is not enabled, only return output media items. 943 if (!enableInputRouting()) { 944 return getOutputDeviceList(addConnectDeviceButton); 945 } 946 947 // If input routing is enabled, return both output and input media items. 948 List<MediaItem> mediaItems = new ArrayList<>(); 949 addOutputDevices(mediaItems, addConnectDeviceButton); 950 addInputDevices(mediaItems); 951 return mediaItems; 952 } 953 getCurrentConnectedMediaDevice()954 public MediaDevice getCurrentConnectedMediaDevice() { 955 return mLocalMediaManager.getCurrentConnectedDevice(); 956 } 957 958 @VisibleForTesting clearMediaItemList()959 void clearMediaItemList() { 960 mOutputMediaItemListProxy.clear(); 961 } 962 addDeviceToPlayMedia(MediaDevice device)963 boolean addDeviceToPlayMedia(MediaDevice device) { 964 mMetricLogger.logInteractionExpansion(device); 965 return mLocalMediaManager.addDeviceToPlayMedia(device); 966 } 967 removeDeviceFromPlayMedia(MediaDevice device)968 boolean removeDeviceFromPlayMedia(MediaDevice device) { 969 return mLocalMediaManager.removeDeviceFromPlayMedia(device); 970 } 971 getSelectableMediaDevice()972 List<MediaDevice> getSelectableMediaDevice() { 973 return mLocalMediaManager.getSelectableMediaDevice(); 974 } 975 getTransferableMediaDevices()976 List<MediaDevice> getTransferableMediaDevices() { 977 return mLocalMediaManager.getTransferableMediaDevices(); 978 } 979 getSelectedMediaDevice()980 public List<MediaDevice> getSelectedMediaDevice() { 981 if (!enableInputRouting()) { 982 return mLocalMediaManager.getSelectedMediaDevice(); 983 } 984 985 // Add selected input device if input routing is supported. 986 List<MediaDevice> selectedDevices = 987 new ArrayList<>(mLocalMediaManager.getSelectedMediaDevice()); 988 MediaDevice selectedInputDevice = mInputRouteManager.getSelectedInputDevice(); 989 if (selectedInputDevice != null) { 990 selectedDevices.add(selectedInputDevice); 991 } 992 return selectedDevices; 993 } 994 getDeselectableMediaDevice()995 List<MediaDevice> getDeselectableMediaDevice() { 996 return mLocalMediaManager.getDeselectableMediaDevice(); 997 } 998 adjustSessionVolume(int volume)999 void adjustSessionVolume(int volume) { 1000 mLocalMediaManager.adjustSessionVolume(volume); 1001 } 1002 getSessionVolumeMax()1003 int getSessionVolumeMax() { 1004 return mLocalMediaManager.getSessionVolumeMax(); 1005 } 1006 getSessionVolume()1007 int getSessionVolume() { 1008 return mLocalMediaManager.getSessionVolume(); 1009 } 1010 1011 @Nullable getSessionName()1012 CharSequence getSessionName() { 1013 return mLocalMediaManager.getSessionName(); 1014 } 1015 releaseSession()1016 void releaseSession() { 1017 mMetricLogger.logInteractionStopCasting(); 1018 mLocalMediaManager.releaseSession(); 1019 } 1020 getActiveRemoteMediaDevices()1021 List<RoutingSessionInfo> getActiveRemoteMediaDevices() { 1022 return new ArrayList<>(mLocalMediaManager.getRemoteRoutingSessions()); 1023 } 1024 adjustVolume(MediaDevice device, int volume)1025 void adjustVolume(MediaDevice device, int volume) { 1026 ThreadUtils.postOnBackgroundThread(() -> { 1027 mLocalMediaManager.adjustDeviceVolume(device, volume); 1028 }); 1029 } 1030 logInteractionAdjustVolume(MediaDevice device)1031 void logInteractionAdjustVolume(MediaDevice device) { 1032 mMetricLogger.logInteractionAdjustVolume(device); 1033 } 1034 logInteractionMuteDevice(MediaDevice device)1035 void logInteractionMuteDevice(MediaDevice device) { 1036 mMetricLogger.logInteractionMute(device); 1037 } 1038 logInteractionUnmuteDevice(MediaDevice device)1039 void logInteractionUnmuteDevice(MediaDevice device) { 1040 mMetricLogger.logInteractionUnmute(device); 1041 } 1042 hasAdjustVolumeUserRestriction()1043 boolean hasAdjustVolumeUserRestriction() { 1044 if (RestrictedLockUtilsInternal.checkIfRestrictionEnforced( 1045 mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId()) != null) { 1046 return true; 1047 } 1048 final UserManager um = mContext.getSystemService(UserManager.class); 1049 return um.hasBaseUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME, 1050 UserHandle.of(UserHandle.myUserId())); 1051 } 1052 isAnyDeviceTransferring()1053 public boolean isAnyDeviceTransferring() { 1054 synchronized (mMediaDevicesLock) { 1055 for (MediaItem mediaItem : mOutputMediaItemListProxy.getOutputMediaItemList()) { 1056 if (mediaItem.getMediaDevice().isPresent() 1057 && mediaItem.getMediaDevice().get().getState() 1058 == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) { 1059 return true; 1060 } 1061 } 1062 } 1063 return false; 1064 } 1065 launchBluetoothPairing(View view)1066 void launchBluetoothPairing(View view) { 1067 ActivityTransitionAnimator.Controller controller = 1068 mDialogTransitionAnimator.createActivityTransitionController(view); 1069 1070 if (controller == null || (mKeyGuardManager != null 1071 && mKeyGuardManager.isKeyguardLocked())) { 1072 mCallback.dismissDialog(); 1073 } 1074 1075 Intent launchIntent = 1076 new Intent(ACTION_BLUETOOTH_SETTINGS) 1077 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 1078 final Intent deepLinkIntent = 1079 new Intent(Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY); 1080 if (deepLinkIntent.resolveActivity(mContext.getPackageManager()) != null) { 1081 Log.d(TAG, "Device support split mode, launch page with deep link"); 1082 deepLinkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1083 deepLinkIntent.putExtra( 1084 Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI, 1085 launchIntent.toUri(Intent.URI_INTENT_SCHEME)); 1086 deepLinkIntent.putExtra( 1087 Settings.EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY, 1088 PAGE_CONNECTED_DEVICES_KEY); 1089 startActivity(deepLinkIntent, controller); 1090 return; 1091 } 1092 startActivity(launchIntent, controller); 1093 } 1094 launchLeBroadcastNotifyDialog( View mediaOutputDialog, BroadcastSender broadcastSender, BroadcastNotifyDialog action, final DialogInterface.OnClickListener listener)1095 void launchLeBroadcastNotifyDialog( 1096 View mediaOutputDialog, 1097 BroadcastSender broadcastSender, 1098 BroadcastNotifyDialog action, 1099 final DialogInterface.OnClickListener listener) { 1100 final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); 1101 switch (action) { 1102 case ACTION_FIRST_LAUNCH: 1103 builder.setTitle(R.string.media_output_first_broadcast_title); 1104 builder.setMessage(R.string.media_output_first_notify_broadcast_message); 1105 builder.setNegativeButton(android.R.string.cancel, null); 1106 builder.setPositiveButton(R.string.media_output_broadcast, listener); 1107 break; 1108 case ACTION_BROADCAST_INFO_ICON: 1109 builder.setTitle(R.string.media_output_broadcast); 1110 builder.setMessage(R.string.media_output_broadcasting_message); 1111 builder.setPositiveButton(android.R.string.ok, null); 1112 break; 1113 } 1114 1115 final AlertDialog dialog = builder.create(); 1116 dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); 1117 SystemUIDialog.setShowForAllUsers(dialog, true); 1118 SystemUIDialog.registerDismissListener(dialog); 1119 dialog.show(); 1120 } 1121 launchMediaOutputBroadcastDialog(View mediaOutputDialog, BroadcastSender broadcastSender)1122 void launchMediaOutputBroadcastDialog(View mediaOutputDialog, BroadcastSender broadcastSender) { 1123 MediaSwitchingController controller = 1124 new MediaSwitchingController( 1125 mContext, 1126 mPackageName, 1127 mUserHandle, 1128 mToken, 1129 mMediaSessionManager, 1130 mLocalBluetoothManager, 1131 mActivityStarter, 1132 mNotifCollection, 1133 mDialogTransitionAnimator, 1134 mNearbyMediaDevicesManager, 1135 mAudioManager, 1136 mPowerExemptionManager, 1137 mKeyGuardManager, 1138 mFeatureFlags, 1139 mVolumePanelGlobalStateInteractor, 1140 mUserTracker); 1141 MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true, 1142 broadcastSender, controller, mMainExecutor, mBackgroundExecutor); 1143 dialog.show(); 1144 } 1145 getBroadcastName()1146 String getBroadcastName() { 1147 LocalBluetoothLeBroadcast broadcast = 1148 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1149 if (broadcast == null) { 1150 Log.d(TAG, "getBroadcastName: LE Audio Broadcast is null"); 1151 return ""; 1152 } 1153 return broadcast.getProgramInfo(); 1154 } 1155 setBroadcastName(String broadcastName)1156 void setBroadcastName(String broadcastName) { 1157 LocalBluetoothLeBroadcast broadcast = 1158 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1159 if (broadcast == null) { 1160 Log.d(TAG, "setBroadcastName: LE Audio Broadcast is null"); 1161 return; 1162 } 1163 broadcast.setProgramInfo(broadcastName); 1164 } 1165 getBroadcastCode()1166 String getBroadcastCode() { 1167 LocalBluetoothLeBroadcast broadcast = 1168 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1169 if (broadcast == null) { 1170 Log.d(TAG, "getBroadcastCode: LE Audio Broadcast is null"); 1171 return ""; 1172 } 1173 return new String(broadcast.getBroadcastCode(), StandardCharsets.UTF_8); 1174 } 1175 setBroadcastCode(String broadcastCode)1176 void setBroadcastCode(String broadcastCode) { 1177 LocalBluetoothLeBroadcast broadcast = 1178 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1179 if (broadcast == null) { 1180 Log.d(TAG, "setBroadcastCode: LE Audio Broadcast is null"); 1181 return; 1182 } 1183 broadcast.setBroadcastCode(broadcastCode.getBytes(StandardCharsets.UTF_8)); 1184 } 1185 setTemporaryAllowListExceptionIfNeeded(MediaDevice targetDevice)1186 protected void setTemporaryAllowListExceptionIfNeeded(MediaDevice targetDevice) { 1187 if (mPowerExemptionManager == null || mPackageName == null) { 1188 Log.w(TAG, "powerExemptionManager or package name is null"); 1189 return; 1190 } 1191 mPowerExemptionManager.addToTemporaryAllowList(mPackageName, 1192 PowerExemptionManager.REASON_MEDIA_NOTIFICATION_TRANSFER, 1193 ALLOWLIST_REASON, 1194 ALLOWLIST_DURATION_MS); 1195 } 1196 getLocalBroadcastMetadataQrCodeString()1197 String getLocalBroadcastMetadataQrCodeString() { 1198 LocalBluetoothLeBroadcast broadcast = 1199 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1200 if (broadcast == null) { 1201 Log.d(TAG, "getLocalBroadcastMetadataQrCodeString: LE Audio Broadcast is null"); 1202 return ""; 1203 } 1204 final LocalBluetoothLeBroadcastMetadata metadata = 1205 broadcast.getLocalBluetoothLeBroadcastMetaData(); 1206 return metadata != null ? metadata.convertToQrCodeString() : ""; 1207 } 1208 getBroadcastMetadata()1209 BluetoothLeBroadcastMetadata getBroadcastMetadata() { 1210 LocalBluetoothLeBroadcast broadcast = 1211 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1212 if (broadcast == null) { 1213 Log.d(TAG, "getBroadcastMetadata: LE Audio Broadcast is null"); 1214 return null; 1215 } 1216 1217 return broadcast.getLatestBluetoothLeBroadcastMetadata(); 1218 } 1219 isActiveRemoteDevice(@onNull MediaDevice device)1220 boolean isActiveRemoteDevice(@NonNull MediaDevice device) { 1221 final List<String> features = device.getFeatures(); 1222 return (features.contains(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK) 1223 || features.contains(MediaRoute2Info.FEATURE_REMOTE_AUDIO_PLAYBACK) 1224 || features.contains(MediaRoute2Info.FEATURE_REMOTE_VIDEO_PLAYBACK) 1225 || features.contains(MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK)); 1226 } 1227 isBluetoothLeDevice(@onNull MediaDevice device)1228 boolean isBluetoothLeDevice(@NonNull MediaDevice device) { 1229 return device.isBLEDevice(); 1230 } 1231 isBroadcastSupported()1232 boolean isBroadcastSupported() { 1233 LocalBluetoothLeBroadcast broadcast = 1234 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1235 return broadcast != null; 1236 } 1237 isBluetoothLeBroadcastEnabled()1238 boolean isBluetoothLeBroadcastEnabled() { 1239 LocalBluetoothLeBroadcast broadcast = 1240 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1241 if (broadcast == null) { 1242 return false; 1243 } 1244 return broadcast.isEnabled(null); 1245 } 1246 startBluetoothLeBroadcast()1247 boolean startBluetoothLeBroadcast() { 1248 LocalBluetoothLeBroadcast broadcast = 1249 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1250 if (broadcast == null) { 1251 Log.d(TAG, "The broadcast profile is null"); 1252 return false; 1253 } 1254 broadcast.startBroadcast(getAppSourceName(), /*language*/ null); 1255 return true; 1256 } 1257 stopBluetoothLeBroadcast()1258 boolean stopBluetoothLeBroadcast() { 1259 LocalBluetoothLeBroadcast broadcast = 1260 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1261 if (broadcast == null) { 1262 Log.d(TAG, "The broadcast profile is null"); 1263 return false; 1264 } 1265 broadcast.stopLatestBroadcast(); 1266 return true; 1267 } 1268 updateBluetoothLeBroadcast()1269 boolean updateBluetoothLeBroadcast() { 1270 LocalBluetoothLeBroadcast broadcast = 1271 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1272 if (broadcast == null) { 1273 Log.d(TAG, "The broadcast profile is null"); 1274 return false; 1275 } 1276 broadcast.updateBroadcast(getAppSourceName(), /*language*/ null); 1277 return true; 1278 } 1279 registerLeBroadcastServiceCallback( @onNull @allbackExecutor Executor executor, @NonNull BluetoothLeBroadcast.Callback callback)1280 void registerLeBroadcastServiceCallback( 1281 @NonNull @CallbackExecutor Executor executor, 1282 @NonNull BluetoothLeBroadcast.Callback callback) { 1283 LocalBluetoothLeBroadcast broadcast = 1284 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1285 if (broadcast == null) { 1286 Log.d(TAG, "The broadcast profile is null"); 1287 return; 1288 } 1289 Log.d(TAG, "Register LE broadcast callback"); 1290 broadcast.registerServiceCallBack(executor, callback); 1291 } 1292 unregisterLeBroadcastServiceCallback( @onNull BluetoothLeBroadcast.Callback callback)1293 void unregisterLeBroadcastServiceCallback( 1294 @NonNull BluetoothLeBroadcast.Callback callback) { 1295 LocalBluetoothLeBroadcast broadcast = 1296 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile(); 1297 if (broadcast == null) { 1298 Log.d(TAG, "The broadcast profile is null"); 1299 return; 1300 } 1301 Log.d(TAG, "Unregister LE broadcast callback"); 1302 broadcast.unregisterServiceCallBack(callback); 1303 } 1304 getConnectedBroadcastSinkDevices()1305 List<BluetoothDevice> getConnectedBroadcastSinkDevices() { 1306 LocalBluetoothLeBroadcastAssistant assistant = 1307 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 1308 if (assistant == null) { 1309 Log.d(TAG, "getConnectedBroadcastSinkDevices: The broadcast assistant profile is null"); 1310 return null; 1311 } 1312 1313 return assistant.getConnectedDevices(); 1314 } 1315 isThereAnyBroadcastSourceIntoSinkDevice(BluetoothDevice sink)1316 boolean isThereAnyBroadcastSourceIntoSinkDevice(BluetoothDevice sink) { 1317 LocalBluetoothLeBroadcastAssistant assistant = 1318 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 1319 if (assistant == null) { 1320 Log.d(TAG, "isThereAnyBroadcastSourceIntoSinkDevice: The broadcast assistant profile " 1321 + "is null"); 1322 return false; 1323 } 1324 List<BluetoothLeBroadcastReceiveState> sourceList = assistant.getAllSources(sink); 1325 Log.d(TAG, "isThereAnyBroadcastSourceIntoSinkDevice: List size: " + sourceList.size()); 1326 return !sourceList.isEmpty(); 1327 } 1328 addSourceIntoSinkDeviceWithBluetoothLeAssistant( BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp)1329 boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant( 1330 BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) { 1331 LocalBluetoothLeBroadcastAssistant assistant = 1332 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 1333 if (assistant == null) { 1334 Log.d(TAG, "addSourceIntoSinkDeviceWithBluetoothLeAssistant: The broadcast assistant " 1335 + "profile is null"); 1336 return false; 1337 } 1338 assistant.addSource(sink, metadata, isGroupOp); 1339 return true; 1340 } 1341 registerLeBroadcastAssistantServiceCallback( @onNull @allbackExecutor Executor executor, @NonNull BluetoothLeBroadcastAssistant.Callback callback)1342 void registerLeBroadcastAssistantServiceCallback( 1343 @NonNull @CallbackExecutor Executor executor, 1344 @NonNull BluetoothLeBroadcastAssistant.Callback callback) { 1345 LocalBluetoothLeBroadcastAssistant assistant = 1346 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 1347 if (assistant == null) { 1348 Log.d(TAG, "registerLeBroadcastAssistantServiceCallback: The broadcast assistant " 1349 + "profile is null"); 1350 return; 1351 } 1352 Log.d(TAG, "Register LE broadcast assistant callback"); 1353 assistant.registerServiceCallBack(executor, callback); 1354 } 1355 unregisterLeBroadcastAssistantServiceCallback( @onNull BluetoothLeBroadcastAssistant.Callback callback)1356 void unregisterLeBroadcastAssistantServiceCallback( 1357 @NonNull BluetoothLeBroadcastAssistant.Callback callback) { 1358 LocalBluetoothLeBroadcastAssistant assistant = 1359 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 1360 if (assistant == null) { 1361 Log.d(TAG, "unregisterLeBroadcastAssistantServiceCallback: The broadcast assistant " 1362 + "profile is null"); 1363 return; 1364 } 1365 Log.d(TAG, "Unregister LE broadcast assistant callback"); 1366 assistant.unregisterServiceCallBack(callback); 1367 } 1368 isPlaying()1369 boolean isPlaying() { 1370 if (mMediaController == null) { 1371 return false; 1372 } 1373 1374 PlaybackState state = mMediaController.getPlaybackState(); 1375 if (state == null) { 1376 return false; 1377 } 1378 1379 return (state.getState() == PlaybackState.STATE_PLAYING); 1380 } 1381 isVolumeControlEnabled(@onNull MediaDevice device)1382 boolean isVolumeControlEnabled(@NonNull MediaDevice device) { 1383 return !device.isVolumeFixed(); 1384 } 1385 isVolumeControlEnabledForSession()1386 boolean isVolumeControlEnabledForSession() { 1387 return mLocalMediaManager.isMediaSessionAvailableForVolumeControl(); 1388 } 1389 startActivity(Intent intent, ActivityTransitionAnimator.Controller controller)1390 private void startActivity(Intent intent, ActivityTransitionAnimator.Controller controller) { 1391 // Media Output dialog can be shown from the volume panel. This makes sure the panel is 1392 // closed when navigating to another activity, so it doesn't stays on top of it 1393 mVolumePanelGlobalStateInteractor.setVisible(false); 1394 mActivityStarter.startActivity(intent, true, controller); 1395 } 1396 1397 @Override onDevicesUpdated(List<NearbyDevice> nearbyDevices)1398 public void onDevicesUpdated(List<NearbyDevice> nearbyDevices) throws RemoteException { 1399 mNearbyDeviceInfoMap.clear(); 1400 for (NearbyDevice nearbyDevice : nearbyDevices) { 1401 mNearbyDeviceInfoMap.put(nearbyDevice.getMediaRoute2Id(), nearbyDevice.getRangeZone()); 1402 } 1403 mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this); 1404 } 1405 1406 @Override asBinder()1407 public IBinder asBinder() { 1408 return null; 1409 } 1410 1411 @VisibleForTesting 1412 final MediaController.Callback mCb = new MediaController.Callback() { 1413 @Override 1414 public void onMetadataChanged(MediaMetadata metadata) { 1415 mCallback.onMediaChanged(); 1416 } 1417 1418 @Override 1419 public void onPlaybackStateChanged(PlaybackState playbackState) { 1420 final int newState = 1421 playbackState == null ? PlaybackState.STATE_STOPPED : playbackState.getState(); 1422 if (mCurrentState == newState) { 1423 return; 1424 } 1425 1426 if (newState == PlaybackState.STATE_STOPPED) { 1427 mCallback.onMediaStoppedOrPaused(); 1428 } 1429 mCurrentState = newState; 1430 } 1431 }; 1432 1433 public interface Callback { 1434 /** 1435 * Override to handle the media content updating. 1436 */ onMediaChanged()1437 void onMediaChanged(); 1438 1439 /** 1440 * Override to handle the media state updating. 1441 */ onMediaStoppedOrPaused()1442 void onMediaStoppedOrPaused(); 1443 1444 /** 1445 * Override to handle the device status or attributes updating. 1446 */ onRouteChanged()1447 void onRouteChanged(); 1448 1449 /** 1450 * Override to handle the devices set updating. 1451 */ onDeviceListChanged()1452 void onDeviceListChanged(); 1453 1454 /** 1455 * Override to dismiss dialog. 1456 */ dismissDialog()1457 void dismissDialog(); 1458 } 1459 } 1460