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
This commit is contained in:
Angela Wang
2023-11-22 12:39:17 +00:00
parent 4ee6d84eb4
commit 3e9f1ff659
13 changed files with 905 additions and 13 deletions

View File

@@ -32,7 +32,7 @@ flag {
flag { flag {
name: "new_hearing_device_pairing_page" name: "new_hearing_device_pairing_page"
namespace: "accessibility" 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" bug: "307473972"
} }

View File

@@ -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>

View File

@@ -28,11 +28,10 @@
settings:controller="com.android.settings.accessibility.AvailableHearingDevicePreferenceController"/> settings:controller="com.android.settings.accessibility.AvailableHearingDevicePreferenceController"/>
<com.android.settingslib.RestrictedPreference <com.android.settingslib.RestrictedPreference
android:key="add_bt_devices" android:key="hearing_device_add_bt_devices"
android:title="@string/bluetooth_pairing_pref_title" android:title="@string/bluetooth_pairing_pref_title"
android:icon="@drawable/ic_add_24dp" android:icon="@drawable/ic_add_24dp"
android:summary="@string/connected_device_add_device_summary" android:summary="@string/connected_device_add_device_summary"
android:fragment="com.android.settings.accessibility.HearingDevicePairingDetail"
settings:userRestriction="no_config_bluetooth" settings:userRestriction="no_config_bluetooth"
settings:useAdminDisabledSummary="true" settings:useAdminDisabledSummary="true"
settings:controller="com.android.settings.connecteddevice.AddDevicePreferenceController"/> settings:controller="com.android.settings.connecteddevice.AddDevicePreferenceController"/>

View File

@@ -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>

View File

@@ -36,9 +36,9 @@ import com.android.settingslib.search.SearchIndexable;
/** Accessibility settings for hearing aids. */ /** Accessibility settings for hearing aids. */
@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC) @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
public class AccessibilityHearingAidsFragment extends AccessibilityShortcutPreferenceFragment { public class AccessibilityHearingAidsFragment extends AccessibilityShortcutPreferenceFragment {
private static final String TAG = "AccessibilityHearingAidsFragment"; private static final String TAG = "AccessibilityHearingAidsFragment";
private static final String KEY_HEARING_OPTIONS_CATEGORY = "hearing_options_category"; 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 static final int SHORTCUT_PREFERENCE_IN_CATEGORY_INDEX = 20;
private String mFeatureName; private String mFeatureName;

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -156,7 +156,7 @@ public final class BluetoothDevicePreference extends GearPreference {
return R.layout.preference_widget_gear; return R.layout.preference_widget_gear;
} }
CachedBluetoothDevice getCachedDevice() { public CachedBluetoothDevice getCachedDevice() {
return mCachedDevice; 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(); Context context = getContext();
int bondState = mCachedDevice.getBondState(); int bondState = mCachedDevice.getBondState();

View File

@@ -29,8 +29,10 @@ import androidx.appcompat.app.AlertDialog;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.accessibility.HearingDevicePairingDetail; import com.android.settings.accessibility.HearingDevicePairingDetail;
import com.android.settings.accessibility.HearingDevicePairingFragment;
import com.android.settings.core.SubSettingLauncher; import com.android.settings.core.SubSettingLauncher;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settings.flags.Flags;
import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.HearingAidInfo; import com.android.settingslib.bluetooth.HearingAidInfo;
import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothManager;
@@ -123,8 +125,11 @@ public class HearingAidPairingDialogFragment extends InstrumentedDialogFragment
final int launchPage = getArguments().getInt(KEY_LAUNCH_PAGE); final int launchPage = getArguments().getInt(KEY_LAUNCH_PAGE);
final boolean launchFromA11y = (launchPage == SettingsEnums.ACCESSIBILITY) final boolean launchFromA11y = (launchPage == SettingsEnums.ACCESSIBILITY)
|| (launchPage == SettingsEnums.ACCESSIBILITY_HEARING_AID_SETTINGS); || (launchPage == SettingsEnums.ACCESSIBILITY_HEARING_AID_SETTINGS);
final String a11yDestination = Flags.newHearingDevicePairingPage()
? HearingDevicePairingFragment.class.getName()
: HearingDevicePairingDetail.class.getName();
final String destination = launchFromA11y final String destination = launchFromA11y
? HearingDevicePairingDetail.class.getName() ? a11yDestination
: BluetoothPairingDetail.class.getName(); : BluetoothPairingDetail.class.getName();
new SubSettingLauncher(getActivity()) new SubSettingLauncher(getActivity())
.setDestination(destination) .setDestination(destination)

View File

@@ -15,18 +15,25 @@
*/ */
package com.android.settings.connecteddevice; package com.android.settings.connecteddevice;
import static com.android.settings.accessibility.AccessibilityHearingAidsFragment.KEY_HEARING_DEVICE_ADD_BT_DEVICES;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.text.TextUtils;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import com.android.settings.R; 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.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.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop; 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 @Override
public int getAvailabilityStatus() { public int getAvailabilityStatus() {
return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH) return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)

View File

@@ -32,6 +32,10 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; 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.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity; 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.BluetoothPairingDetail;
import com.android.settings.bluetooth.HearingAidPairingDialogFragment; import com.android.settings.bluetooth.HearingAidPairingDialogFragment;
import com.android.settings.bluetooth.Utils; 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.ShadowBluetoothAdapter;
import com.android.settings.testutils.shadow.ShadowBluetoothUtils; import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDevice;
@@ -77,6 +82,9 @@ public class HearingAidPairingDialogFragmentTest {
@Rule @Rule
public final MockitoRule mockito = MockitoJUnit.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 String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1";
private static final int TEST_LAUNCH_PAGE = SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY; private static final int TEST_LAUNCH_PAGE = SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY;
@@ -129,7 +137,22 @@ public class HearingAidPairingDialogFragmentTest {
} }
@Test @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); setupDialog(SettingsEnums.ACCESSIBILITY);
final AlertDialog dialog = (AlertDialog) mFragment.onCreateDialog(Bundle.EMPTY); final AlertDialog dialog = (AlertDialog) mFragment.onCreateDialog(Bundle.EMPTY);
dialog.show(); dialog.show();

View File

@@ -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);
}
}
}

View File

@@ -15,34 +15,49 @@
*/ */
package com.android.settings.connecteddevice; 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.AVAILABLE;
import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertTrue; 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.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; 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 android.text.TextUtils;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R; 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 com.android.settingslib.RestrictedPreference;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowApplicationPackageManager; import org.robolectric.shadows.ShadowApplicationPackageManager;
import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers;
@@ -51,12 +66,16 @@ import org.robolectric.util.ReflectionHelpers;
@Config(shadows = ShadowApplicationPackageManager.class) @Config(shadows = ShadowApplicationPackageManager.class)
public class AddDevicePreferenceControllerTest { public class AddDevicePreferenceControllerTest {
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
@Mock @Mock
private PreferenceScreen mScreen; private PreferenceScreen mScreen;
@Mock @Mock
private BluetoothAdapter mBluetoothAdapter; private BluetoothAdapter mBluetoothAdapter;
private Context mContext; @Spy
private Context mContext = ApplicationProvider.getApplicationContext();
private AddDevicePreferenceController mAddDevicePreferenceController; private AddDevicePreferenceController mAddDevicePreferenceController;
private RestrictedPreference mAddDevicePreference; private RestrictedPreference mAddDevicePreference;
private ShadowApplicationPackageManager mPackageManager; private ShadowApplicationPackageManager mPackageManager;
@@ -66,8 +85,7 @@ public class AddDevicePreferenceControllerTest {
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application; mPackageManager = (ShadowApplicationPackageManager) shadowOf(
mPackageManager = (ShadowApplicationPackageManager) Shadows.shadowOf(
mContext.getPackageManager()); mContext.getPackageManager());
mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true); mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true);
@@ -82,6 +100,8 @@ public class AddDevicePreferenceControllerTest {
when(mBluetoothAdapter.isEnabled()).thenReturn(true); when(mBluetoothAdapter.isEnabled()).thenReturn(true);
when(mScreen.findPreference(key)).thenReturn(mAddDevicePreference); when(mScreen.findPreference(key)).thenReturn(mAddDevicePreference);
mAddDevicePreferenceController.displayPreference(mScreen); mAddDevicePreferenceController.displayPreference(mScreen);
doNothing().when(mContext).startActivity(any(Intent.class));
} }
@Test @Test
@@ -137,4 +157,30 @@ public class AddDevicePreferenceControllerTest {
assertThat(mAddDevicePreferenceController.getAvailabilityStatus()) assertThat(mAddDevicePreferenceController.getAvailabilityStatus())
.isEqualTo(UNSUPPORTED_ON_DEVICE); .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());
}
} }