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

Commit 5cb00f66 authored by Angela Wang's avatar Angela Wang
Browse files

New hearing device pairing page (2/2): MFi devices

Some of the hearing aids support both ASHA + MFi, however, they only
advertise MFi service uuid in advertisement packets.

We can filter the devices with MFi uuid while scanning and then connect
gatt to discover the remote services before pairing to make sure if the
devices are compatible with Android or not. Only devices that support
ASHA/HAP will be shown.

Bug: 307890347
Test: atest HearingDevicePairingFragmentTest
Change-Id: Ie1f4eedddd4c43fad0fcbcd35f436dea5ab06925
parent 3e9f1ff6
Loading
Loading
Loading
Loading
+95 −1
Original line number Diff line number Diff line
@@ -22,15 +22,20 @@ import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.os.Bundle;
import android.os.ParcelUuid;
import android.os.SystemProperties;
import android.util.Log;
import android.widget.Toast;
@@ -83,6 +88,7 @@ public class HearingDevicePairingFragment extends RestrictedDashboardFragment im
    @Nullable
    BluetoothDevice mSelectedDevice;
    final List<BluetoothDevice> mSelectedDeviceList = new ArrayList<>();
    final List<BluetoothGatt> mConnectingGattList = new ArrayList<>();
    final Map<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap =
            new HashMap<>();

@@ -140,6 +146,9 @@ public class HearingDevicePairingFragment extends RestrictedDashboardFragment im
        }
        stopScanning();
        removeAllDevices();
        for (BluetoothGatt gatt: mConnectingGattList) {
            gatt.disconnect();
        }
        mLocalManager.setForegroundActivity(null);
        mLocalManager.getEventManager().unregisterCallback(this);
    }
@@ -325,7 +334,16 @@ public class HearingDevicePairingFragment extends RestrictedDashboardFragment im
            }
            cachedDevice.setHearingAidInfo(new HearingAidInfo.Builder().build());
        }
        // No need to handle the device if the device is already in the list or discovering services
        if (mDevicePreferenceMap.get(cachedDevice) == null
                && mConnectingGattList.stream().noneMatch(
                        gatt -> gatt.getDevice().equals(device))) {
            if (isAndroidCompatibleHearingAid(result)) {
                addDevice(cachedDevice);
            } else {
                discoverServices(cachedDevice);
            }
        }
    }

    void startLeScanning() {
@@ -388,6 +406,82 @@ public class HearingDevicePairingFragment extends RestrictedDashboardFragment im
        mLeScanFilters.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HAS).build());
        mLeScanFilters.add(new ScanFilter.Builder()
                .setServiceData(BluetoothUuid.HAS, new byte[0]).build());
        // Filters for MFi hearing aids
        mLeScanFilters.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.MFI_HAS).build());
        mLeScanFilters.add(new ScanFilter.Builder()
                .setServiceData(BluetoothUuid.MFI_HAS, new byte[0]).build());
    }

    boolean isAndroidCompatibleHearingAid(ScanResult scanResult) {
        ScanRecord scanRecord = scanResult.getScanRecord();
        if (scanRecord == null) {
            if (DEBUG) {
                Log.d(TAG, "Scan record is null, not compatible with Android. device: "
                        + scanResult.getDevice());
            }
            return false;
        }
        List<ParcelUuid> uuids = scanRecord.getServiceUuids();
        if (uuids != null) {
            if (uuids.contains(BluetoothUuid.HEARING_AID) || uuids.contains(BluetoothUuid.HAS)) {
                if (DEBUG) {
                    Log.d(TAG, "Scan record uuid matched, compatible with Android. device: "
                            + scanResult.getDevice());
                }
                return true;
            }
        }
        if (scanRecord.getServiceData(BluetoothUuid.HEARING_AID) != null
                || scanRecord.getServiceData(BluetoothUuid.HAS) != null) {
            if (DEBUG) {
                Log.d(TAG, "Scan record service data matched, compatible with Android. device: "
                        + scanResult.getDevice());
            }
            return true;
        }
        if (DEBUG) {
            Log.d(TAG, "Scan record mismatched, not compatible with Android. device: "
                    + scanResult.getDevice());
        }
        return false;
    }

    void discoverServices(CachedBluetoothDevice cachedDevice) {
        if (DEBUG) {
            Log.d(TAG, "connectGattToCheckCompatibility, device: " + cachedDevice);
        }
        BluetoothGatt gatt = cachedDevice.getDevice().connectGatt(getContext(), false,
                new BluetoothGattCallback() {
                    @Override
                    public void onConnectionStateChange(BluetoothGatt gatt, int status,
                            int newState) {
                        super.onConnectionStateChange(gatt, status, newState);
                        if (DEBUG) {
                            Log.d(TAG, "onConnectionStateChange, status: " + status + ", newState: "
                                    + newState + ", device: " + cachedDevice);
                        }
                        if (newState == BluetoothProfile.STATE_CONNECTED) {
                            gatt.discoverServices();
                        }
                    }

                    @Override
                    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
                        super.onServicesDiscovered(gatt, status);
                        boolean isCompatible = gatt.getService(BluetoothUuid.HEARING_AID.getUuid())
                                != null
                                || gatt.getService(BluetoothUuid.HAS.getUuid()) != null;
                        if (DEBUG) {
                            Log.d(TAG,
                                    "onServicesDiscovered, compatible with Android: " + isCompatible
                                            + ", device: " + cachedDevice);
                        }
                        if (isCompatible) {
                            addDevice(cachedDevice);
                        }
                    }
                });
        mConnectingGattList.add(gatt);
    }

    void showBluetoothTurnedOnToast() {
+81 −0
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import static org.mockito.Mockito.verify;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.content.Context;
import android.graphics.drawable.Drawable;
@@ -54,6 +56,8 @@ import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import java.util.List;

/** Tests for {@link HearingDevicePairingFragment}. */
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowBluetoothAdapter.class})
@@ -159,9 +163,32 @@ public class HearingDevicePairingFragmentTest {
        mFragment.handleLeScanResult(scanResult);

        verify(mCachedDevice).setHearingAidInfo(new HearingAidInfo.Builder().build());
    }

    @Test
    public void handleLeScanResult_isAndroidCompatible_addDevice() {
        ScanResult scanResult = mock(ScanResult.class);
        doReturn(mDevice).when(scanResult).getDevice();
        doReturn(mCachedDevice).when(mCachedDeviceManager).findDevice(mDevice);
        doReturn(true).when(mFragment).isAndroidCompatibleHearingAid(scanResult);

        mFragment.handleLeScanResult(scanResult);

        verify(mFragment).addDevice(mCachedDevice);
    }

    @Test
    public void handleLeScanResult_isNotAndroidCompatible_() {
        ScanResult scanResult = mock(ScanResult.class);
        doReturn(mDevice).when(scanResult).getDevice();
        doReturn(mCachedDevice).when(mCachedDeviceManager).findDevice(mDevice);
        doReturn(false).when(mFragment).isAndroidCompatibleHearingAid(scanResult);

        mFragment.handleLeScanResult(scanResult);

        verify(mFragment).discoverServices(mCachedDevice);
    }

    @Test
    public void onProfileConnectionStateChanged_deviceConnected_inSelectedList_finish() {
        doReturn(true).when(mCachedDevice).isConnected();
@@ -225,6 +252,60 @@ public class HearingDevicePairingFragmentTest {
        verify(mFragment).startScanning();
    }

    @Test
    public void isAndroidCompatibleHearingAid_asha_returnTrue() {
        ScanResult scanResult = createAshaScanResult();

        boolean isCompatible = mFragment.isAndroidCompatibleHearingAid(scanResult);

        assertThat(isCompatible).isTrue();
    }

    @Test
    public void isAndroidCompatibleHearingAid_has_returnTrue() {
        ScanResult scanResult = createHasScanResult();

        boolean isCompatible = mFragment.isAndroidCompatibleHearingAid(scanResult);

        assertThat(isCompatible).isTrue();
    }

    @Test
    public void isAndroidCompatibleHearingAid_mfiHas_returnFalse() {
        ScanResult scanResult = createMfiHasScanResult();

        boolean isCompatible = mFragment.isAndroidCompatibleHearingAid(scanResult);

        assertThat(isCompatible).isFalse();
    }

    private ScanResult createAshaScanResult() {
        ScanResult scanResult = mock(ScanResult.class);
        ScanRecord scanRecord = mock(ScanRecord.class);
        byte[] fakeAshaServiceData = new byte[] {
                0x09, 0x16, (byte) 0xf0, (byte) 0xfd, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04};
        doReturn(scanRecord).when(scanResult).getScanRecord();
        doReturn(fakeAshaServiceData).when(scanRecord).getServiceData(BluetoothUuid.HEARING_AID);
        return scanResult;
    }

    private ScanResult createHasScanResult() {
        ScanResult scanResult = mock(ScanResult.class);
        ScanRecord scanRecord = mock(ScanRecord.class);
        doReturn(scanRecord).when(scanResult).getScanRecord();
        doReturn(List.of(BluetoothUuid.HAS)).when(scanRecord).getServiceUuids();
        return scanResult;
    }

    private ScanResult createMfiHasScanResult() {
        ScanResult scanResult = mock(ScanResult.class);
        ScanRecord scanRecord = mock(ScanRecord.class);
        byte[] fakeMfiServiceData = new byte[] {0x00, 0x00, 0x00, 0x00};
        doReturn(scanRecord).when(scanResult).getScanRecord();
        doReturn(fakeMfiServiceData).when(scanRecord).getServiceData(BluetoothUuid.MFI_HAS);
        return scanResult;
    }

    private class TestHearingDevicePairingFragment extends HearingDevicePairingFragment {
        @Override
        protected Preference getCachedPreference(String key) {