Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a1477b70 authored by jasonwshsu's avatar jasonwshsu
Browse files

Fix crash in Hearing Devices dialog when connecting to ASHA device with LE Audio disabled

Root Cause: HapClientProfile can not be initiated

Solution: set device to preset control only if device and remote device both supports HAP profile

Bug: 356542605
Test: atest HearingDevicesPresetsControllerTest
Flag: EXEMPT bugfix
Change-Id: Ib1b736d9d56306a5b26c26f0d18d6e78cfa7459c
parent 0a258ab8
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -228,7 +228,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
        mHearingDeviceItemList = getHearingDevicesList();
        if (mPresetsController != null) {
            activeHearingDevice = getActiveHearingDevice(mHearingDeviceItemList);
            mPresetsController.setActiveHearingDevice(activeHearingDevice);
            mPresetsController.setHearingDeviceIfSupportHap(activeHearingDevice);
        } else {
            activeHearingDevice = null;
        }
@@ -336,7 +336,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
        }
        final CachedBluetoothDevice activeHearingDevice = getActiveHearingDevice(
                mHearingDeviceItemList);
        mPresetsController.setActiveHearingDevice(activeHearingDevice);
        mPresetsController.setHearingDeviceIfSupportHap(activeHearingDevice);

        mPresetInfoAdapter = new ArrayAdapter<>(dialog.getContext(),
                R.layout.hearing_devices_preset_spinner_selected,
+22 −11
Original line number Diff line number Diff line
@@ -113,7 +113,7 @@ public class HearingDevicesPresetsController implements

    @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 @@ public class HearingDevicesPresetsController implements

    @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 @@ public class HearingDevicesPresetsController implements
    }

    /**
     * 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) {
    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 @@ public class HearingDevicesPresetsController implements
     * @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 @@ public class HearingDevicesPresetsController implements
     * @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 @@ public class HearingDevicesPresetsController implements
    }

    private void selectPresetIndependently(int presetIndex) {
        if (mActiveHearingDevice == null) {
        if (mActiveHearingDevice == null || mHapClientProfile == null) {
            return;
        }
        if (DEBUG) {
+285 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}