[Pair hearing devices] Add "Hearing devices" to show connected hearing devices
Bug: 237625815
Test: make RunSettingsRoboTests ROBOTEST_FILTER=AvailableHearingDeviceUpdaterTest
Change-Id: I15bff230cac29fdbad13d452878bc57b57d9773e
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3dfdc8a..ad68657 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4541,6 +4541,8 @@
<!-- Title for the hearing device pairing preference. [CHAR LIMIT=20] -->
<string name="accessibility_hearing_device_pairing_title">Pair new device</string>
<!-- Title for the preference category containing the connected hearing device group. [CHAR LIMIT=20]-->
+ <string name="accessibility_hearing_device_connected_title">Hearing devices</string>
+ <!-- Title for the preference category containing the previously connected hearing device group. [CHAR LIMIT=20]-->
<string name="accessibility_hearing_device_saved_title">Saved devices</string>
<!-- Title for the preference category containing the controls of the hearing device. [CHAR LIMIT=35] -->
<string name="accessibility_hearing_device_control">Hearing device controls</string>
diff --git a/res/xml/accessibility_hearing_aids.xml b/res/xml/accessibility_hearing_aids.xml
index 5d6cff1..4d4028a 100644
--- a/res/xml/accessibility_hearing_aids.xml
+++ b/res/xml/accessibility_hearing_aids.xml
@@ -19,6 +19,11 @@
android:key="accessibility_hearing_devices_screen"
android:title="@string/accessibility_hearingaid_title">
+ <PreferenceCategory
+ android:key="available_hearing_devices"
+ android:title="@string/accessibility_hearing_device_connected_title"
+ settings:controller="com.android.settings.accessibility.AvailableHearingDevicePreferenceController"/>
+
<com.android.settingslib.RestrictedPreference
android:key="add_bt_devices"
android:title="@string/bluetooth_pairing_pref_title"
diff --git a/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceController.java b/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceController.java
index cec48bb..cd76b47 100644
--- a/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceController.java
+++ b/src/com/android/settings/accessibility/AccessibilityHearingAidPreferenceController.java
@@ -29,7 +29,6 @@
import android.os.Bundle;
import android.text.TextUtils;
import android.util.FeatureFlagUtils;
-import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.FragmentManager;
@@ -55,8 +54,6 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.FutureTask;
import java.util.stream.Collectors;
/**
@@ -84,7 +81,8 @@
public AccessibilityHearingAidPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
- mLocalBluetoothManager = getLocalBluetoothManager();
+ mLocalBluetoothManager = com.android.settings.bluetooth.Utils.getLocalBluetoothManager(
+ context);
mProfileManager = mLocalBluetoothManager.getProfileManager();
mCachedDeviceManager = mLocalBluetoothManager.getCachedDeviceManager();
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
@@ -269,19 +267,6 @@
return false;
}
- private LocalBluetoothManager getLocalBluetoothManager() {
- final FutureTask<LocalBluetoothManager> localBtManagerFutureTask = new FutureTask<>(
- // Avoid StrictMode ThreadPolicy violation
- () -> com.android.settings.bluetooth.Utils.getLocalBtManager(mContext));
- try {
- localBtManagerFutureTask.run();
- return localBtManagerFutureTask.get();
- } catch (InterruptedException | ExecutionException e) {
- Log.w(TAG, "Error getting LocalBluetoothManager.", e);
- return null;
- }
- }
-
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
void setPreference(Preference preference) {
mHearingAidPreference = preference;
diff --git a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java
index 519b751..85783b73 100644
--- a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java
+++ b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java
@@ -48,6 +48,7 @@
@Override
public void onAttach(Context context) {
super.onAttach(context);
+ use(AvailableHearingDevicePreferenceController.class).init(this);
use(SavedHearingDevicePreferenceController.class).init(this);
}
diff --git a/src/com/android/settings/accessibility/AvailableHearingDevicePreferenceController.java b/src/com/android/settings/accessibility/AvailableHearingDevicePreferenceController.java
new file mode 100644
index 0000000..076432c
--- /dev/null
+++ b/src/com/android/settings/accessibility/AvailableHearingDevicePreferenceController.java
@@ -0,0 +1,109 @@
+/*
+ * 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.BluetoothProfile;
+import android.content.Context;
+
+import androidx.fragment.app.FragmentManager;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.bluetooth.BluetoothDeviceUpdater;
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnStart;
+import com.android.settingslib.core.lifecycle.events.OnStop;
+
+/**
+ * Controller to update the {@link androidx.preference.PreferenceCategory} for all
+ * connected hearing devices, including ASHA and HAP profile.
+ * Parent class {@link BaseBluetoothDevicePreferenceController} will use
+ * {@link DevicePreferenceCallback} to add/remove {@link Preference}.
+ */
+public class AvailableHearingDevicePreferenceController extends
+ BaseBluetoothDevicePreferenceController implements LifecycleObserver, OnStart, OnStop,
+ BluetoothCallback {
+
+ private static final String TAG = "AvailableHearingDevicePreferenceController";
+
+ private BluetoothDeviceUpdater mAvailableHearingDeviceUpdater;
+ private final LocalBluetoothManager mLocalBluetoothManager;
+ private FragmentManager mFragmentManager;
+
+ public AvailableHearingDevicePreferenceController(Context context,
+ String preferenceKey) {
+ super(context, preferenceKey);
+ mLocalBluetoothManager = com.android.settings.bluetooth.Utils.getLocalBluetoothManager(
+ context);
+ }
+
+ /**
+ * Initializes objects in this controller. Need to call this before onStart().
+ *
+ * <p>Should not call this more than 1 time.
+ *
+ * @param fragment The {@link DashboardFragment} uses the controller.
+ */
+ public void init(DashboardFragment fragment) {
+ if (mAvailableHearingDeviceUpdater != null) {
+ throw new IllegalStateException("Should not call init() more than 1 time.");
+ }
+ mAvailableHearingDeviceUpdater = new AvailableHearingDeviceUpdater(fragment.getContext(),
+ this, fragment.getMetricsCategory());
+ mFragmentManager = fragment.getParentFragmentManager();
+ }
+
+ @Override
+ public void onStart() {
+ mAvailableHearingDeviceUpdater.registerCallback();
+ mAvailableHearingDeviceUpdater.refreshPreference();
+ mLocalBluetoothManager.getEventManager().registerCallback(this);
+ }
+
+ @Override
+ public void onStop() {
+ mAvailableHearingDeviceUpdater.unregisterCallback();
+ mLocalBluetoothManager.getEventManager().unregisterCallback(this);
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+
+ if (isAvailable()) {
+ final Context context = screen.getContext();
+ mAvailableHearingDeviceUpdater.setPrefContext(context);
+ mAvailableHearingDeviceUpdater.forceUpdate();
+ }
+ }
+
+ @Override
+ public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {
+ if (activeDevice == null) {
+ return;
+ }
+
+ if (bluetoothProfile == BluetoothProfile.HEARING_AID) {
+ HearingAidUtils.launchHearingAidPairingDialog(mFragmentManager, activeDevice);
+ }
+ }
+}
diff --git a/src/com/android/settings/accessibility/AvailableHearingDeviceUpdater.java b/src/com/android/settings/accessibility/AvailableHearingDeviceUpdater.java
new file mode 100644
index 0000000..b3d3715
--- /dev/null
+++ b/src/com/android/settings/accessibility/AvailableHearingDeviceUpdater.java
@@ -0,0 +1,51 @@
+/*
+ * 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.content.Context;
+
+import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+
+/**
+ * Maintains and updates connected hearing devices, including ASHA and HAP profile.
+ */
+public class AvailableHearingDeviceUpdater extends AvailableMediaBluetoothDeviceUpdater {
+
+ private static final String PREF_KEY = "connected_hearing_device";
+
+ public AvailableHearingDeviceUpdater(Context context,
+ DevicePreferenceCallback devicePreferenceCallback, int metricsCategory) {
+ super(context, devicePreferenceCallback, metricsCategory);
+ }
+
+ @Override
+ public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) {
+ final BluetoothDevice device = cachedDevice.getDevice();
+ final boolean isConnectedHearingAidDevice = (cachedDevice.isConnectedHearingAidDevice()
+ && (device.getBondState() == BluetoothDevice.BOND_BONDED));
+
+ return isConnectedHearingAidDevice && isDeviceInCachedDevicesList(cachedDevice);
+ }
+
+ @Override
+ protected String getPreferenceKey() {
+ return PREF_KEY;
+ }
+}
diff --git a/src/com/android/settings/bluetooth/Utils.java b/src/com/android/settings/bluetooth/Utils.java
index 9aa363a..5abc72b 100644
--- a/src/com/android/settings/bluetooth/Utils.java
+++ b/src/com/android/settings/bluetooth/Utils.java
@@ -46,6 +46,9 @@
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothManager.BluetoothManagerCallback;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
/**
* Utils is a helper class that contains constants for various
* Android resource IDs, debug logging flags, and static methods
@@ -136,6 +139,24 @@
return LocalBluetoothManager.getInstance(context, mOnInitCallback);
}
+ /**
+ * Obtains a {@link LocalBluetoothManager}.
+ *
+ * To avoid StrictMode ThreadPolicy violation, will get it in another thread.
+ */
+ public static LocalBluetoothManager getLocalBluetoothManager(Context context) {
+ final FutureTask<LocalBluetoothManager> localBtManagerFutureTask = new FutureTask<>(
+ // Avoid StrictMode ThreadPolicy violation
+ () -> getLocalBtManager(context));
+ try {
+ localBtManagerFutureTask.run();
+ return localBtManagerFutureTask.get();
+ } catch (InterruptedException | ExecutionException e) {
+ Log.w(TAG, "Error getting LocalBluetoothManager.", e);
+ return null;
+ }
+ }
+
public static String createRemoteName(Context context, BluetoothDevice device) {
String mRemoteName = device != null ? device.getAlias() : null;
diff --git a/tests/robotests/src/com/android/settings/accessibility/AvailableHearingDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/accessibility/AvailableHearingDeviceUpdaterTest.java
new file mode 100644
index 0000000..6305014
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/accessibility/AvailableHearingDeviceUpdaterTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.when;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+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.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Tests for {@link AvailableHearingDeviceUpdater}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothUtils.class})
+public class AvailableHearingDeviceUpdaterTest {
+ @Rule
+ public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+
+ @Mock
+ private DevicePreferenceCallback mDevicePreferenceCallback;
+ @Mock
+ private CachedBluetoothDeviceManager mCachedDeviceManager;
+ @Mock
+ private LocalBluetoothManager mLocalBluetoothManager;
+ @Mock
+ private CachedBluetoothDevice mCachedBluetoothDevice;
+ @Mock
+ private BluetoothDevice mBluetoothDevice;
+ private AvailableHearingDeviceUpdater mUpdater;
+
+ @Before
+ public void setUp() {
+ ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
+ mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+ when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+ mUpdater = new AvailableHearingDeviceUpdater(mContext,
+ mDevicePreferenceCallback, /* metricsCategory= */ 0);
+ }
+
+ @Test
+ public void isFilterMatch_connectedHearingDevice_returnTrue() {
+ CachedBluetoothDevice connectedHearingDevice = mCachedBluetoothDevice;
+ when(connectedHearingDevice.isConnectedHearingAidDevice()).thenReturn(true);
+ doReturn(BluetoothDevice.BOND_BONDED).when(mBluetoothDevice).getBondState();
+ when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(
+ new ArrayList<>(List.of(connectedHearingDevice)));
+
+ assertThat(mUpdater.isFilterMatched(connectedHearingDevice)).isEqualTo(true);
+ }
+
+ @Test
+ public void isFilterMatch_nonConnectedHearingDevice_returnFalse() {
+ CachedBluetoothDevice nonConnectedHearingDevice = mCachedBluetoothDevice;
+ when(nonConnectedHearingDevice.isConnectedHearingAidDevice()).thenReturn(false);
+ doReturn(BluetoothDevice.BOND_BONDED).when(mBluetoothDevice).getBondState();
+ when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(
+ new ArrayList<>(List.of(nonConnectedHearingDevice)));
+
+ assertThat(mUpdater.isFilterMatched(nonConnectedHearingDevice)).isEqualTo(false);
+ }
+
+ @Test
+ public void isFilterMatch_connectedBondingHearingDevice_returnFalse() {
+ CachedBluetoothDevice connectedBondingHearingDevice = mCachedBluetoothDevice;
+ when(connectedBondingHearingDevice.isHearingAidDevice()).thenReturn(true);
+ doReturn(BluetoothDevice.BOND_BONDING).when(mBluetoothDevice).getBondState();
+ when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(
+ new ArrayList<>(List.of(connectedBondingHearingDevice)));
+
+ assertThat(mUpdater.isFilterMatched(connectedBondingHearingDevice)).isEqualTo(false);
+ }
+
+ @Test
+ public void isFilterMatch_hearingDeviceNotInCachedDevicesList_returnFalse() {
+ CachedBluetoothDevice notInCachedDevicesListDevice = mCachedBluetoothDevice;
+ when(notInCachedDevicesListDevice.isHearingAidDevice()).thenReturn(true);
+ doReturn(BluetoothDevice.BOND_BONDED).when(mBluetoothDevice).getBondState();
+ doReturn(false).when(mBluetoothDevice).isConnected();
+ when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(new ArrayList<>());
+
+ assertThat(mUpdater.isFilterMatched(notInCachedDevicesListDevice)).isEqualTo(false);
+ }
+}