diff --git a/res/values/strings.xml b/res/values/strings.xml index ad6865758c9..14908bd8267 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4557,7 +4557,7 @@ Pair hearing device - Available devices + Available hearing devices Don\u2019t see your hearing device? diff --git a/res/xml/accessibility_hearing_aids.xml b/res/xml/accessibility_hearing_aids.xml index 4d4028ac00b..76910a03d0f 100644 --- a/res/xml/accessibility_hearing_aids.xml +++ b/res/xml/accessibility_hearing_aids.xml @@ -29,8 +29,10 @@ 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:useAdminDisabledSummary="true" + settings:controller="com.android.settings.connecteddevice.AddDevicePreferenceController"/> + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/accessibility/HearingDevicePairingDetail.java b/src/com/android/settings/accessibility/HearingDevicePairingDetail.java new file mode 100644 index 00000000000..aa9b587d818 --- /dev/null +++ b/src/com/android/settings/accessibility/HearingDevicePairingDetail.java @@ -0,0 +1,82 @@ +/* + * 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.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothUuid; +import android.bluetooth.le.ScanFilter; + +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.bluetooth.BluetoothDevicePairingDetailBase; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; + +import java.util.Collections; + +/** + * HearingDevicePairingDetail is a page to scan hearing devices. This page shows scanning icons and + * pairing them. + */ +public class HearingDevicePairingDetail extends BluetoothDevicePairingDetailBase { + + private static final String TAG = "HearingDevicePairingDetail"; + @VisibleForTesting + static final String KEY_AVAILABLE_HEARING_DEVICES = "available_hearing_devices"; + + public HearingDevicePairingDetail() { + super(); + final ScanFilter filter = new ScanFilter.Builder() + .setServiceData(BluetoothUuid.HEARING_AID, new byte[]{0}, new byte[]{0}) + .build(); + setFilter(Collections.singletonList(filter)); + } + + @Override + public void onStart() { + super.onStart(); + mAvailableDevicesCategory.setProgress(mBluetoothAdapter.isEnabled()); + } + + @Override + public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { + super.onDeviceBondStateChanged(cachedDevice, bondState); + + mAvailableDevicesCategory.setProgress(bondState == BluetoothDevice.BOND_NONE); + } + + @Override + public int getMetricsCategory() { + // TODO(b/262839191): To be updated settings_enums.proto + return 0; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.hearing_device_pairing_detail; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public String getDeviceListKey() { + return KEY_AVAILABLE_HEARING_DEVICES; + } +} diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java index 21813099237..522b5cb10b0 100644 --- a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java +++ b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java @@ -18,6 +18,11 @@ package com.android.settings.bluetooth; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +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.os.Bundle; import android.os.SystemProperties; import android.text.BidiFormatter; @@ -33,6 +38,7 @@ import com.android.settings.dashboard.RestrictedDashboardFragment; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.BluetoothDeviceFilter; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothManager; import java.util.ArrayList; @@ -59,37 +65,51 @@ public abstract class DeviceListPreferenceFragment extends "persist.bluetooth.showdeviceswithoutnames"; private BluetoothDeviceFilter.Filter mFilter; + private List mLeScanFilters; + private ScanCallback mScanCallback; @VisibleForTesting - boolean mScanEnabled; + protected boolean mScanEnabled; - BluetoothDevice mSelectedDevice; + protected BluetoothDevice mSelectedDevice; - BluetoothAdapter mBluetoothAdapter; - LocalBluetoothManager mLocalManager; + protected BluetoothAdapter mBluetoothAdapter; + protected LocalBluetoothManager mLocalManager; + protected CachedBluetoothDeviceManager mCachedDeviceManager; @VisibleForTesting - PreferenceGroup mDeviceListGroup; + protected PreferenceGroup mDeviceListGroup; - final HashMap mDevicePreferenceMap = + protected final HashMap mDevicePreferenceMap = new HashMap<>(); - final List mSelectedList = new ArrayList<>(); + protected final List mSelectedList = new ArrayList<>(); - boolean mShowDevicesWithoutNames; + protected boolean mShowDevicesWithoutNames; - DeviceListPreferenceFragment(String restrictedKey) { + public DeviceListPreferenceFragment(String restrictedKey) { super(restrictedKey); mFilter = BluetoothDeviceFilter.ALL_FILTER; } - final void setFilter(BluetoothDeviceFilter.Filter filter) { + protected final void setFilter(BluetoothDeviceFilter.Filter filter) { mFilter = filter; } - final void setFilter(int filterType) { + protected final void setFilter(int filterType) { mFilter = BluetoothDeviceFilter.getFilter(filterType); } + /** + * Sets the bluetooth device scanning filter with {@link ScanFilter}s. It will change to start + * {@link BluetoothLeScanner} which will scan BLE device only. + * + * @param leScanFilters list of settings to filter scan result + */ + protected void setFilter(List leScanFilters) { + mFilter = null; + mLeScanFilters = leScanFilters; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -100,6 +120,7 @@ public abstract class DeviceListPreferenceFragment extends return; } mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mCachedDeviceManager = mLocalManager.getCachedDeviceManager(); mShowDevicesWithoutNames = SystemProperties.getBoolean( BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false); @@ -109,7 +130,7 @@ public abstract class DeviceListPreferenceFragment extends } /** find and update preference that already existed in preference screen */ - abstract void initPreferencesFromPreferenceScreen(); + protected abstract void initPreferencesFromPreferenceScreen(); @Override public void onStart() { @@ -139,7 +160,7 @@ public abstract class DeviceListPreferenceFragment extends void addCachedDevices() { Collection cachedDevices = - mLocalManager.getCachedDeviceManager().getCachedDevicesCopy(); + mCachedDeviceManager.getCachedDevicesCopy(); for (CachedBluetoothDevice cachedDevice : cachedDevices) { onDeviceAdded(cachedDevice); } @@ -164,7 +185,7 @@ public abstract class DeviceListPreferenceFragment extends return super.onPreferenceTreeClick(preference); } - void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { + protected void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { btPreference.onClicked(); } @@ -177,7 +198,8 @@ public abstract class DeviceListPreferenceFragment extends // Prevent updates while the list shows one of the state messages if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) return; - if (mFilter.matches(cachedDevice.getDevice())) { + if (mLeScanFilters != null + || (mFilter != null && mFilter.matches(cachedDevice.getDevice()))) { createDevicePreference(cachedDevice); } } @@ -227,7 +249,7 @@ public abstract class DeviceListPreferenceFragment extends } @VisibleForTesting - void enableScanning() { + protected void enableScanning() { // BluetoothAdapter already handles repeated scan requests if (!mScanEnabled) { startScanning(); @@ -236,7 +258,7 @@ public abstract class DeviceListPreferenceFragment extends } @VisibleForTesting - void disableScanning() { + protected void disableScanning() { if (mScanEnabled) { stopScanning(); mScanEnabled = false; @@ -250,31 +272,6 @@ public abstract class DeviceListPreferenceFragment extends } } - /** - * Add bluetooth device preferences to {@code preferenceGroup} which satisfy the {@code filter} - * - * This method will also (1) set the title for {@code preferenceGroup} and (2) change the - * default preferenceGroup and filter - * @param preferenceGroup - * @param titleId - * @param filter - * @param addCachedDevices - */ - public void addDeviceCategory(PreferenceGroup preferenceGroup, int titleId, - BluetoothDeviceFilter.Filter filter, boolean addCachedDevices) { - cacheRemoveAllPrefs(preferenceGroup); - preferenceGroup.setTitle(titleId); - mDeviceListGroup = preferenceGroup; - if (addCachedDevices) { - // Don't show bonded devices when screen turned back on - setFilter(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER); - addCachedDevices(); - } - setFilter(filter); - preferenceGroup.setEnabled(true); - removeCachedPrefs(preferenceGroup); - } - /** * Return the key of the {@link PreferenceGroup} that contains the bluetooth devices */ @@ -284,15 +281,65 @@ public abstract class DeviceListPreferenceFragment extends return mShowDevicesWithoutNames; } + @VisibleForTesting void startScanning() { + if (mFilter != null) { + startClassicScanning(); + } else if (mLeScanFilters != null) { + startLeScanning(); + } + + } + + @VisibleForTesting + void stopScanning() { + if (mFilter != null) { + stopClassicScanning(); + } else if (mLeScanFilters != null) { + stopLeScanning(); + } + } + + private void startClassicScanning() { if (!mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.startDiscovery(); } } - void stopScanning() { + private void stopClassicScanning() { if (mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.cancelDiscovery(); } } + + private void startLeScanning() { + final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); + final ScanSettings settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .build(); + mScanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + final BluetoothDevice device = result.getDevice(); + CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device); + if (cachedDevice == null) { + cachedDevice = mCachedDeviceManager.addDevice(device); + } + onDeviceAdded(cachedDevice); + } + + @Override + public void onScanFailed(int errorCode) { + Log.w(TAG, "BLE Scan failed with error code " + errorCode); + } + }; + scanner.startScan(mLeScanFilters, settings, mScanCallback); + } + + private void stopLeScanning() { + final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); + if (scanner != null) { + scanner.stopScan(mScanCallback); + } + } } diff --git a/src/com/android/settings/bluetooth/DevicePickerFragment.java b/src/com/android/settings/bluetooth/DevicePickerFragment.java index e8adac01e4f..2e810620e7c 100644 --- a/src/com/android/settings/bluetooth/DevicePickerFragment.java +++ b/src/com/android/settings/bluetooth/DevicePickerFragment.java @@ -27,8 +27,8 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.UserManager; -import android.util.Log; import android.text.TextUtils; +import android.util.Log; import android.view.Menu; import android.view.MenuInflater; @@ -68,7 +68,7 @@ public final class DevicePickerFragment extends DeviceListPreferenceFragment { } @Override - void initPreferencesFromPreferenceScreen() { + public void initPreferencesFromPreferenceScreen() { Intent intent = getActivity().getIntent(); mNeedAuth = intent.getBooleanExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false); setFilter(intent.getIntExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE, @@ -136,7 +136,7 @@ public final class DevicePickerFragment extends DeviceListPreferenceFragment { } @Override - void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { + public void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { disableScanning(); LocalBluetoothPreferences.persistSelectedDeviceInPicker( getActivity(), mSelectedDevice.getAddress()); diff --git a/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingDetailTest.java b/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingDetailTest.java new file mode 100644 index 00000000000..e1651d9d4ed --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/HearingDevicePairingDetailTest.java @@ -0,0 +1,115 @@ +/* + * 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.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.bluetooth.BluetoothProgressCategory; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; + +/** Tests for {@link HearingDevicePairingDetail}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class}) +public class HearingDevicePairingDetailTest { + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + private final Context mContext = ApplicationProvider.getApplicationContext(); + + @Mock + private CachedBluetoothDevice mCachedBluetoothDevice; + private BluetoothProgressCategory mProgressCategory; + private TestHearingDevicePairingDetail mFragment; + + @Before + public void setUp() { + final BluetoothAdapter bluetoothAdapter = spy(BluetoothAdapter.getDefaultAdapter()); + final ShadowBluetoothAdapter shadowBluetoothAdapter = Shadow.extract( + BluetoothAdapter.getDefaultAdapter()); + shadowBluetoothAdapter.setEnabled(true); + + mProgressCategory = spy(new BluetoothProgressCategory(mContext)); + mFragment = spy(new TestHearingDevicePairingDetail()); + when(mFragment.getContext()).thenReturn(mContext); + when(mFragment.findPreference( + HearingDevicePairingDetail.KEY_AVAILABLE_HEARING_DEVICES)).thenReturn( + mProgressCategory); + mFragment.setBluetoothAdapter(bluetoothAdapter); + + } + + @Test + public void getDeviceListKey_expectedKey() { + assertThat(mFragment.getDeviceListKey()).isEqualTo( + HearingDevicePairingDetail.KEY_AVAILABLE_HEARING_DEVICES); + } + + @Test + public void onDeviceBondStateChanged_bondNone_setProgressFalse() { + mFragment.initPreferencesFromPreferenceScreen(); + + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); + + verify(mProgressCategory).setProgress(true); + } + + @Test + public void onDeviceBondStateChanged_bonding_setProgressTrue() { + mFragment.initPreferencesFromPreferenceScreen(); + + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + + verify(mProgressCategory).setProgress(false); + } + + private static class TestHearingDevicePairingDetail extends HearingDevicePairingDetail { + TestHearingDevicePairingDetail() { + super(); + } + + public void setBluetoothAdapter(BluetoothAdapter bluetoothAdapter) { + this.mBluetoothAdapter = bluetoothAdapter; + } + + public void enableScanning() { + super.enableScanning(); + } + } +} diff --git a/tests/robotests/src/com/android/settings/bluetooth/DeviceListPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/bluetooth/DeviceListPreferenceFragmentTest.java index 19de2e4b125..4f46ce93d03 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/DeviceListPreferenceFragmentTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/DeviceListPreferenceFragmentTest.java @@ -26,6 +26,11 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothUuid; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanSettings; import android.content.Context; import android.content.res.Resources; @@ -45,6 +50,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import java.util.Collections; import java.util.List; @RunWith(RobolectricTestRunner.class) @@ -57,10 +63,14 @@ public class DeviceListPreferenceFragmentTest { private Resources mResource; @Mock private Context mContext; + @Mock + private BluetoothLeScanner mBluetoothLeScanner; private TestFragment mFragment; private Preference mMyDevicePreference; + + private BluetoothAdapter mBluetoothAdapter; @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -68,7 +78,8 @@ public class DeviceListPreferenceFragmentTest { mFragment = spy(new TestFragment()); doReturn(mContext).when(mFragment).getContext(); doReturn(mResource).when(mFragment).getResources(); - mFragment.mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mBluetoothAdapter = spy(BluetoothAdapter.getDefaultAdapter()); + mFragment.mBluetoothAdapter = mBluetoothAdapter; mMyDevicePreference = new Preference(RuntimeEnvironment.application); } @@ -169,6 +180,20 @@ public class DeviceListPreferenceFragmentTest { verify(mFragment, times(1)).startScanning(); } + @Test + public void startScanning_setLeScanFilter_shouldStartLeScan() { + final ScanFilter leScanFilter = new ScanFilter.Builder() + .setServiceData(BluetoothUuid.HEARING_AID, new byte[]{0}, new byte[]{0}) + .build(); + doReturn(mBluetoothLeScanner).when(mBluetoothAdapter).getBluetoothLeScanner(); + + mFragment.setFilter(Collections.singletonList(leScanFilter)); + mFragment.startScanning(); + + verify(mBluetoothLeScanner).startScan(eq(Collections.singletonList(leScanFilter)), + any(ScanSettings.class), any(ScanCallback.class)); + } + /** * Fragment to test since {@code DeviceListPreferenceFragment} is abstract */ @@ -187,7 +212,7 @@ public class DeviceListPreferenceFragmentTest { public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {} @Override - void initPreferencesFromPreferenceScreen() {} + protected void initPreferencesFromPreferenceScreen() {} @Override public String getDeviceListKey() {