diff options
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, |