Merge "Fix crash in Hearing Devices dialog when connecting to ASHA device with LE Audio disabled" into main
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 b6fe0df..d08653c3 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -228,7 +228,7 @@
         mHearingDeviceItemList = getHearingDevicesList();
         if (mPresetsController != null) {
             activeHearingDevice = getActiveHearingDevice(mHearingDeviceItemList);
-            mPresetsController.setActiveHearingDevice(activeHearingDevice);
+            mPresetsController.setHearingDeviceIfSupportHap(activeHearingDevice);
         } else {
             activeHearingDevice = null;
         }
@@ -336,7 +336,7 @@
         }
         final CachedBluetoothDevice activeHearingDevice = getActiveHearingDevice(
                 mHearingDeviceItemList);
-        mPresetsController.setActiveHearingDevice(activeHearingDevice);
+        mPresetsController.setHearingDeviceIfSupportHap(activeHearingDevice);
 
         mPresetInfoAdapter = new ArrayAdapter<>(dialog.getContext(),
                 R.layout.hearing_devices_preset_spinner_selected,
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
index f81124e..aa95fd0 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
@@ -113,7 +113,7 @@
 
     @Override
     public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) {
-        if (mActiveHearingDevice == null) {
+        if (mActiveHearingDevice == null || mHapClientProfile == null) {
             return;
         }
         if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) {
@@ -137,7 +137,7 @@
 
     @Override
     public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) {
-        if (mActiveHearingDevice == null) {
+        if (mActiveHearingDevice == null || mHapClientProfile == null) {
             return;
         }
         if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) {
@@ -177,22 +177,33 @@
     }
 
     /**
-     * Sets the hearing device for this controller to control the preset.
+     * Sets the hearing device for this controller to control the preset if it supports
+     * {@link HapClientProfile}.
      *
      * @param activeHearingDevice the {@link CachedBluetoothDevice} need to be hearing aid device
+     *                            and support {@link HapClientProfile}.
      */
-    public void setActiveHearingDevice(CachedBluetoothDevice activeHearingDevice) {
-        mActiveHearingDevice = activeHearingDevice;
+    public void setHearingDeviceIfSupportHap(CachedBluetoothDevice activeHearingDevice) {
+        if (mHapClientProfile == null || activeHearingDevice == null) {
+            mActiveHearingDevice = null;
+            return;
+        }
+        if (activeHearingDevice.getProfiles().stream().anyMatch(
+                profile -> profile instanceof HapClientProfile)) {
+            mActiveHearingDevice = activeHearingDevice;
+        } else {
+            mActiveHearingDevice = null;
+        }
     }
 
     /**
      * Selects the currently active preset for {@code mActiveHearingDevice} individual device or
-     * the device group accoridng to whether it supports synchronized presets or not.
+     * the device group according 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) {
+        if (mActiveHearingDevice == null || mHapClientProfile == null) {
             return;
         }
         mSelectedPresetIndex = presetIndex;
@@ -217,7 +228,7 @@
      * @return a list of all known preset info
      */
     public List<BluetoothHapPresetInfo> getAllPresetInfo() {
-        if (mActiveHearingDevice == null) {
+        if (mActiveHearingDevice == null || mHapClientProfile == null) {
             return emptyList();
         }
         return mHapClientProfile.getAllPresetInfo(mActiveHearingDevice.getDevice()).stream().filter(
@@ -230,14 +241,14 @@
      * @return active preset index
      */
     public int getActivePresetIndex() {
-        if (mActiveHearingDevice == null) {
+        if (mActiveHearingDevice == null || mHapClientProfile == null) {
             return BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
         }
         return mHapClientProfile.getActivePresetIndex(mActiveHearingDevice.getDevice());
     }
 
     private void selectPresetSynchronously(int groupId, int presetIndex) {
-        if (mActiveHearingDevice == null) {
+        if (mActiveHearingDevice == null || mHapClientProfile == null) {
             return;
         }
         if (DEBUG) {
@@ -250,7 +261,7 @@
     }
 
     private void selectPresetIndependently(int presetIndex) {
-        if (mActiveHearingDevice == null) {
+        if (mActiveHearingDevice == null || mHapClientProfile == null) {
             return;
         }
         if (DEBUG) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java
new file mode 100644
index 0000000..2ac5d10
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java
@@ -0,0 +1,285 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static java.util.Collections.emptyList;
+
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothHapPresetInfo;
+import android.testing.TestableLooper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.HapClientProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/** Tests for {@link HearingDevicesPresetsController}. */
+@RunWith(AndroidJUnit4.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class HearingDevicesPresetsControllerTest extends SysuiTestCase {
+
+    private static final int TEST_PRESET_INDEX = 1;
+    private static final String TEST_PRESET_NAME = "test_preset";
+    private static final int TEST_HAP_GROUP_ID = 1;
+    private static final int TEST_REASON = 1024;
+
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private LocalBluetoothProfileManager mProfileManager;
+    @Mock
+    private HapClientProfile mHapClientProfile;
+    @Mock
+    private CachedBluetoothDevice mCachedBluetoothDevice;
+    @Mock
+    private CachedBluetoothDevice mSubCachedBluetoothDevice;
+    @Mock
+    private BluetoothDevice mBluetoothDevice;
+    @Mock
+    private BluetoothDevice mSubBluetoothDevice;
+
+    @Mock
+    private HearingDevicesPresetsController.PresetCallback mCallback;
+
+    private HearingDevicesPresetsController mController;
+
+    @Before
+    public void setUp() {
+        when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile);
+        when(mHapClientProfile.isProfileReady()).thenReturn(true);
+        when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+        when(mCachedBluetoothDevice.getSubDevice()).thenReturn(mSubCachedBluetoothDevice);
+        when(mSubCachedBluetoothDevice.getDevice()).thenReturn(mSubBluetoothDevice);
+
+        mController = new HearingDevicesPresetsController(mProfileManager, mCallback);
+    }
+
+    @Test
+    public void onServiceConnected_callExpectedCallback() {
+        mController.onServiceConnected();
+
+        verify(mHapClientProfile).registerCallback(any(Executor.class),
+                any(BluetoothHapClient.Callback.class));
+        verify(mCallback).onPresetInfoUpdated(anyList(), anyInt());
+    }
+
+    @Test
+    public void getAllPresetInfo_setInvalidHearingDevice_getEmpty() {
+        when(mCachedBluetoothDevice.getProfiles()).thenReturn(emptyList());
+        mController.setHearingDeviceIfSupportHap(mCachedBluetoothDevice);
+        BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+        when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+                List.of(hapPresetInfo));
+
+        assertThat(mController.getAllPresetInfo()).isEmpty();
+    }
+
+    @Test
+    public void getAllPresetInfo_containsNotAvailablePresetInfo_getEmpty() {
+        setValidHearingDeviceSupportHap();
+        BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(false);
+        when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+                List.of(hapPresetInfo));
+
+        assertThat(mController.getAllPresetInfo()).isEmpty();
+    }
+
+    @Test
+    public void getAllPresetInfo_containsOnePresetInfo_getOnePresetInfo() {
+        setValidHearingDeviceSupportHap();
+        BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+        when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+                List.of(hapPresetInfo));
+
+        assertThat(mController.getAllPresetInfo()).contains(hapPresetInfo);
+    }
+
+    @Test
+    public void getActivePresetIndex_getExpectedIndex() {
+        setValidHearingDeviceSupportHap();
+        when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn(
+                TEST_PRESET_INDEX);
+
+        assertThat(mController.getActivePresetIndex()).isEqualTo(TEST_PRESET_INDEX);
+    }
+
+    @Test
+    public void onPresetSelected_presetIndex_callOnPresetInfoUpdatedWithExpectedPresetIndex() {
+        setValidHearingDeviceSupportHap();
+        BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+        when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+                List.of(hapPresetInfo));
+        when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn(
+                TEST_PRESET_INDEX);
+
+        mController.onPresetSelected(mBluetoothDevice, TEST_PRESET_INDEX, TEST_REASON);
+
+        verify(mCallback).onPresetInfoUpdated(eq(List.of(hapPresetInfo)), eq(TEST_PRESET_INDEX));
+    }
+
+    @Test
+    public void onPresetInfoChanged_presetIndex_callOnPresetInfoUpdatedWithExpectedPresetIndex() {
+        setValidHearingDeviceSupportHap();
+        BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+        when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+                List.of(hapPresetInfo));
+        when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn(
+                TEST_PRESET_INDEX);
+
+        mController.onPresetInfoChanged(mBluetoothDevice, List.of(hapPresetInfo), TEST_REASON);
+
+        verify(mCallback).onPresetInfoUpdated(List.of(hapPresetInfo), TEST_PRESET_INDEX);
+    }
+
+    @Test
+    public void onPresetSelectionFailed_callOnPresetCommandFailed() {
+        setValidHearingDeviceSupportHap();
+
+        mController.onPresetSelectionFailed(mBluetoothDevice, TEST_REASON);
+
+        verify(mCallback).onPresetCommandFailed(TEST_REASON);
+    }
+
+    @Test
+    public void onSetPresetNameFailed_callOnPresetCommandFailed() {
+        setValidHearingDeviceSupportHap();
+
+        mController.onSetPresetNameFailed(mBluetoothDevice, TEST_REASON);
+
+        verify(mCallback).onPresetCommandFailed(TEST_REASON);
+    }
+
+    @Test
+    public void onPresetSelectionForGroupFailed_callSelectPresetIndividual() {
+        setValidHearingDeviceSupportHap();
+        mController.selectPreset(TEST_PRESET_INDEX);
+        Mockito.reset(mHapClientProfile);
+        when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID);
+
+        mController.onPresetSelectionForGroupFailed(TEST_HAP_GROUP_ID, TEST_REASON);
+
+
+        verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX);
+        verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX);
+    }
+
+    @Test
+    public void onSetPresetNameForGroupFailed_callOnPresetCommandFailed() {
+        setValidHearingDeviceSupportHap();
+
+        mController.onSetPresetNameForGroupFailed(TEST_HAP_GROUP_ID, TEST_REASON);
+
+        verify(mCallback).onPresetCommandFailed(TEST_REASON);
+    }
+
+    @Test
+    public void registerHapCallback_callHapRegisterCallback() {
+        mController.registerHapCallback();
+
+        verify(mHapClientProfile).registerCallback(any(Executor.class),
+                any(BluetoothHapClient.Callback.class));
+    }
+
+    @Test
+    public void unregisterHapCallback_callHapUnregisterCallback() {
+        mController.unregisterHapCallback();
+
+        verify(mHapClientProfile).unregisterCallback(any(BluetoothHapClient.Callback.class));
+    }
+
+    @Test
+    public void selectPreset_supportSynchronized_validGroupId_callSelectPresetForGroup() {
+        setValidHearingDeviceSupportHap();
+        when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(true);
+        when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID);
+
+        mController.selectPreset(TEST_PRESET_INDEX);
+
+        verify(mHapClientProfile).selectPresetForGroup(TEST_HAP_GROUP_ID, TEST_PRESET_INDEX);
+    }
+
+    @Test
+    public void selectPreset_supportSynchronized_invalidGroupId_callSelectPresetIndividual() {
+        setValidHearingDeviceSupportHap();
+        when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(true);
+        when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(
+                BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+
+        mController.selectPreset(TEST_PRESET_INDEX);
+
+        verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX);
+        verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX);
+    }
+
+    @Test
+    public void selectPreset_notSupportSynchronized_validGroupId_callSelectPresetIndividual() {
+        setValidHearingDeviceSupportHap();
+        when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(false);
+        when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID);
+
+        mController.selectPreset(TEST_PRESET_INDEX);
+
+        verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX);
+        verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX);
+    }
+
+    private BluetoothHapPresetInfo getHapPresetInfo(boolean available) {
+        BluetoothHapPresetInfo info = mock(BluetoothHapPresetInfo.class);
+        when(info.getName()).thenReturn(TEST_PRESET_NAME);
+        when(info.getIndex()).thenReturn(TEST_PRESET_INDEX);
+        when(info.isAvailable()).thenReturn(available);
+        return info;
+    }
+
+    private void setValidHearingDeviceSupportHap() {
+        LocalBluetoothProfile hapClientProfile = mock(HapClientProfile.class);
+        List<LocalBluetoothProfile> profiles = List.of(hapClientProfile);
+        when(mCachedBluetoothDevice.getProfiles()).thenReturn(profiles);
+
+        mController.setHearingDeviceIfSupportHap(mCachedBluetoothDevice);
+    }
+}