From 3e9f1ff659a2aee540de75c188c67df2d910750f Mon Sep 17 00:00:00 2001 From: Angela Wang Date: Wed, 22 Nov 2023 12:39:17 +0000 Subject: [PATCH] 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()); + } }