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
diff --git a/aconfig/settings_accessibility_flag_declarations_legacy.aconfig b/aconfig/settings_accessibility_flag_declarations_legacy.aconfig
index acdce96..5a464b5 100644
--- a/aconfig/settings_accessibility_flag_declarations_legacy.aconfig
+++ b/aconfig/settings_accessibility_flag_declarations_legacy.aconfig
@@ -32,7 +32,7 @@
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 0000000..0924a44
--- /dev/null
+++ b/res/layout/arrow_preference.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingVertical="@dimen/settingslib_switchbar_margin"
+ android:background="@android:color/transparent">
+
+ <LinearLayout
+ android:id="@+id/background"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:paddingStart="@dimen/settingslib_switchbar_padding_left"
+ android:paddingEnd="@dimen/settingslib_switchbar_padding_right"
+ android:background="@drawable/settingslib_switch_bar_bg_on"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_gravity="start|center_vertical"
+ android:layout_weight="1"
+ android:paddingVertical="@dimen/settingslib_switch_title_margin"
+ android:ellipsize="end"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:hyphenationFrequency="normalFast"
+ android:lineBreakWordStyle="phrase"
+ style="@style/MainSwitchText.Settingslib"/>
+
+ <ImageView
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="@null"
+ android:src="@drawable/ic_arrow_forward"/>
+
+ </LinearLayout>
+</FrameLayout>
diff --git a/res/xml/accessibility_hearing_aids.xml b/res/xml/accessibility_hearing_aids.xml
index 20c8e29..57a0fe2 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"/>
<com.android.settingslib.RestrictedPreference
- android:key="add_bt_devices"
+ android:key="hearing_device_add_bt_devices"
android:title="@string/bluetooth_pairing_pref_title"
android:icon="@drawable/ic_add_24dp"
android:summary="@string/connected_device_add_device_summary"
- android:fragment="com.android.settings.accessibility.HearingDevicePairingDetail"
settings:userRestriction="no_config_bluetooth"
settings:useAdminDisabledSummary="true"
settings:controller="com.android.settings.connecteddevice.AddDevicePreferenceController"/>
diff --git a/res/xml/hearing_device_pairing_fragment.xml b/res/xml/hearing_device_pairing_fragment.xml
new file mode 100644
index 0000000..1ccc1dd
--- /dev/null
+++ b/res/xml/hearing_device_pairing_fragment.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:title="@string/bluetooth_pairing_pref_title">
+
+ <com.android.settings.bluetooth.BluetoothProgressCategory
+ android:key="available_hearing_devices"
+ android:title="@string/accessibility_found_hearing_devices" />
+
+ <PreferenceCategory
+ android:key="more_devices_category"
+ android:title="@string/accessibility_found_all_devices">
+ <com.android.settings.accessibility.ArrowPreference
+ android:key="more_devices"
+ android:title="@string/accessibility_list_all_devices_title"
+ settings:searchable="false"
+ settings:userRestriction="no_config_bluetooth"
+ settings:useAdminDisabledSummary="true"
+ settings:controller="com.android.settings.accessibility.ViewAllBluetoothDevicesPreferenceController"/>
+ </PreferenceCategory>
+
+ <com.android.settings.accessibility.AccessibilityFooterPreference
+ android:key="hearing_device_footer"
+ android:title="@string/accessibility_hearing_device_footer_summary"
+ android:selectable="false"
+ settings:searchable="false"
+ settings:controller="com.android.settings.accessibility.PairHearingDeviceFooterPreferenceController"/>
+
+</PreferenceScreen>
\ 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 33fef62..80a03c6 100644
--- a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java
+++ b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java
@@ -36,9 +36,9 @@
/** 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 0000000..32e2bcb
--- /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 0000000..ffb5960
--- /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<BluetoothDevice> mSelectedDeviceList = new ArrayList<>();
+ final Map<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap =
+ new HashMap<>();
+
+ private List<ScanFilter> 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<ScanResult> 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 98d78f2..ac0c63b 100644
--- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
+++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
@@ -156,7 +156,7 @@
return R.layout.preference_widget_gear;
}
- CachedBluetoothDevice getCachedDevice() {
+ public CachedBluetoothDevice getCachedDevice() {
return mCachedDevice;
}
@@ -362,7 +362,11 @@
}
}
- 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 12cbd58..3a16e3e 100644
--- a/src/com/android/settings/bluetooth/HearingAidPairingDialogFragment.java
+++ b/src/com/android/settings/bluetooth/HearingAidPairingDialogFragment.java
@@ -29,8 +29,10 @@
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 @@
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 d2bc319..ef44843 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;
@@ -76,6 +83,21 @@
}
@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)
? AVAILABLE
diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingAidPairingDialogFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingAidPairingDialogFragmentTest.java
index 6c1de59..bd57e9d 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.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.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 @@
@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 @@
}
@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 0000000..134f865
--- /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<Drawable, String> 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 7384d3a..63fa88d 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 @@
@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 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 @@
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 @@
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<Intent> 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<Intent> 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());
+ }
}