diff --git a/src/com/android/settings/accessibility/HearingDevicePairingFragment.java b/src/com/android/settings/accessibility/HearingDevicePairingFragment.java index ffb5960cbd8..fb79ece55bf 100644 --- a/src/com/android/settings/accessibility/HearingDevicePairingFragment.java +++ b/src/com/android/settings/accessibility/HearingDevicePairingFragment.java @@ -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 mSelectedDeviceList = new ArrayList<>(); + final List mConnectingGattList = new ArrayList<>(); final Map 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()); } - addDevice(cachedDevice); + // 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 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() { diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingFragmentTest.java index 134f8652b38..e14686e4a18 100644 --- a/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingFragmentTest.java @@ -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) {