1 /* 2 * Copyright (C) 2024 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.accessibility.hearingaid; 18 19 import static android.view.View.GONE; 20 import static android.view.View.VISIBLE; 21 22 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; 23 24 import static java.util.Collections.emptyList; 25 26 import android.bluetooth.BluetoothHapClient; 27 import android.bluetooth.BluetoothHapPresetInfo; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.PackageManager; 31 import android.content.pm.ResolveInfo; 32 import android.content.res.Resources; 33 import android.media.AudioManager; 34 import android.os.Bundle; 35 import android.provider.Settings; 36 import android.util.Log; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.accessibility.AccessibilityNodeInfo; 41 import android.widget.AdapterView; 42 import android.widget.Button; 43 import android.widget.ImageView; 44 import android.widget.LinearLayout; 45 import android.widget.Space; 46 import android.widget.Spinner; 47 import android.widget.TextView; 48 import android.widget.Toast; 49 50 import androidx.annotation.NonNull; 51 import androidx.annotation.Nullable; 52 import androidx.annotation.VisibleForTesting; 53 import androidx.annotation.WorkerThread; 54 import androidx.recyclerview.widget.LinearLayoutManager; 55 import androidx.recyclerview.widget.RecyclerView; 56 57 import com.android.settingslib.bluetooth.AmbientVolumeUiController; 58 import com.android.settingslib.bluetooth.BluetoothCallback; 59 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 60 import com.android.settingslib.bluetooth.LocalBluetoothManager; 61 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 62 import com.android.systemui.accessibility.hearingaid.HearingDevicesListAdapter.HearingDeviceItemCallback; 63 import com.android.systemui.animation.DialogTransitionAnimator; 64 import com.android.systemui.bluetooth.qsdialog.ActiveHearingDeviceItemFactory; 65 import com.android.systemui.bluetooth.qsdialog.AvailableHearingDeviceItemFactory; 66 import com.android.systemui.bluetooth.qsdialog.ConnectedHearingDeviceItemFactory; 67 import com.android.systemui.bluetooth.qsdialog.DeviceItem; 68 import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory; 69 import com.android.systemui.bluetooth.qsdialog.DeviceItemType; 70 import com.android.systemui.bluetooth.qsdialog.SavedHearingDeviceItemFactory; 71 import com.android.systemui.dagger.qualifiers.Background; 72 import com.android.systemui.dagger.qualifiers.Main; 73 import com.android.systemui.plugins.ActivityStarter; 74 import com.android.systemui.qs.shared.QSSettingsPackageRepository; 75 import com.android.systemui.res.R; 76 import com.android.systemui.statusbar.phone.SystemUIDialog; 77 78 import dagger.assisted.Assisted; 79 import dagger.assisted.AssistedFactory; 80 import dagger.assisted.AssistedInject; 81 82 import java.util.ArrayList; 83 import java.util.List; 84 import java.util.Objects; 85 import java.util.concurrent.Executor; 86 import java.util.stream.Collectors; 87 88 /** 89 * Dialog for showing hearing devices controls. 90 */ 91 public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, 92 HearingDeviceItemCallback, BluetoothCallback { 93 94 private static final String TAG = "HearingDevicesDialogDelegate"; 95 @VisibleForTesting 96 static final String ACTION_BLUETOOTH_DEVICE_DETAILS = 97 "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"; 98 private static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"; 99 private static final String KEY_BLUETOOTH_ADDRESS = "device_address"; 100 @VisibleForTesting 101 static final Intent LIVE_CAPTION_INTENT = new Intent( 102 "com.android.settings.action.live_caption"); 103 104 private final SystemUIDialog.Factory mSystemUIDialogFactory; 105 private final DialogTransitionAnimator mDialogTransitionAnimator; 106 private final ActivityStarter mActivityStarter; 107 private final LocalBluetoothManager mLocalBluetoothManager; 108 private final Executor mMainExecutor; 109 private final Executor mBgExecutor; 110 private final AudioManager mAudioManager; 111 private final LocalBluetoothProfileManager mProfileManager; 112 private final HearingDevicesUiEventLogger mUiEventLogger; 113 private final boolean mShowPairNewDevice; 114 private final int mLaunchSourceId; 115 private final QSSettingsPackageRepository mQSSettingsPackageRepository; 116 117 private SystemUIDialog mDialog; 118 private HearingDevicesListAdapter mDeviceListAdapter; 119 120 private View mPresetLayout; 121 private Spinner mPresetSpinner; 122 private HearingDevicesPresetsController mPresetController; 123 private HearingDevicesSpinnerAdapter mPresetInfoAdapter; 124 125 private View mInputRoutingLayout; 126 private Spinner mInputRoutingSpinner; 127 private HearingDevicesInputRoutingController.Factory mInputRoutingControllerFactory; 128 private HearingDevicesInputRoutingController mInputRoutingController; 129 private HearingDevicesSpinnerAdapter mInputRoutingAdapter; 130 131 private final HearingDevicesPresetsController.PresetCallback mPresetCallback = 132 new HearingDevicesPresetsController.PresetCallback() { 133 @Override 134 public void onPresetInfoUpdated(List<BluetoothHapPresetInfo> presetInfos, 135 int activePresetIndex) { 136 mMainExecutor.execute( 137 () -> refreshPresetUi(presetInfos, activePresetIndex)); 138 } 139 140 @Override 141 public void onPresetCommandFailed(int reason) { 142 mPresetController.refreshPresetInfo(); 143 mMainExecutor.execute(() -> { 144 showErrorToast(R.string.hearing_devices_presets_error); 145 }); 146 } 147 }; 148 149 private AmbientVolumeUiController mAmbientController; 150 151 private final List<DeviceItemFactory> mHearingDeviceItemFactoryList = List.of( 152 new ActiveHearingDeviceItemFactory(), 153 new AvailableHearingDeviceItemFactory(), 154 new ConnectedHearingDeviceItemFactory(), 155 new SavedHearingDeviceItemFactory() 156 ); 157 158 /** Factory to create a {@link HearingDevicesDialogDelegate} dialog instance. */ 159 @AssistedFactory 160 public interface Factory { 161 /** Create a {@link HearingDevicesDialogDelegate} instance */ create( boolean showPairNewDevice, @HearingDevicesUiEventLogger.LaunchSourceId int launchSource)162 HearingDevicesDialogDelegate create( 163 boolean showPairNewDevice, 164 @HearingDevicesUiEventLogger.LaunchSourceId int launchSource); 165 } 166 167 @AssistedInject HearingDevicesDialogDelegate( @ssisted boolean showPairNewDevice, @Assisted @HearingDevicesUiEventLogger.LaunchSourceId int launchSourceId, SystemUIDialog.Factory systemUIDialogFactory, ActivityStarter activityStarter, DialogTransitionAnimator dialogTransitionAnimator, @Nullable LocalBluetoothManager localBluetoothManager, @Main Executor mainExecutor, @Background Executor bgExecutor, AudioManager audioManager, HearingDevicesUiEventLogger uiEventLogger, QSSettingsPackageRepository qsSettingsPackageRepository, HearingDevicesInputRoutingController.Factory inputRoutingControllerFactory)168 public HearingDevicesDialogDelegate( 169 @Assisted boolean showPairNewDevice, 170 @Assisted @HearingDevicesUiEventLogger.LaunchSourceId int launchSourceId, 171 SystemUIDialog.Factory systemUIDialogFactory, 172 ActivityStarter activityStarter, 173 DialogTransitionAnimator dialogTransitionAnimator, 174 @Nullable LocalBluetoothManager localBluetoothManager, 175 @Main Executor mainExecutor, 176 @Background Executor bgExecutor, 177 AudioManager audioManager, 178 HearingDevicesUiEventLogger uiEventLogger, 179 QSSettingsPackageRepository qsSettingsPackageRepository, 180 HearingDevicesInputRoutingController.Factory inputRoutingControllerFactory) { 181 mShowPairNewDevice = showPairNewDevice; 182 mSystemUIDialogFactory = systemUIDialogFactory; 183 mActivityStarter = activityStarter; 184 mDialogTransitionAnimator = dialogTransitionAnimator; 185 mLocalBluetoothManager = localBluetoothManager; 186 mMainExecutor = mainExecutor; 187 mBgExecutor = bgExecutor; 188 mAudioManager = audioManager; 189 mProfileManager = localBluetoothManager.getProfileManager(); 190 mUiEventLogger = uiEventLogger; 191 mLaunchSourceId = launchSourceId; 192 mQSSettingsPackageRepository = qsSettingsPackageRepository; 193 mInputRoutingControllerFactory = inputRoutingControllerFactory; 194 } 195 196 @Override createDialog()197 public SystemUIDialog createDialog() { 198 SystemUIDialog dialog = mSystemUIDialogFactory.create(this); 199 dismissDialogIfExists(); 200 mDialog = dialog; 201 202 return dialog; 203 } 204 205 @Override onDeviceItemGearClicked(@onNull DeviceItem deviceItem, @NonNull View view)206 public void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view) { 207 mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK, mLaunchSourceId); 208 dismissDialogIfExists(); 209 Bundle bundle = new Bundle(); 210 bundle.putString(KEY_BLUETOOTH_ADDRESS, deviceItem.getCachedBluetoothDevice().getAddress()); 211 Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS) 212 .setPackage(mQSSettingsPackageRepository.getSettingsPackageName()) 213 .putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle); 214 mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0, 215 mDialogTransitionAnimator.createActivityTransitionController(view)); 216 } 217 218 @Override onDeviceItemClicked(@onNull DeviceItem deviceItem, @NonNull View view)219 public void onDeviceItemClicked(@NonNull DeviceItem deviceItem, @NonNull View view) { 220 CachedBluetoothDevice cachedBluetoothDevice = deviceItem.getCachedBluetoothDevice(); 221 switch (deviceItem.getType()) { 222 case ACTIVE_MEDIA_BLUETOOTH_DEVICE, CONNECTED_BLUETOOTH_DEVICE -> { 223 mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DISCONNECT, 224 mLaunchSourceId); 225 cachedBluetoothDevice.disconnect(); 226 } 227 case AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> { 228 mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_SET_ACTIVE, 229 mLaunchSourceId); 230 cachedBluetoothDevice.setActive(); 231 } 232 case SAVED_BLUETOOTH_DEVICE -> { 233 mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_CONNECT, mLaunchSourceId); 234 cachedBluetoothDevice.connect(); 235 } 236 } 237 } 238 239 @Override onActiveDeviceChanged(@ullable CachedBluetoothDevice activeDevice, int bluetoothProfile)240 public void onActiveDeviceChanged(@Nullable CachedBluetoothDevice activeDevice, 241 int bluetoothProfile) { 242 List<DeviceItem> hearingDeviceItemList = getHearingDeviceItemList(); 243 refreshDeviceUi(hearingDeviceItemList); 244 mMainExecutor.execute(() -> { 245 CachedBluetoothDevice device = getActiveHearingDevice(hearingDeviceItemList); 246 if (mPresetController != null) { 247 mPresetController.setDevice(device); 248 mPresetLayout.setVisibility( 249 mPresetController.isPresetControlAvailable() ? VISIBLE : GONE); 250 } 251 if (mInputRoutingController != null) { 252 mInputRoutingController.setDevice(device); 253 mInputRoutingController.isInputRoutingControlAvailable( 254 available -> mMainExecutor.execute(() -> mInputRoutingLayout.setVisibility( 255 available ? VISIBLE : GONE))); 256 } 257 if (mAmbientController != null) { 258 mAmbientController.loadDevice(device); 259 } 260 }); 261 } 262 263 @Override onProfileConnectionStateChanged(@onNull CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile)264 public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice, 265 int state, int bluetoothProfile) { 266 List<DeviceItem> hearingDeviceItemList = getHearingDeviceItemList(); 267 refreshDeviceUi(hearingDeviceItemList); 268 } 269 270 @Override onAclConnectionStateChanged(@onNull CachedBluetoothDevice cachedDevice, int state)271 public void onAclConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice, 272 int state) { 273 List<DeviceItem> hearingDeviceItemList = getHearingDeviceItemList(); 274 refreshDeviceUi(hearingDeviceItemList); 275 } 276 277 @Override beforeCreate(@onNull SystemUIDialog dialog, @Nullable Bundle savedInstanceState)278 public void beforeCreate(@NonNull SystemUIDialog dialog, @Nullable Bundle savedInstanceState) { 279 dialog.setTitle(R.string.quick_settings_hearing_devices_dialog_title); 280 dialog.setView(LayoutInflater.from(dialog.getContext()).inflate( 281 R.layout.hearing_devices_tile_dialog, null)); 282 dialog.setNegativeButton( 283 R.string.hearing_devices_settings_button, 284 (dialogInterface, which) -> { 285 mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_SETTINGS_CLICK, 286 mLaunchSourceId); 287 final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS) 288 .putExtra(Intent.EXTRA_COMPONENT_NAME, 289 ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString()) 290 .setPackage(mQSSettingsPackageRepository.getSettingsPackageName()); 291 292 mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0, 293 mDialogTransitionAnimator.createActivityTransitionController( 294 dialog)); 295 }, 296 /* dismissOnClick = */ true 297 ); 298 dialog.setPositiveButton( 299 R.string.quick_settings_done, 300 /* onClick = */ null, 301 /* dismissOnClick = */ true 302 ); 303 } 304 305 @Override onCreate(@onNull SystemUIDialog dialog, @Nullable Bundle savedInstanceState)306 public void onCreate(@NonNull SystemUIDialog dialog, @Nullable Bundle savedInstanceState) { 307 if (mLocalBluetoothManager == null) { 308 return; 309 } 310 311 // Remove the default padding of the system ui dialog 312 View container = dialog.findViewById(android.R.id.custom); 313 if (container != null && container.getParent() != null) { 314 View containerParent = (View) container.getParent(); 315 containerParent.setPadding(0, 0, 0, 0); 316 } 317 318 mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DIALOG_SHOW, mLaunchSourceId); 319 320 mBgExecutor.execute(() -> { 321 List<DeviceItem> hearingDeviceItemList = getHearingDeviceItemList(); 322 CachedBluetoothDevice activeHearingDevice = getActiveHearingDevice( 323 hearingDeviceItemList); 324 mLocalBluetoothManager.getEventManager().registerCallback(this); 325 326 mMainExecutor.execute(() -> { 327 setupDeviceListView(dialog, hearingDeviceItemList); 328 setupPairNewDeviceButton(dialog); 329 setupPresetSpinner(dialog, activeHearingDevice); 330 if (com.android.settingslib.flags.Flags.hearingDevicesInputRoutingControl()) { 331 setupInputRoutingSpinner(dialog, activeHearingDevice); 332 } 333 if (com.android.settingslib.flags.Flags.hearingDevicesAmbientVolumeControl()) { 334 setupAmbientControls(activeHearingDevice); 335 } 336 setupRelatedToolsView(dialog); 337 }); 338 }); 339 } 340 341 @Override onStop(@onNull SystemUIDialog dialog)342 public void onStop(@NonNull SystemUIDialog dialog) { 343 mBgExecutor.execute(() -> { 344 if (mLocalBluetoothManager != null) { 345 mLocalBluetoothManager.getEventManager().unregisterCallback(this); 346 } 347 if (mPresetController != null) { 348 mPresetController.unregisterHapCallback(); 349 } 350 if (mAmbientController != null) { 351 mAmbientController.stop(); 352 } 353 }); 354 } 355 setupDeviceListView(SystemUIDialog dialog, List<DeviceItem> hearingDeviceItemList)356 private void setupDeviceListView(SystemUIDialog dialog, 357 List<DeviceItem> hearingDeviceItemList) { 358 final RecyclerView deviceList = dialog.requireViewById(R.id.device_list); 359 deviceList.setLayoutManager(new LinearLayoutManager(dialog.getContext())); 360 mDeviceListAdapter = new HearingDevicesListAdapter(hearingDeviceItemList, this); 361 deviceList.setAdapter(mDeviceListAdapter); 362 } 363 setupPresetSpinner(SystemUIDialog dialog, CachedBluetoothDevice activeHearingDevice)364 private void setupPresetSpinner(SystemUIDialog dialog, 365 CachedBluetoothDevice activeHearingDevice) { 366 mPresetController = new HearingDevicesPresetsController(mProfileManager, mPresetCallback); 367 mPresetController.setDevice(activeHearingDevice); 368 369 mPresetSpinner = dialog.requireViewById(R.id.preset_spinner); 370 mPresetInfoAdapter = new HearingDevicesSpinnerAdapter(dialog.getContext()); 371 mPresetSpinner.setAdapter(mPresetInfoAdapter); 372 // Disable redundant Touch & Hold accessibility action for Switch Access 373 mPresetSpinner.setAccessibilityDelegate(new View.AccessibilityDelegate() { 374 @Override 375 public void onInitializeAccessibilityNodeInfo(@NonNull View host, 376 @NonNull AccessibilityNodeInfo info) { 377 info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); 378 super.onInitializeAccessibilityNodeInfo(host, info); 379 } 380 }); 381 // Should call setSelection(index, false) for the spinner before setOnItemSelectedListener() 382 // to avoid extra onItemSelected() get called when first register the listener. 383 refreshPresetUi(mPresetController.getAllPresetInfo(), 384 mPresetController.getActivePresetIndex()); 385 mPresetSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 386 @Override 387 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 388 mPresetInfoAdapter.setSelected(position); 389 mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PRESET_SELECT, 390 mLaunchSourceId); 391 mPresetController.selectPreset( 392 mPresetController.getAllPresetInfo().get(position).getIndex()); 393 } 394 395 @Override 396 public void onNothingSelected(AdapterView<?> parent) { 397 // Do nothing 398 } 399 }); 400 401 mPresetLayout = dialog.requireViewById(R.id.preset_layout); 402 mPresetLayout.setVisibility(mPresetController.isPresetControlAvailable() ? VISIBLE : GONE); 403 mBgExecutor.execute(() -> mPresetController.registerHapCallback()); 404 } 405 setupInputRoutingSpinner(SystemUIDialog dialog, CachedBluetoothDevice activeHearingDevice)406 private void setupInputRoutingSpinner(SystemUIDialog dialog, 407 CachedBluetoothDevice activeHearingDevice) { 408 mInputRoutingController = mInputRoutingControllerFactory.create(dialog.getContext()); 409 mInputRoutingController.setDevice(activeHearingDevice); 410 411 mInputRoutingSpinner = dialog.requireViewById(R.id.input_routing_spinner); 412 mInputRoutingAdapter = new HearingDevicesSpinnerAdapter(dialog.getContext()); 413 mInputRoutingAdapter.addAll( 414 HearingDevicesInputRoutingController.getInputRoutingOptions(dialog.getContext())); 415 mInputRoutingSpinner.setAdapter(mInputRoutingAdapter); 416 // Disable redundant Touch & Hold accessibility action for Switch Access 417 mInputRoutingSpinner.setAccessibilityDelegate(new View.AccessibilityDelegate() { 418 @Override 419 public void onInitializeAccessibilityNodeInfo(@NonNull View host, 420 @NonNull AccessibilityNodeInfo info) { 421 info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); 422 super.onInitializeAccessibilityNodeInfo(host, info); 423 } 424 }); 425 // Should call setSelection(index, false) for the spinner before setOnItemSelectedListener() 426 // to avoid extra onItemSelected() get called when first register the listener. 427 final int initialPosition = 428 mInputRoutingController.getUserPreferredInputRoutingValue(); 429 mInputRoutingSpinner.setSelection(initialPosition, false); 430 mInputRoutingAdapter.setSelected(initialPosition); 431 mInputRoutingSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 432 @Override 433 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 434 mInputRoutingAdapter.setSelected(position); 435 mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_INPUT_ROUTING_SELECT, 436 mLaunchSourceId); 437 mInputRoutingController.selectInputRouting(position); 438 } 439 440 @Override 441 public void onNothingSelected(AdapterView<?> parent) { 442 // Do nothing 443 } 444 }); 445 446 mInputRoutingLayout = dialog.requireViewById(R.id.input_routing_layout); 447 mInputRoutingController.isInputRoutingControlAvailable( 448 available -> mMainExecutor.execute(() -> mInputRoutingLayout.setVisibility( 449 available ? VISIBLE : GONE))); 450 } 451 setupAmbientControls(CachedBluetoothDevice activeHearingDevice)452 private void setupAmbientControls(CachedBluetoothDevice activeHearingDevice) { 453 final AmbientVolumeLayout ambientLayout = mDialog.requireViewById(R.id.ambient_layout); 454 ambientLayout.setUiEventLogger(mUiEventLogger, mLaunchSourceId); 455 mAmbientController = new AmbientVolumeUiController( 456 mDialog.getContext(), mLocalBluetoothManager, ambientLayout); 457 mAmbientController.setShowUiWhenLocalDataExist(false); 458 mAmbientController.loadDevice(activeHearingDevice); 459 mBgExecutor.execute(() -> mAmbientController.start()); 460 } 461 setupPairNewDeviceButton(SystemUIDialog dialog)462 private void setupPairNewDeviceButton(SystemUIDialog dialog) { 463 final Button pairButton = dialog.requireViewById(R.id.pair_new_device_button); 464 pairButton.setVisibility(mShowPairNewDevice ? VISIBLE : GONE); 465 if (mShowPairNewDevice) { 466 pairButton.setOnClickListener(v -> { 467 mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR, mLaunchSourceId); 468 dismissDialogIfExists(); 469 final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS) 470 .setPackage(mQSSettingsPackageRepository.getSettingsPackageName()); 471 mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0, 472 mDialogTransitionAnimator.createActivityTransitionController(dialog)); 473 }); 474 } 475 } 476 setupRelatedToolsView(SystemUIDialog dialog)477 private void setupRelatedToolsView(SystemUIDialog dialog) { 478 final Context context = dialog.getContext(); 479 final List<ToolItem> toolItemList = new ArrayList<>(); 480 final String[] toolNameArray; 481 final String[] toolIconArray; 482 483 ToolItem preInstalledItem = getLiveCaptionToolItem(context); 484 if (preInstalledItem != null) { 485 toolItemList.add(preInstalledItem); 486 } 487 try { 488 toolNameArray = context.getResources().getStringArray( 489 R.array.config_quickSettingsHearingDevicesRelatedToolName); 490 toolIconArray = context.getResources().getStringArray( 491 R.array.config_quickSettingsHearingDevicesRelatedToolIcon); 492 toolItemList.addAll( 493 HearingDevicesToolItemParser.parseStringArray(context, toolNameArray, 494 toolIconArray)); 495 } catch (Resources.NotFoundException e) { 496 Log.i(TAG, "No hearing devices related tool config resource"); 497 } 498 499 final View toolsLayout = dialog.requireViewById(R.id.tools_layout); 500 toolsLayout.setVisibility(toolItemList.isEmpty() ? GONE : VISIBLE); 501 502 final LinearLayout toolsContainer = dialog.requireViewById(R.id.tools_container); 503 for (int i = 0; i < toolItemList.size(); i++) { 504 View view = createToolView(context, toolItemList.get(i), toolsContainer); 505 toolsContainer.addView(view); 506 if (i != toolItemList.size() - 1) { 507 final int spaceSize = context.getResources().getDimensionPixelSize( 508 R.dimen.hearing_devices_layout_margin); 509 Space space = new Space(context); 510 space.setLayoutParams(new LinearLayout.LayoutParams(spaceSize, 0)); 511 toolsContainer.addView(space); 512 } 513 } 514 } 515 refreshDeviceUi(List<DeviceItem> hearingDeviceItemList)516 private void refreshDeviceUi(List<DeviceItem> hearingDeviceItemList) { 517 mMainExecutor.execute(() -> { 518 if (mDeviceListAdapter != null) { 519 mDeviceListAdapter.refreshDeviceItemList(hearingDeviceItemList); 520 } 521 }); 522 } 523 refreshPresetUi(List<BluetoothHapPresetInfo> presetInfos, int activePresetIndex)524 private void refreshPresetUi(List<BluetoothHapPresetInfo> presetInfos, int activePresetIndex) { 525 mPresetInfoAdapter.clear(); 526 mPresetInfoAdapter.addAll( 527 presetInfos.stream().map(BluetoothHapPresetInfo::getName).toList()); 528 if (activePresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE) { 529 final int size = mPresetInfoAdapter.getCount(); 530 for (int position = 0; position < size; position++) { 531 if (presetInfos.get(position).getIndex() == activePresetIndex) { 532 mPresetSpinner.setSelection(position, /* animate= */ false); 533 mPresetInfoAdapter.setSelected(position); 534 } 535 } 536 } 537 } 538 getHearingDeviceItemList()539 private List<DeviceItem> getHearingDeviceItemList() { 540 if (mLocalBluetoothManager == null 541 || !mLocalBluetoothManager.getBluetoothAdapter().isEnabled()) { 542 return emptyList(); 543 } 544 return mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy().stream() 545 .map(this::createHearingDeviceItem) 546 .filter(Objects::nonNull) 547 .collect(Collectors.toList()); 548 } 549 550 @Nullable getActiveHearingDevice( List<DeviceItem> hearingDeviceItemList)551 private static CachedBluetoothDevice getActiveHearingDevice( 552 List<DeviceItem> hearingDeviceItemList) { 553 return hearingDeviceItemList.stream() 554 .filter(item -> item.getType() == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE) 555 .map(DeviceItem::getCachedBluetoothDevice) 556 .findFirst() 557 .orElse(null); 558 } 559 560 @WorkerThread createHearingDeviceItem(CachedBluetoothDevice cachedDevice)561 private DeviceItem createHearingDeviceItem(CachedBluetoothDevice cachedDevice) { 562 final Context context = mDialog.getContext(); 563 if (cachedDevice == null) { 564 return null; 565 } 566 int mode = mAudioManager.getMode(); 567 boolean isOngoingCall = mode == AudioManager.MODE_RINGTONE 568 || mode == AudioManager.MODE_IN_CALL 569 || mode == AudioManager.MODE_IN_COMMUNICATION; 570 for (DeviceItemFactory itemFactory : mHearingDeviceItemFactoryList) { 571 if (itemFactory.isFilterMatched(context, cachedDevice, isOngoingCall)) { 572 return itemFactory.create(context, cachedDevice); 573 } 574 } 575 return null; 576 } 577 578 @NonNull createToolView(Context context, ToolItem item, ViewGroup container)579 private View createToolView(Context context, ToolItem item, ViewGroup container) { 580 View view = LayoutInflater.from(context).inflate(R.layout.hearing_tool_item, container, 581 false); 582 ImageView icon = view.requireViewById(R.id.tool_icon); 583 TextView text = view.requireViewById(R.id.tool_name); 584 view.setContentDescription(item.getToolName()); 585 icon.setImageDrawable(item.getToolIcon()); 586 if (item.isCustomIcon()) { 587 icon.getDrawable().mutate().setTint(context.getColor( 588 com.android.internal.R.color.materialColorOnPrimaryContainer)); 589 } 590 text.setText(item.getToolName()); 591 Intent intent = item.getToolIntent(); 592 view.setOnClickListener(v -> { 593 final String name = intent.getComponent() != null 594 ? intent.getComponent().flattenToString() 595 : intent.getPackage() + "/" + intent.getAction(); 596 mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_RELATED_TOOL_CLICK, 597 mLaunchSourceId, name); 598 dismissDialogIfExists(); 599 mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0, 600 mDialogTransitionAnimator.createActivityTransitionController(view)); 601 }); 602 return view; 603 } 604 getLiveCaptionToolItem(Context context)605 private ToolItem getLiveCaptionToolItem(Context context) { 606 final PackageManager packageManager = context.getPackageManager(); 607 LIVE_CAPTION_INTENT.setPackage(packageManager.getSystemCaptionsServicePackageName()); 608 final List<ResolveInfo> resolved = packageManager.queryIntentActivities(LIVE_CAPTION_INTENT, 609 /* flags= */ 0); 610 if (!resolved.isEmpty()) { 611 return new ToolItem( 612 context.getString(R.string.quick_settings_hearing_devices_live_caption_title), 613 context.getDrawable(R.drawable.ic_volume_odi_captions), 614 LIVE_CAPTION_INTENT, 615 /* isCustomIcon= */ true); 616 } 617 return null; 618 } 619 dismissDialogIfExists()620 private void dismissDialogIfExists() { 621 if (mDialog != null) { 622 mDialog.dismiss(); 623 } 624 } 625 showErrorToast(int stringResId)626 private void showErrorToast(int stringResId) { 627 Toast.makeText(mDialog.getContext(), stringResId, Toast.LENGTH_SHORT).show(); 628 } 629 } 630