From 3e9f1ff659a2aee540de75c188c67df2d910750f Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Wed, 22 Nov 2023 12:39:17 +0000 Subject: [PATCH 1/2] New hearing device pairing page (1/2) Rewrite a new hearing device pairing page with update UI for "See more devices". Bug: 307473972 Test: atest HearingDevicePairingFragmentTest Test: flip the flag com.android.settings.flags.new_hearing_device_pairing_page && atest HearingAidPairingDialogFragmentTest AddDevicePreferenceControllerTest Change-Id: Ic60601905e3d0d7d7c5b1ef9733652118a211f1d --- ...ssibility_flag_declarations_legacy.aconfig | 2 +- res/layout/arrow_preference.xml | 56 +++ res/xml/accessibility_hearing_aids.xml | 3 +- res/xml/hearing_device_pairing_fragment.xml | 45 ++ .../AccessibilityHearingAidsFragment.java | 2 +- .../accessibility/ArrowPreference.java | 58 +++ .../HearingDevicePairingFragment.java | 397 ++++++++++++++++++ .../bluetooth/BluetoothDevicePreference.java | 8 +- .../HearingAidPairingDialogFragment.java | 7 +- .../AddDevicePreferenceController.java | 22 + .../HearingAidPairingDialogFragmentTest.java | 25 +- .../HearingDevicePairingFragmentTest.java | 237 +++++++++++ .../AddDevicePreferenceControllerTest.java | 56 ++- 13 files changed, 905 insertions(+), 13 deletions(-) create mode 100644 res/layout/arrow_preference.xml create mode 100644 res/xml/hearing_device_pairing_fragment.xml create mode 100644 src/com/android/settings/accessibility/ArrowPreference.java create mode 100644 src/com/android/settings/accessibility/HearingDevicePairingFragment.java create mode 100644 tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingFragmentTest.java diff --git a/aconfig/settings_accessibility_flag_declarations_legacy.aconfig b/aconfig/settings_accessibility_flag_declarations_legacy.aconfig index acdce961c1e..5a464b587f1 100644 --- a/aconfig/settings_accessibility_flag_declarations_legacy.aconfig +++ b/aconfig/settings_accessibility_flag_declarations_legacy.aconfig @@ -32,7 +32,7 @@ flag { flag { name: "new_hearing_device_pairing_page" namespace: "accessibility" - description: "New hearing device pairing page with deny list method" + description: "New hearing device pairing page with extra MFi+ASHA filtering" bug: "307473972" } diff --git a/res/layout/arrow_preference.xml b/res/layout/arrow_preference.xml new file mode 100644 index 00000000000..0924a4452b2 --- /dev/null +++ b/res/layout/arrow_preference.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + diff --git a/res/xml/accessibility_hearing_aids.xml b/res/xml/accessibility_hearing_aids.xml index 20c8e29981a..57a0fe27819 100644 --- a/res/xml/accessibility_hearing_aids.xml +++ b/res/xml/accessibility_hearing_aids.xml @@ -28,11 +28,10 @@ settings:controller="com.android.settings.accessibility.AvailableHearingDevicePreferenceController"/> diff --git a/res/xml/hearing_device_pairing_fragment.xml b/res/xml/hearing_device_pairing_fragment.xml new file mode 100644 index 00000000000..1ccc1dd77c4 --- /dev/null +++ b/res/xml/hearing_device_pairing_fragment.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java index 33fef62f205..80a03c6ec2b 100644 --- a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java +++ b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java @@ -36,9 +36,9 @@ import com.android.settingslib.search.SearchIndexable; /** Accessibility settings for hearing aids. */ @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) public class AccessibilityHearingAidsFragment extends AccessibilityShortcutPreferenceFragment { - private static final String TAG = "AccessibilityHearingAidsFragment"; private static final String KEY_HEARING_OPTIONS_CATEGORY = "hearing_options_category"; + public static final String KEY_HEARING_DEVICE_ADD_BT_DEVICES = "hearing_device_add_bt_devices"; private static final int SHORTCUT_PREFERENCE_IN_CATEGORY_INDEX = 20; private String mFeatureName; diff --git a/src/com/android/settings/accessibility/ArrowPreference.java b/src/com/android/settings/accessibility/ArrowPreference.java new file mode 100644 index 00000000000..32e2bcb9ed6 --- /dev/null +++ b/src/com/android/settings/accessibility/ArrowPreference.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 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.settings.accessibility; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.Preference; + +import com.android.settings.R; + +/** + * A settings preference with colored rounded rectangle background and an arrow icon on the right + */ +public class ArrowPreference extends Preference { + + public ArrowPreference(@NonNull Context context) { + this(context, null); + } + + public ArrowPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, TypedArrayUtils.getAttr(context, + androidx.preference.R.attr.preferenceStyle, + android.R.attr.preferenceStyle)); + } + + public ArrowPreference(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ArrowPreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + setLayoutResource(R.layout.arrow_preference); + } +} diff --git a/src/com/android/settings/accessibility/HearingDevicePairingFragment.java b/src/com/android/settings/accessibility/HearingDevicePairingFragment.java new file mode 100644 index 00000000000..ffb5960cbd8 --- /dev/null +++ b/src/com/android/settings/accessibility/HearingDevicePairingFragment.java @@ -0,0 +1,397 @@ +/* + * Copyright (C) 2023 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.settings.accessibility; + +import static android.app.Activity.RESULT_OK; +import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; + +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothUuid; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.content.Context; +import android.os.Bundle; +import android.os.SystemProperties; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.bluetooth.BluetoothDevicePreference; +import com.android.settings.bluetooth.BluetoothProgressCategory; +import com.android.settings.bluetooth.Utils; +import com.android.settings.dashboard.RestrictedDashboardFragment; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.bluetooth.BluetoothCallback; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.HearingAidInfo; +import com.android.settingslib.bluetooth.HearingAidStatsLogUtils; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This fragment shows all scanned hearing devices through BLE scanning. Users can + * pair them in this page. + */ +public class HearingDevicePairingFragment extends RestrictedDashboardFragment implements + BluetoothCallback { + + private static final boolean DEBUG = true; + private static final String TAG = "HearingDevicePairingFragment"; + private static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY = + "persist.bluetooth.showdeviceswithoutnames"; + private static final String KEY_AVAILABLE_HEARING_DEVICES = "available_hearing_devices"; + + LocalBluetoothManager mLocalManager; + @Nullable + BluetoothAdapter mBluetoothAdapter; + @Nullable + CachedBluetoothDeviceManager mCachedDeviceManager; + + private boolean mShowDevicesWithoutNames; + @Nullable + private BluetoothProgressCategory mAvailableHearingDeviceGroup; + + @Nullable + BluetoothDevice mSelectedDevice; + final List mSelectedDeviceList = new ArrayList<>(); + final Map mDevicePreferenceMap = + new HashMap<>(); + + private List mLeScanFilters; + + public HearingDevicePairingFragment() { + super(DISALLOW_CONFIG_BLUETOOTH); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mLocalManager = Utils.getLocalBtManager(getActivity()); + if (mLocalManager == null) { + Log.e(TAG, "Bluetooth is not supported on this device"); + return; + } + mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter(); + mCachedDeviceManager = mLocalManager.getCachedDeviceManager(); + mShowDevicesWithoutNames = SystemProperties.getBoolean( + BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false); + + initPreferencesFromPreferenceScreen(); + initHearingDeviceLeScanFilters(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + use(ViewAllBluetoothDevicesPreferenceController.class).init(this); + } + + @Override + public void onStart() { + super.onStart(); + if (mLocalManager == null || mBluetoothAdapter == null || isUiRestricted()) { + return; + } + mLocalManager.setForegroundActivity(getActivity()); + mLocalManager.getEventManager().registerCallback(this); + if (mBluetoothAdapter.isEnabled()) { + startScanning(); + } else { + // Turn on bluetooth if it is disabled + mBluetoothAdapter.enable(); + } + } + + @Override + public void onStop() { + super.onStop(); + if (mLocalManager == null || isUiRestricted()) { + return; + } + stopScanning(); + removeAllDevices(); + mLocalManager.setForegroundActivity(null); + mLocalManager.getEventManager().unregisterCallback(this); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (preference instanceof BluetoothDevicePreference) { + stopScanning(); + BluetoothDevicePreference devicePreference = (BluetoothDevicePreference) preference; + mSelectedDevice = devicePreference.getCachedDevice().getDevice(); + if (mSelectedDevice != null) { + mSelectedDeviceList.add(mSelectedDevice); + } + devicePreference.onClicked(); + return true; + } + return super.onPreferenceTreeClick(preference); + } + + @Override + public void onDeviceDeleted(@NonNull CachedBluetoothDevice cachedDevice) { + removeDevice(cachedDevice); + } + + @Override + public void onBluetoothStateChanged(int bluetoothState) { + switch (bluetoothState) { + case BluetoothAdapter.STATE_ON: + startScanning(); + showBluetoothTurnedOnToast(); + break; + case BluetoothAdapter.STATE_OFF: + finish(); + break; + } + } + + @Override + public void onDeviceBondStateChanged(@NonNull CachedBluetoothDevice cachedDevice, + int bondState) { + if (DEBUG) { + Log.d(TAG, "onDeviceBondStateChanged: " + cachedDevice.getName() + ", state = " + + bondState); + } + if (bondState == BluetoothDevice.BOND_BONDED) { + // If one device is connected(bonded), then close this fragment. + setResult(RESULT_OK); + finish(); + return; + } else if (bondState == BluetoothDevice.BOND_BONDING) { + // Set the bond entry where binding process starts for logging hearing aid device info + final int pageId = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider() + .getAttribution(getActivity()); + final int bondEntry = AccessibilityStatsLogUtils.convertToHearingAidInfoBondEntry( + pageId); + HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice); + } + if (mSelectedDevice != null) { + BluetoothDevice device = cachedDevice.getDevice(); + if (mSelectedDevice.equals(device) && bondState == BluetoothDevice.BOND_NONE) { + // If current selected device failed to bond, restart scanning + startScanning(); + } + } + } + + @Override + public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice, + int state, int bluetoothProfile) { + // This callback is used to handle the case that bonded device is connected in pairing list. + // 1. If user selected multiple bonded devices in pairing list, after connected + // finish this page. + // 2. If the bonded devices auto connected in paring list, after connected it will be + // removed from paring list. + if (cachedDevice.isConnected()) { + final BluetoothDevice device = cachedDevice.getDevice(); + if (device != null && mSelectedDeviceList.contains(device)) { + setResult(RESULT_OK); + finish(); + } else { + removeDevice(cachedDevice); + } + } + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.HEARING_AID_PAIRING; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.hearing_device_pairing_fragment; + } + + + @Override + protected String getLogTag() { + return TAG; + } + + void addDevice(CachedBluetoothDevice cachedDevice) { + if (mBluetoothAdapter == null) { + return; + } + // Do not create new preference while the list shows one of the state messages + if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) { + return; + } + if (mDevicePreferenceMap.get(cachedDevice) != null) { + return; + } + String key = cachedDevice.getDevice().getAddress(); + BluetoothDevicePreference preference = (BluetoothDevicePreference) getCachedPreference(key); + if (preference == null) { + preference = new BluetoothDevicePreference(getPrefContext(), cachedDevice, + mShowDevicesWithoutNames, BluetoothDevicePreference.SortType.TYPE_FIFO); + preference.setKey(key); + preference.hideSecondTarget(true); + } + if (mAvailableHearingDeviceGroup != null) { + mAvailableHearingDeviceGroup.addPreference(preference); + } + mDevicePreferenceMap.put(cachedDevice, preference); + if (DEBUG) { + Log.d(TAG, "Add device. device: " + cachedDevice); + } + } + + void removeDevice(CachedBluetoothDevice cachedDevice) { + if (DEBUG) { + Log.d(TAG, "removeDevice: " + cachedDevice); + } + BluetoothDevicePreference preference = mDevicePreferenceMap.remove(cachedDevice); + if (mAvailableHearingDeviceGroup != null && preference != null) { + mAvailableHearingDeviceGroup.removePreference(preference); + } + } + + void startScanning() { + if (mCachedDeviceManager != null) { + mCachedDeviceManager.clearNonBondedDevices(); + } + removeAllDevices(); + startLeScanning(); + } + + void stopScanning() { + stopLeScanning(); + } + + private final ScanCallback mLeScanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + handleLeScanResult(result); + } + + @Override + public void onBatchScanResults(List results) { + for (ScanResult result: results) { + handleLeScanResult(result); + } + } + + @Override + public void onScanFailed(int errorCode) { + Log.w(TAG, "BLE Scan failed with error code " + errorCode); + } + }; + + void handleLeScanResult(ScanResult result) { + if (mCachedDeviceManager == null) { + return; + } + final BluetoothDevice device = result.getDevice(); + CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device); + if (cachedDevice == null) { + cachedDevice = mCachedDeviceManager.addDevice(device); + } + if (cachedDevice.getHearingAidInfo() == null) { + if (DEBUG) { + Log.d(TAG, "Set hearing aid info on device: " + cachedDevice); + } + cachedDevice.setHearingAidInfo(new HearingAidInfo.Builder().build()); + } + addDevice(cachedDevice); + } + + void startLeScanning() { + if (mBluetoothAdapter == null) { + return; + } + if (DEBUG) { + Log.v(TAG, "startLeScanning"); + } + final BluetoothLeScanner leScanner = mBluetoothAdapter.getBluetoothLeScanner(); + if (leScanner == null) { + Log.w(TAG, "LE scanner not found, cannot start LE scanning"); + } else { + final ScanSettings settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setLegacy(false) + .build(); + leScanner.startScan(mLeScanFilters, settings, mLeScanCallback); + if (mAvailableHearingDeviceGroup != null) { + mAvailableHearingDeviceGroup.setProgress(true); + } + } + } + + void stopLeScanning() { + if (mBluetoothAdapter == null) { + return; + } + if (DEBUG) { + Log.v(TAG, "stopLeScanning"); + } + final BluetoothLeScanner leScanner = mBluetoothAdapter.getBluetoothLeScanner(); + if (leScanner != null) { + leScanner.stopScan(mLeScanCallback); + if (mAvailableHearingDeviceGroup != null) { + mAvailableHearingDeviceGroup.setProgress(false); + } + } + } + + private void removeAllDevices() { + mDevicePreferenceMap.clear(); + if (mAvailableHearingDeviceGroup != null) { + mAvailableHearingDeviceGroup.removeAll(); + } + } + + void initPreferencesFromPreferenceScreen() { + mAvailableHearingDeviceGroup = findPreference(KEY_AVAILABLE_HEARING_DEVICES); + } + + private void initHearingDeviceLeScanFilters() { + mLeScanFilters = new ArrayList<>(); + // Filters for ASHA hearing aids + mLeScanFilters.add( + new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HEARING_AID).build()); + mLeScanFilters.add(new ScanFilter.Builder() + .setServiceData(BluetoothUuid.HEARING_AID, new byte[0]).build()); + // Filters for LE audio hearing aids + mLeScanFilters.add(new ScanFilter.Builder().setServiceUuid(BluetoothUuid.HAS).build()); + mLeScanFilters.add(new ScanFilter.Builder() + .setServiceData(BluetoothUuid.HAS, new byte[0]).build()); + } + + void showBluetoothTurnedOnToast() { + Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast, + Toast.LENGTH_SHORT).show(); + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java index 98d78f24341..ac0c63bc6de 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java @@ -156,7 +156,7 @@ public final class BluetoothDevicePreference extends GearPreference { return R.layout.preference_widget_gear; } - CachedBluetoothDevice getCachedDevice() { + public CachedBluetoothDevice getCachedDevice() { return mCachedDevice; } @@ -362,7 +362,11 @@ public final class BluetoothDevicePreference extends GearPreference { } } - void onClicked() { + /** + * Performs different actions according to the device connected and bonded state after + * clicking on the preference. + */ + public void onClicked() { Context context = getContext(); int bondState = mCachedDevice.getBondState(); diff --git a/src/com/android/settings/bluetooth/HearingAidPairingDialogFragment.java b/src/com/android/settings/bluetooth/HearingAidPairingDialogFragment.java index 12cbd58c48a..3a16e3e3ebb 100644 --- a/src/com/android/settings/bluetooth/HearingAidPairingDialogFragment.java +++ b/src/com/android/settings/bluetooth/HearingAidPairingDialogFragment.java @@ -29,8 +29,10 @@ import androidx.appcompat.app.AlertDialog; import com.android.settings.R; import com.android.settings.accessibility.HearingDevicePairingDetail; +import com.android.settings.accessibility.HearingDevicePairingFragment; import com.android.settings.core.SubSettingLauncher; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.flags.Flags; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HearingAidInfo; import com.android.settingslib.bluetooth.LocalBluetoothManager; @@ -123,8 +125,11 @@ public class HearingAidPairingDialogFragment extends InstrumentedDialogFragment final int launchPage = getArguments().getInt(KEY_LAUNCH_PAGE); final boolean launchFromA11y = (launchPage == SettingsEnums.ACCESSIBILITY) || (launchPage == SettingsEnums.ACCESSIBILITY_HEARING_AID_SETTINGS); + final String a11yDestination = Flags.newHearingDevicePairingPage() + ? HearingDevicePairingFragment.class.getName() + : HearingDevicePairingDetail.class.getName(); final String destination = launchFromA11y - ? HearingDevicePairingDetail.class.getName() + ? a11yDestination : BluetoothPairingDetail.class.getName(); new SubSettingLauncher(getActivity()) .setDestination(destination) diff --git a/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java b/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java index d2bc319ce95..ef448438bbb 100644 --- a/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java +++ b/src/com/android/settings/connecteddevice/AddDevicePreferenceController.java @@ -15,18 +15,25 @@ */ package com.android.settings.connecteddevice; +import static com.android.settings.accessibility.AccessibilityHearingAidsFragment.KEY_HEARING_DEVICE_ADD_BT_DEVICES; + import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.text.TextUtils; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.accessibility.HearingDevicePairingDetail; +import com.android.settings.accessibility.HearingDevicePairingFragment; import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.flags.Flags; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; @@ -75,6 +82,21 @@ public class AddDevicePreferenceController extends BasePreferenceController } } + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (TextUtils.equals(preference.getKey(), KEY_HEARING_DEVICE_ADD_BT_DEVICES)) { + String destination = Flags.newHearingDevicePairingPage() + ? HearingDevicePairingFragment.class.getName() + : HearingDevicePairingDetail.class.getName(); + new SubSettingLauncher(preference.getContext()) + .setDestination(destination) + .setSourceMetricsCategory(getMetricsCategory()) + .launch(); + return true; + } + return super.handlePreferenceTreeClick(preference); + } + @Override public int getAvailabilityStatus() { return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingAidPairingDialogFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingAidPairingDialogFragmentTest.java index 6c1de594bf1..bd57e9d6ad7 100644 --- a/tests/robotests/src/com/android/settings/accessibility/HearingAidPairingDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/HearingAidPairingDialogFragmentTest.java @@ -32,6 +32,10 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.FragmentActivity; @@ -43,6 +47,7 @@ import com.android.settings.SettingsActivity; import com.android.settings.bluetooth.BluetoothPairingDetail; import com.android.settings.bluetooth.HearingAidPairingDialogFragment; import com.android.settings.bluetooth.Utils; +import com.android.settings.flags.Flags; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowBluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; @@ -77,6 +82,9 @@ public class HearingAidPairingDialogFragmentTest { @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"; private static final int TEST_LAUNCH_PAGE = SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY; @@ -129,7 +137,22 @@ public class HearingAidPairingDialogFragmentTest { } @Test - public void dialogPositiveButtonClick_intentToA11yPairingPage() { + @RequiresFlagsEnabled(Flags.FLAG_NEW_HEARING_DEVICE_PAIRING_PAGE) + public void dialogPositiveButtonClick_intentToNewA11yPairingPage() { + setupDialog(SettingsEnums.ACCESSIBILITY); + final AlertDialog dialog = (AlertDialog) mFragment.onCreateDialog(Bundle.EMPTY); + dialog.show(); + + dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick(); + + final Intent intent = shadowOf(mActivity).getNextStartedActivity(); + assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(HearingDevicePairingFragment.class.getName()); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_NEW_HEARING_DEVICE_PAIRING_PAGE) + public void dialogPositiveButtonClick_intentToOldA11yPairingPage() { setupDialog(SettingsEnums.ACCESSIBILITY); final AlertDialog dialog = (AlertDialog) mFragment.onCreateDialog(Bundle.EMPTY); dialog.show(); diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingFragmentTest.java new file mode 100644 index 00000000000..134f8652b38 --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingFragmentTest.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2023 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.settings.accessibility; + + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.le.ScanResult; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.Pair; + +import androidx.preference.Preference; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.bluetooth.BluetoothDevicePreference; +import com.android.settings.bluetooth.BluetoothProgressCategory; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.HearingAidInfo; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Tests for {@link HearingDevicePairingFragment}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class}) +public class HearingDevicePairingFragmentTest { + + private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"; + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Spy + private final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + @Spy + private final HearingDevicePairingFragment mFragment = new TestHearingDevicePairingFragment(); + + @Mock + private LocalBluetoothManager mLocalManager; + @Mock + private CachedBluetoothDeviceManager mCachedDeviceManager; + @Mock + private CachedBluetoothDevice mCachedDevice; + @Mock + private BluetoothProgressCategory mAvailableHearingDeviceGroup; + + private final Context mContext = ApplicationProvider.getApplicationContext(); + private BluetoothDevice mDevice; + private BluetoothDevicePreference mDevicePreference; + + + @Before + public void setUp() { + mFragment.mLocalManager = mLocalManager; + mFragment.mCachedDeviceManager = mCachedDeviceManager; + mFragment.mBluetoothAdapter = mBluetoothAdapter; + doReturn(mContext).when(mFragment).getContext(); + doReturn(mAvailableHearingDeviceGroup).when(mFragment).findPreference( + "available_hearing_devices"); + mFragment.initPreferencesFromPreferenceScreen(); + + + mDevice = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS); + doReturn(mDevice).when(mCachedDevice).getDevice(); + final Pair pair = new Pair<>(mock(Drawable.class), "test_device"); + doReturn(pair).when(mCachedDevice).getDrawableWithDescription(); + + mDevicePreference = new BluetoothDevicePreference(mContext, mCachedDevice, true, + BluetoothDevicePreference.SortType.TYPE_DEFAULT); + } + + @Test + public void startAndStopScanning_stateIsCorrect() { + mFragment.startScanning(); + + verify(mFragment).startLeScanning(); + + mFragment.stopScanning(); + + verify(mFragment).stopLeScanning(); + } + + @Test + public void onDeviceDeleted_stateIsCorrect() { + mFragment.mDevicePreferenceMap.put(mCachedDevice, mDevicePreference); + + assertThat(mFragment.mDevicePreferenceMap).isNotEmpty(); + + mFragment.onDeviceDeleted(mCachedDevice); + + assertThat(mFragment.mDevicePreferenceMap).isEmpty(); + verify(mAvailableHearingDeviceGroup).removePreference(mDevicePreference); + } + + @Test + public void addDevice_bluetoothOff_doNothing() { + doReturn(BluetoothAdapter.STATE_OFF).when(mBluetoothAdapter).getState(); + + assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0); + + mFragment.addDevice(mCachedDevice); + + verify(mAvailableHearingDeviceGroup, never()).addPreference(mDevicePreference); + assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0); + } + + @Test + public void addDevice_addToAvailableHearingDeviceGroup() { + doReturn(BluetoothAdapter.STATE_ON).when(mBluetoothAdapter).getState(); + + assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(0); + + mFragment.addDevice(mCachedDevice); + + verify(mAvailableHearingDeviceGroup).addPreference(mDevicePreference); + assertThat(mFragment.mDevicePreferenceMap.size()).isEqualTo(1); + } + + @Test + public void handleLeScanResult_markDeviceAsHearingAid() { + ScanResult scanResult = mock(ScanResult.class); + doReturn(mDevice).when(scanResult).getDevice(); + doReturn(mCachedDevice).when(mCachedDeviceManager).findDevice(mDevice); + + mFragment.handleLeScanResult(scanResult); + + verify(mCachedDevice).setHearingAidInfo(new HearingAidInfo.Builder().build()); + verify(mFragment).addDevice(mCachedDevice); + } + + @Test + public void onProfileConnectionStateChanged_deviceConnected_inSelectedList_finish() { + doReturn(true).when(mCachedDevice).isConnected(); + mFragment.mSelectedDeviceList.add(mDevice); + + mFragment.onProfileConnectionStateChanged(mCachedDevice, BluetoothAdapter.STATE_CONNECTED, + BluetoothProfile.A2DP); + + verify(mFragment).finish(); + } + + @Test + public void onProfileConnectionStateChanged_deviceConnected_notInSelectedList_deleteDevice() { + doReturn(true).when(mCachedDevice).isConnected(); + + mFragment.onProfileConnectionStateChanged(mCachedDevice, BluetoothAdapter.STATE_CONNECTED, + BluetoothProfile.A2DP); + + verify(mFragment).removeDevice(mCachedDevice); + } + + @Test + public void onProfileConnectionStateChanged_deviceNotConnected_doNothing() { + doReturn(false).when(mCachedDevice).isConnected(); + + mFragment.onProfileConnectionStateChanged(mCachedDevice, BluetoothAdapter.STATE_CONNECTED, + BluetoothProfile.A2DP); + + verify(mFragment, never()).finish(); + verify(mFragment, never()).removeDevice(mCachedDevice); + } + + @Test + public void onBluetoothStateChanged_stateOn_startScanningAndShowToast() { + mFragment.onBluetoothStateChanged(BluetoothAdapter.STATE_ON); + + verify(mFragment).startScanning(); + verify(mFragment).showBluetoothTurnedOnToast(); + } + + @Test + public void onBluetoothStateChanged_stateOff_finish() { + mFragment.onBluetoothStateChanged(BluetoothAdapter.STATE_OFF); + + verify(mFragment).finish(); + } + + @Test + public void onDeviceBondStateChanged_bonded_finish() { + mFragment.onDeviceBondStateChanged(mCachedDevice, BluetoothDevice.BOND_BONDED); + + verify(mFragment).finish(); + } + + @Test + public void onDeviceBondStateChanged_selectedDeviceNotBonded_startScanning() { + mFragment.mSelectedDevice = mDevice; + + mFragment.onDeviceBondStateChanged(mCachedDevice, BluetoothDevice.BOND_NONE); + + verify(mFragment).startScanning(); + } + + private class TestHearingDevicePairingFragment extends HearingDevicePairingFragment { + @Override + protected Preference getCachedPreference(String key) { + if (key.equals(TEST_DEVICE_ADDRESS)) { + return mDevicePreference; + } + return super.getCachedPreference(key); + } + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/AddDevicePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/AddDevicePreferenceControllerTest.java index 7384d3ace39..63fa88d7792 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/AddDevicePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/AddDevicePreferenceControllerTest.java @@ -15,34 +15,49 @@ */ package com.android.settings.connecteddevice; +import static com.android.settings.accessibility.AccessibilityHearingAidsFragment.KEY_HEARING_DEVICE_ADD_BT_DEVICES; import static com.android.settings.core.BasePreferenceController.AVAILABLE; import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; import android.bluetooth.BluetoothAdapter; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.text.TextUtils; import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.accessibility.HearingDevicePairingDetail; +import com.android.settings.accessibility.HearingDevicePairingFragment; +import com.android.settings.flags.Flags; import com.android.settingslib.RestrictedPreference; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplicationPackageManager; import org.robolectric.util.ReflectionHelpers; @@ -51,12 +66,16 @@ import org.robolectric.util.ReflectionHelpers; @Config(shadows = ShadowApplicationPackageManager.class) public class AddDevicePreferenceControllerTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Mock private PreferenceScreen mScreen; @Mock private BluetoothAdapter mBluetoothAdapter; - private Context mContext; + @Spy + private Context mContext = ApplicationProvider.getApplicationContext(); private AddDevicePreferenceController mAddDevicePreferenceController; private RestrictedPreference mAddDevicePreference; private ShadowApplicationPackageManager mPackageManager; @@ -66,8 +85,7 @@ public class AddDevicePreferenceControllerTest { public void setUp() { MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; - mPackageManager = (ShadowApplicationPackageManager) Shadows.shadowOf( + mPackageManager = (ShadowApplicationPackageManager) shadowOf( mContext.getPackageManager()); mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true); @@ -82,6 +100,8 @@ public class AddDevicePreferenceControllerTest { when(mBluetoothAdapter.isEnabled()).thenReturn(true); when(mScreen.findPreference(key)).thenReturn(mAddDevicePreference); mAddDevicePreferenceController.displayPreference(mScreen); + + doNothing().when(mContext).startActivity(any(Intent.class)); } @Test @@ -137,4 +157,30 @@ public class AddDevicePreferenceControllerTest { assertThat(mAddDevicePreferenceController.getAvailabilityStatus()) .isEqualTo(UNSUPPORTED_ON_DEVICE); } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_NEW_HEARING_DEVICE_PAIRING_PAGE) + public void handlePreferenceClick_A11yPreference_redirectToNewPairingPage() { + mAddDevicePreference.setKey(KEY_HEARING_DEVICE_ADD_BT_DEVICES); + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + + mAddDevicePreferenceController.handlePreferenceTreeClick(mAddDevicePreference); + + verify(mContext).startActivity(intentCaptor.capture()); + assertThat(intentCaptor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(HearingDevicePairingFragment.class.getName()); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_NEW_HEARING_DEVICE_PAIRING_PAGE) + public void handlePreferenceClick_A11yPreference_redirectToOldPairingPage() { + mAddDevicePreference.setKey(KEY_HEARING_DEVICE_ADD_BT_DEVICES); + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + + mAddDevicePreferenceController.handlePreferenceTreeClick(mAddDevicePreference); + + verify(mContext).startActivity(intentCaptor.capture()); + assertThat(intentCaptor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(HearingDevicePairingDetail.class.getName()); + } } From 5cb00f6602f9025b97829a56eb13da466b0ea218 Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Mon, 27 Nov 2023 20:30:46 +0000 Subject: [PATCH 2/2] 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 --- .../HearingDevicePairingFragment.java | 96 ++++++++++++++++++- .../HearingDevicePairingFragmentTest.java | 81 ++++++++++++++++ 2 files changed, 176 insertions(+), 1 deletion(-) 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) {