summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/drawable/hearing_devices_preset_spinner_background.xml43
-rw-r--r--packages/SystemUI/res/drawable/hearing_devices_preset_spinner_popup_background.xml22
-rw-r--r--packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml29
-rw-r--r--packages/SystemUI/res/values/dimens.xml8
-rw-r--r--packages/SystemUI/res/values/strings.xml2
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java141
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java298
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java10
8 files changed, 546 insertions, 7 deletions
diff --git a/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_background.xml b/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_background.xml
new file mode 100644
index 000000000000..6e6e032ef2c5
--- /dev/null
+++ b/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_background.xml
@@ -0,0 +1,43 @@
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:paddingMode="stack">
+ <item>
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/hearing_devices_preset_spinner_background_radius" />
+ <stroke
+ android:width="1dp"
+ android:color="?androidprv:attr/textColorTertiary" />
+ <solid android:color="@android:color/transparent"/>
+ </shape>
+ </item>
+ <item
+ android:end="20dp"
+ android:gravity="end|center_vertical">
+ <vector
+ android:width="@dimen/screenrecord_spinner_arrow_size"
+ android:height="@dimen/screenrecord_spinner_arrow_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?androidprv:attr/colorControlNormal">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" />
+ </vector>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_popup_background.xml b/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_popup_background.xml
new file mode 100644
index 000000000000..f35975ee8548
--- /dev/null
+++ b/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_popup_background.xml
@@ -0,0 +1,22 @@
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/hearing_devices_preset_spinner_background_radius"/>
+ <solid android:color="?androidprv:attr/materialColorSurfaceContainer" />
+</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
index a5cdaeb2f925..8e1d0a57100c 100644
--- a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
@@ -17,6 +17,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/root"
style="@style/Widget.SliceView.Panel"
android:layout_width="wrap_content"
@@ -26,9 +27,33 @@
android:id="@+id/device_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintBottom_toTopOf="@id/pair_new_device_button" />
+ app:layout_constraintBottom_toTopOf="@id/preset_spinner" />
+
+ <Spinner
+ android:id="@+id/preset_spinner"
+ style="@style/BluetoothTileDialog.Device"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/hearing_devices_preset_spinner_height"
+ android:layout_marginTop="@dimen/hearing_devices_preset_spinner_margin"
+ android:layout_marginBottom="@dimen/hearing_devices_preset_spinner_margin"
+ android:gravity="center_vertical"
+ android:background="@drawable/hearing_devices_preset_spinner_background"
+ android:popupBackground="@drawable/hearing_devices_preset_spinner_popup_background"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/device_list"
+ app:layout_constraintBottom_toTopOf="@id/pair_new_device_button"
+ android:visibility="gone"/>
+
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/device_barrier"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="device_list,preset_spinner" />
<Button
android:id="@+id/pair_new_device_button"
@@ -41,7 +66,7 @@
android:contentDescription="@string/accessibility_hearing_device_pair_new_device"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@id/device_list"
+ app:layout_constraintTop_toBottomOf="@id/device_barrier"
android:drawableStart="@drawable/ic_add"
android:drawablePadding="20dp"
android:drawableTint="?android:attr/textColorPrimary"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 26fa2b136ed4..82395e48de20 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1762,6 +1762,14 @@
<!-- The height of the main scroll view in bluetooth dialog with auto on toggle. -->
<dimen name="bluetooth_dialog_scroll_view_min_height_with_auto_on">350dp</dimen>
+ <!-- Hearing devices dialog related dimensions -->
+ <dimen name="hearing_devices_preset_spinner_height">72dp</dimen>
+ <dimen name="hearing_devices_preset_spinner_margin">24dp</dimen>
+ <dimen name="hearing_devices_preset_spinner_text_padding_start">20dp</dimen>
+ <dimen name="hearing_devices_preset_spinner_text_padding_end">80dp</dimen>
+ <dimen name="hearing_devices_preset_spinner_arrow_size">24dp</dimen>
+ <dimen name="hearing_devices_preset_spinner_background_radius">28dp</dimen>
+
<!-- Height percentage of the parent container occupied by the communal view -->
<item name="communal_source_height_percentage" format="float" type="dimen">0.80</item>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 4690f02b38da..3e1304359a89 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -916,6 +916,8 @@
<string name="quick_settings_pair_hearing_devices">Pair new device</string>
<!-- QuickSettings: Content description of the hearing devices dialog pair new device [CHAR LIMIT=NONE] -->
<string name="accessibility_hearing_device_pair_new_device">Click to pair new device</string>
+ <!-- Message when selecting hearing aids presets failed. [CHAR LIMIT=NONE] -->
+ <string name="hearing_devices_presets_error">Couldn\'t update preset</string>
<!--- Title of dialog triggered if the microphone is disabled but an app tried to access it. [CHAR LIMIT=150] -->
<string name="sensor_privacy_start_use_mic_dialog_title">Unblock device microphone?</string>
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 475bb2c83b0e..7b5a09cb3848 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -21,6 +21,8 @@ import static android.view.View.VISIBLE;
import static java.util.Collections.emptyList;
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothHapPresetInfo;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
@@ -30,7 +32,11 @@ import android.provider.Settings;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.Visibility;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
import android.widget.Button;
+import android.widget.Spinner;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -40,7 +46,9 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.settingslib.bluetooth.BluetoothCallback;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.HapClientProfile;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.systemui.accessibility.hearingaid.HearingDevicesListAdapter.HearingDeviceItemCallback;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.bluetooth.qsdialog.ActiveHearingDeviceItemFactory;
@@ -48,7 +56,9 @@ import com.android.systemui.bluetooth.qsdialog.AvailableHearingDeviceItemFactory
import com.android.systemui.bluetooth.qsdialog.ConnectedDeviceItemFactory;
import com.android.systemui.bluetooth.qsdialog.DeviceItem;
import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory;
+import com.android.systemui.bluetooth.qsdialog.DeviceItemType;
import com.android.systemui.bluetooth.qsdialog.SavedHearingDeviceItemFactory;
+import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.res.R;
@@ -80,11 +90,37 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
private final LocalBluetoothManager mLocalBluetoothManager;
private final Handler mMainHandler;
private final AudioManager mAudioManager;
-
+ private final LocalBluetoothProfileManager mProfileManager;
+ private final HapClientProfile mHapClientProfile;
private HearingDevicesListAdapter mDeviceListAdapter;
+ private HearingDevicesPresetsController mPresetsController;
+ private Context mApplicationContext;
private SystemUIDialog mDialog;
private RecyclerView mDeviceList;
+ private List<DeviceItem> mHearingDeviceItemList;
+ private Spinner mPresetSpinner;
+ private ArrayAdapter<String> mPresetInfoAdapter;
private Button mPairButton;
+ private final HearingDevicesPresetsController.PresetCallback mPresetCallback =
+ new HearingDevicesPresetsController.PresetCallback() {
+ @Override
+ public void onPresetInfoUpdated(List<BluetoothHapPresetInfo> presetInfos,
+ int activePresetIndex) {
+ mMainHandler.post(
+ () -> refreshPresetInfoAdapter(presetInfos, activePresetIndex));
+ }
+
+ @Override
+ public void onPresetCommandFailed(int reason) {
+ final List<BluetoothHapPresetInfo> presetInfos =
+ mPresetsController.getAllPresetInfo();
+ final int activePresetIndex = mPresetsController.getActivePresetIndex();
+ mMainHandler.post(() -> {
+ refreshPresetInfoAdapter(presetInfos, activePresetIndex);
+ showPresetErrorToast(mApplicationContext);
+ });
+ }
+ };
private final List<DeviceItemFactory> mHearingDeviceItemFactoryList = List.of(
new ActiveHearingDeviceItemFactory(),
new AvailableHearingDeviceItemFactory(),
@@ -107,6 +143,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
@AssistedInject
public HearingDevicesDialogDelegate(
+ @Application Context applicationContext,
@Assisted boolean showPairNewDevice,
SystemUIDialog.Factory systemUIDialogFactory,
ActivityStarter activityStarter,
@@ -114,6 +151,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
@Nullable LocalBluetoothManager localBluetoothManager,
@Main Handler handler,
AudioManager audioManager) {
+ mApplicationContext = applicationContext;
mShowPairNewDevice = showPairNewDevice;
mSystemUIDialogFactory = systemUIDialogFactory;
mActivityStarter = activityStarter;
@@ -121,6 +159,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
mLocalBluetoothManager = localBluetoothManager;
mMainHandler = handler;
mAudioManager = audioManager;
+ mProfileManager = localBluetoothManager.getProfileManager();
+ mHapClientProfile = mProfileManager.getHapClientProfile();
}
@Override
@@ -158,19 +198,34 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
@Override
public void onActiveDeviceChanged(@Nullable CachedBluetoothDevice activeDevice,
int bluetoothProfile) {
- mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(getHearingDevicesList()));
+ CachedBluetoothDevice activeHearingDevice;
+ mHearingDeviceItemList = getHearingDevicesList();
+ if (mPresetsController != null) {
+ activeHearingDevice = getActiveHearingDevice(mHearingDeviceItemList);
+ mPresetsController.setActiveHearingDevice(activeHearingDevice);
+ } else {
+ activeHearingDevice = null;
+ }
+ mMainHandler.post(() -> {
+ mDeviceListAdapter.refreshDeviceItemList(mHearingDeviceItemList);
+ mPresetSpinner.setVisibility(
+ (activeHearingDevice != null && !mPresetInfoAdapter.isEmpty()) ? VISIBLE
+ : GONE);
+ });
}
@Override
public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
int state, int bluetoothProfile) {
- mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(getHearingDevicesList()));
+ mHearingDeviceItemList = getHearingDevicesList();
+ mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(mHearingDeviceItemList));
}
@Override
public void onAclConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
int state) {
- mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(getHearingDevicesList()));
+ mHearingDeviceItemList = getHearingDevicesList();
+ mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(mHearingDeviceItemList));
}
@Override
@@ -187,10 +242,15 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
@Override
public void onCreate(@NonNull SystemUIDialog dialog, @Nullable Bundle savedInstanceState) {
+ if (mLocalBluetoothManager == null) {
+ return;
+ }
mPairButton = dialog.requireViewById(R.id.pair_new_device_button);
mDeviceList = dialog.requireViewById(R.id.device_list);
+ mPresetSpinner = dialog.requireViewById(R.id.preset_spinner);
setupDeviceListView(dialog);
+ setupPresetSpinner(dialog);
setupPairNewDeviceButton(dialog, mShowPairNewDevice ? VISIBLE : GONE);
}
@@ -199,7 +259,14 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
if (mLocalBluetoothManager == null) {
return;
}
+
mLocalBluetoothManager.getEventManager().registerCallback(this);
+ if (mPresetsController != null) {
+ mPresetsController.registerHapCallback();
+ if (mHapClientProfile != null && !mHapClientProfile.isProfileReady()) {
+ mProfileManager.addServiceListener(mPresetsController);
+ }
+ }
}
@Override
@@ -207,15 +274,51 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
if (mLocalBluetoothManager == null) {
return;
}
+
+ if (mPresetsController != null) {
+ mPresetsController.unregisterHapCallback();
+ mProfileManager.removeServiceListener(mPresetsController);
+ }
mLocalBluetoothManager.getEventManager().unregisterCallback(this);
}
private void setupDeviceListView(SystemUIDialog dialog) {
mDeviceList.setLayoutManager(new LinearLayoutManager(dialog.getContext()));
- mDeviceListAdapter = new HearingDevicesListAdapter(getHearingDevicesList(), this);
+ mHearingDeviceItemList = getHearingDevicesList();
+ mDeviceListAdapter = new HearingDevicesListAdapter(mHearingDeviceItemList, this);
mDeviceList.setAdapter(mDeviceListAdapter);
}
+ private void setupPresetSpinner(SystemUIDialog dialog) {
+ mPresetsController = new HearingDevicesPresetsController(mProfileManager, mPresetCallback);
+ final CachedBluetoothDevice activeHearingDevice = getActiveHearingDevice(
+ mHearingDeviceItemList);
+ mPresetsController.setActiveHearingDevice(activeHearingDevice);
+
+ mPresetInfoAdapter = new ArrayAdapter<>(dialog.getContext(),
+ android.R.layout.simple_spinner_dropdown_item);
+ mPresetInfoAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mPresetSpinner.setAdapter(mPresetInfoAdapter);
+ mPresetSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ mPresetsController.selectPreset(
+ mPresetsController.getAllPresetInfo().get(position).getIndex());
+ mPresetSpinner.setSelection(position);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // Do nothing
+ }
+ });
+ final List<BluetoothHapPresetInfo> presetInfos = mPresetsController.getAllPresetInfo();
+ final int activePresetIndex = mPresetsController.getActivePresetIndex();
+ refreshPresetInfoAdapter(presetInfos, activePresetIndex);
+ mPresetSpinner.setVisibility(
+ (activeHearingDevice != null && !mPresetInfoAdapter.isEmpty()) ? VISIBLE : GONE);
+ }
+
private void setupPairNewDeviceButton(SystemUIDialog dialog, @Visibility int visibility) {
if (visibility == VISIBLE) {
mPairButton.setOnClickListener(v -> {
@@ -230,6 +333,21 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
}
}
+ private void refreshPresetInfoAdapter(List<BluetoothHapPresetInfo> presetInfos,
+ int activePresetIndex) {
+ mPresetInfoAdapter.clear();
+ mPresetInfoAdapter.addAll(
+ presetInfos.stream().map(BluetoothHapPresetInfo::getName).toList());
+ if (activePresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE) {
+ final int size = mPresetInfoAdapter.getCount();
+ for (int position = 0; position < size; position++) {
+ if (presetInfos.get(position).getIndex() == activePresetIndex) {
+ mPresetSpinner.setSelection(position);
+ }
+ }
+ }
+ }
+
private List<DeviceItem> getHearingDevicesList() {
if (mLocalBluetoothManager == null
|| !mLocalBluetoothManager.getBluetoothAdapter().isEnabled()) {
@@ -242,6 +360,15 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
.collect(Collectors.toList());
}
+ @Nullable
+ private CachedBluetoothDevice getActiveHearingDevice(List<DeviceItem> hearingDeviceItemList) {
+ return hearingDeviceItemList.stream()
+ .filter(item -> item.getType() == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
+ .map(DeviceItem::getCachedBluetoothDevice)
+ .findFirst()
+ .orElse(null);
+ }
+
private DeviceItem createHearingDeviceItem(CachedBluetoothDevice cachedDevice) {
final Context context = mDialog.getContext();
if (cachedDevice == null) {
@@ -260,4 +387,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
mDialog.dismiss();
}
}
+
+ private void showPresetErrorToast(Context context) {
+ Toast.makeText(context, R.string.hearing_devices_presets_error, Toast.LENGTH_SHORT).show();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
new file mode 100644
index 000000000000..02fa003a3628
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.hearingaid;
+
+import static java.util.Collections.emptyList;
+
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothHapPresetInfo;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.HapClientProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.List;
+
+/**
+ * The controller of the hearing devices presets of the bluetooth Hearing Access Profile.
+ */
+public class HearingDevicesPresetsController implements
+ LocalBluetoothProfileManager.ServiceListener, BluetoothHapClient.Callback {
+
+ private static final String TAG = "HearingDevicesPresetsController";
+ private static final boolean DEBUG = true;
+
+ private final LocalBluetoothProfileManager mProfileManager;
+ private final HapClientProfile mHapClientProfile;
+ private final PresetCallback mPresetCallback;
+
+ private CachedBluetoothDevice mActiveHearingDevice;
+ private int mSelectedPresetIndex;
+
+ public HearingDevicesPresetsController(LocalBluetoothProfileManager profileManager,
+ PresetCallback presetCallback) {
+ mProfileManager = profileManager;
+ mHapClientProfile = mProfileManager.getHapClientProfile();
+ mPresetCallback = presetCallback;
+ }
+
+ @Override
+ public void onServiceConnected() {
+ if (mHapClientProfile != null && mHapClientProfile.isProfileReady()) {
+ mProfileManager.removeServiceListener(this);
+ registerHapCallback();
+ mPresetCallback.onPresetInfoUpdated(getAllPresetInfo(), getActivePresetIndex());
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected() {
+ // Do nothing
+ }
+
+ @Override
+ public void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex, int reason) {
+ if (mActiveHearingDevice == null) {
+ return;
+ }
+ if (device.equals(mActiveHearingDevice.getDevice())) {
+ if (DEBUG) {
+ Log.d(TAG, "onPresetSelected, device: " + device.getAddress()
+ + ", presetIndex: " + presetIndex + ", reason: " + reason);
+ }
+ mPresetCallback.onPresetInfoUpdated(getAllPresetInfo(), getActivePresetIndex());
+ }
+ }
+
+ @Override
+ public void onPresetInfoChanged(@NonNull BluetoothDevice device,
+ @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason) {
+ if (mActiveHearingDevice == null) {
+ return;
+ }
+ if (device.equals(mActiveHearingDevice.getDevice())) {
+ if (DEBUG) {
+ Log.d(TAG, "onPresetInfoChanged, device: " + device.getAddress()
+ + ", reason: " + reason + ", infoList: " + presetInfoList);
+ }
+ mPresetCallback.onPresetInfoUpdated(getAllPresetInfo(), getActivePresetIndex());
+ }
+ }
+
+ @Override
+ public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int reason) {
+ if (mActiveHearingDevice == null) {
+ return;
+ }
+ if (device.equals(mActiveHearingDevice.getDevice())) {
+ Log.w(TAG, "onPresetSelectionFailed, device: " + device.getAddress()
+ + ", reason: " + reason);
+ mPresetCallback.onPresetCommandFailed(reason);
+ }
+ }
+
+ @Override
+ public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) {
+ if (mActiveHearingDevice == null) {
+ return;
+ }
+ if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) {
+ Log.w(TAG, "onPresetSelectionForGroupFailed, group: " + hapGroupId
+ + ", reason: " + reason);
+ selectPresetIndependently(mSelectedPresetIndex);
+ }
+ }
+
+ @Override
+ public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int reason) {
+ if (mActiveHearingDevice == null) {
+ return;
+ }
+ if (device.equals(mActiveHearingDevice.getDevice())) {
+ Log.w(TAG, "onSetPresetNameFailed, device: " + device.getAddress()
+ + ", reason: " + reason);
+ mPresetCallback.onPresetCommandFailed(reason);
+ }
+ }
+
+ @Override
+ public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) {
+ if (mActiveHearingDevice == null) {
+ return;
+ }
+ if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) {
+ Log.w(TAG, "onSetPresetNameForGroupFailed, group: " + hapGroupId
+ + ", reason: " + reason);
+ }
+ mPresetCallback.onPresetCommandFailed(reason);
+ }
+
+ /**
+ * Registers a callback to be notified about operation changed for {@link HapClientProfile}.
+ */
+ public void registerHapCallback() {
+ if (mHapClientProfile != null) {
+ try {
+ mHapClientProfile.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
+ } catch (IllegalArgumentException e) {
+ // The callback was already registered
+ Log.w(TAG, "Cannot register callback: " + e.getMessage());
+ }
+
+ }
+ }
+
+ /**
+ * Removes a previously-added {@link HapClientProfile} callback.
+ */
+ public void unregisterHapCallback() {
+ if (mHapClientProfile != null) {
+ try {
+ mHapClientProfile.unregisterCallback(this);
+ } catch (IllegalArgumentException e) {
+ // The callback was never registered or was already unregistered
+ Log.w(TAG, "Cannot unregister callback: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Sets the hearing device for this controller to control the preset.
+ *
+ * @param activeHearingDevice the {@link CachedBluetoothDevice} need to be hearing aid device
+ */
+ public void setActiveHearingDevice(CachedBluetoothDevice activeHearingDevice) {
+ mActiveHearingDevice = activeHearingDevice;
+ }
+
+ /**
+ * Selects the currently active preset for {@code mActiveHearingDevice} individual device or
+ * the device group accoridng to whether it supports synchronized presets or not.
+ *
+ * @param presetIndex an index of one of the available presets
+ */
+ public void selectPreset(int presetIndex) {
+ if (mActiveHearingDevice == null) {
+ return;
+ }
+ mSelectedPresetIndex = presetIndex;
+ boolean supportSynchronizedPresets = mHapClientProfile.supportsSynchronizedPresets(
+ mActiveHearingDevice.getDevice());
+ int hapGroupId = mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice());
+ if (supportSynchronizedPresets) {
+ if (hapGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+ selectPresetSynchronously(hapGroupId, presetIndex);
+ } else {
+ Log.w(TAG, "supportSynchronizedPresets but hapGroupId is invalid.");
+ selectPresetIndependently(presetIndex);
+ }
+ } else {
+ selectPresetIndependently(presetIndex);
+ }
+ }
+
+ /**
+ * Gets all preset info for {@code mActiveHearingDevice} device.
+ *
+ * @return a list of all known preset info
+ */
+ public List<BluetoothHapPresetInfo> getAllPresetInfo() {
+ if (mActiveHearingDevice == null) {
+ return emptyList();
+ }
+ return mHapClientProfile.getAllPresetInfo(mActiveHearingDevice.getDevice());
+ }
+
+ /**
+ * Gets the currently active preset for {@code mActiveHearingDevice} device.
+ *
+ * @return active preset index
+ */
+ public int getActivePresetIndex() {
+ if (mActiveHearingDevice == null) {
+ return BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
+ }
+ return mHapClientProfile.getActivePresetIndex(mActiveHearingDevice.getDevice());
+ }
+
+ private void selectPresetSynchronously(int groupId, int presetIndex) {
+ if (mActiveHearingDevice == null) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "selectPresetSynchronously"
+ + ", presetIndex: " + presetIndex
+ + ", groupId: " + groupId
+ + ", device: " + mActiveHearingDevice.getAddress());
+ }
+ mHapClientProfile.selectPresetForGroup(groupId, presetIndex);
+ }
+
+ private void selectPresetIndependently(int presetIndex) {
+ if (mActiveHearingDevice == null) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "selectPresetIndependently"
+ + ", presetIndex: " + presetIndex
+ + ", device: " + mActiveHearingDevice.getAddress());
+ }
+ mHapClientProfile.selectPreset(mActiveHearingDevice.getDevice(), presetIndex);
+ final CachedBluetoothDevice subDevice = mActiveHearingDevice.getSubDevice();
+ if (subDevice != null) {
+ if (DEBUG) {
+ Log.d(TAG, "selectPreset for subDevice, device: " + subDevice);
+ }
+ mHapClientProfile.selectPreset(subDevice.getDevice(), presetIndex);
+ }
+ for (final CachedBluetoothDevice memberDevice :
+ mActiveHearingDevice.getMemberDevice()) {
+ if (DEBUG) {
+ Log.d(TAG, "selectPreset for memberDevice, device: " + memberDevice);
+ }
+ mHapClientProfile.selectPreset(memberDevice.getDevice(), presetIndex);
+ }
+ }
+
+ /**
+ * Interface to provide callbacks when preset command result from {@link HapClientProfile}
+ * changed.
+ */
+ public interface PresetCallback {
+ /**
+ * Called when preset info from {@link HapClientProfile} operation get updated.
+ *
+ * @param presetInfos all preset info for {@code mActiveHearingDevice} device
+ * @param activePresetIndex currently active preset index for {@code mActiveHearingDevice}
+ * device
+ */
+ void onPresetInfoUpdated(List<BluetoothHapPresetInfo> presetInfos, int activePresetIndex);
+
+ /**
+ * Called when preset operation from {@link HapClientProfile} failed to handle.
+ *
+ * @param reason failure reason
+ */
+ void onPresetCommandFailed(int reason);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
index 15764571ce02..ebb6b48f9532 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
@@ -39,8 +39,10 @@ import androidx.test.filters.SmallTest;
import com.android.settingslib.bluetooth.BluetoothEventManager;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.HapClientProfile;
import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.bluetooth.qsdialog.DeviceItem;
@@ -88,6 +90,10 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
@Mock
private LocalBluetoothAdapter mLocalBluetoothAdapter;
@Mock
+ private LocalBluetoothProfileManager mProfileManager;
+ @Mock
+ private HapClientProfile mHapClientProfile;
+ @Mock
private CachedBluetoothDeviceManager mCachedDeviceManager;
@Mock
private BluetoothEventManager mBluetoothEventManager;
@@ -106,6 +112,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
public void setUp() {
mTestableLooper = TestableLooper.get(this);
when(mLocalBluetoothManager.getBluetoothAdapter()).thenReturn(mLocalBluetoothAdapter);
+ when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager);
+ when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile);
when(mLocalBluetoothAdapter.isEnabled()).thenReturn(true);
when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mDevices);
@@ -163,6 +171,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
private void setUpPairNewDeviceDialog() {
mDialogDelegate = new HearingDevicesDialogDelegate(
+ mContext,
true,
mSystemUIDialogFactory,
mActivityStarter,
@@ -185,6 +194,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
private void setUpDeviceListDialog() {
mDialogDelegate = new HearingDevicesDialogDelegate(
+ mContext,
false,
mSystemUIDialogFactory,
mActivityStarter,