From 23417c3aca92d131e9aea82131fe3ec7f202df14 Mon Sep 17 00:00:00 2001 From: jasonwshsu Date: Wed, 4 Jan 2023 21:38:01 +0800 Subject: [PATCH] [Pair hearing devices] Add "Hearing devices" to show connected hearing devices Bug: 237625815 Test: make RunSettingsRoboTests ROBOTEST_FILTER=AvailableHearingDeviceUpdaterTest Change-Id: I15bff230cac29fdbad13d452878bc57b57d9773e --- res/values/strings.xml | 2 + res/xml/accessibility_hearing_aids.xml | 5 + ...ibilityHearingAidPreferenceController.java | 19 +-- .../AccessibilityHearingAidsFragment.java | 1 + ...ableHearingDevicePreferenceController.java | 109 ++++++++++++++++ .../AvailableHearingDeviceUpdater.java | 51 ++++++++ src/com/android/settings/bluetooth/Utils.java | 21 +++ .../AvailableHearingDeviceUpdaterTest.java | 123 ++++++++++++++++++ 8 files changed, 314 insertions(+), 17 deletions(-) create mode 100644 src/com/android/settings/accessibility/AvailableHearingDevicePreferenceController.java create mode 100644 src/com/android/settings/accessibility/AvailableHearingDeviceUpdater.java create mode 100644 tests/robotests/src/com/android/settings/accessibility/AvailableHearingDeviceUpdaterTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 3dfdc8ab423..ad6865758c9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4541,6 +4541,8 @@ Pair new device + Hearing devices + Saved devices Hearing device controls diff --git a/res/xml/accessibility_hearing_aids.xml b/res/xml/accessibility_hearing_aids.xml index 5d6cff14ca0..4d4028ac00b 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"> + + 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); + } +}