• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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