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 519b751421b..85783b73a77 100644
--- a/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java
+++ b/src/com/android/settings/accessibility/AccessibilityHearingAidsFragment.java
@@ -48,6 +48,7 @@ public class AccessibilityHearingAidsFragment extends AccessibilityShortcutPrefe
@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 00000000000..076432c9b57
--- /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().
+ *
+ * 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 00000000000..b3d371528f6
--- /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 9aa363a1ef5..5abc72bfc95 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.BluetoothUtils.ErrorListener;
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 @@ public final class Utils {
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 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 00000000000..6305014a6e3
--- /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);
+ }
+}