Implement Hearing Devices Quick Settings Tile (3/n)
This patch provides
* Manager to control when to show pair new device dialog
* Click on pair new device button to launch hearing device pairing page
* Add intent to open hearing device pairing page
Bug: 291423171
Bug: 319197158
Test: atest HearingDevicesDialogDelegateTest HearingDevicesDialogManagerTest
Flag: ACONFIG com.android.systemui.hearing_aids_qs_tile_dialog DEVELOPMENT
Change-Id: I17ab63942d5d98e59a46986137caeb7c0f8c4ed8
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index fca1fa5..573d138 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1261,6 +1261,22 @@
"android.settings.BLUETOOTH_PAIRING_SETTINGS";
/**
+ * Activity Action: Show settings to allow pairing hearing devices.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_HEARING_DEVICE_PAIRING_SETTINGS =
+ "android.settings.HEARING_DEVICES_PAIRING_SETTINGS";
+
+ /**
* Activity Action: Show settings to configure input methods, in particular
* allowing the user to enable input methods.
* <p>
diff --git a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
new file mode 100644
index 0000000..8ceda98
--- /dev/null
+++ b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
@@ -0,0 +1,48 @@
+<!--
+ Copyright (C) 2024 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.
+-->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/root"
+ style="@style/Widget.SliceView.Panel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <Button
+ android:id="@+id/pair_new_device_button"
+ style="@style/BluetoothTileDialog.Device"
+ android:paddingEnd="0dp"
+ android:paddingStart="20dp"
+ android:background="@drawable/bluetooth_tile_dialog_bg_off"
+ android:layout_width="0dp"
+ android:layout_height="64dp"
+ android:contentDescription="@string/accessibility_hearing_device_pair_new_device"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ android:drawableStart="@drawable/ic_add"
+ android:drawablePadding="20dp"
+ android:drawableTint="?android:attr/textColorPrimary"
+ android:text="@string/quick_settings_pair_hearing_devices"
+ android:textSize="14sp"
+ android:textAppearance="@style/TextAppearance.Dialog.Title"
+ android:textDirection="locale"
+ android:textAlignment="viewStart"
+ android:maxLines="1"
+ android:ellipsize="end" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 71679f9..c48aa8b 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -899,8 +899,15 @@
<!-- QuickSettings: Contrast tile description: high [CHAR LIMIT=NONE] -->
<string name="quick_settings_contrast_high">High</string>
+ <!-- Hearing devices -->
<!-- QuickSettings: Hearing devices [CHAR LIMIT=NONE] -->
<string name="quick_settings_hearing_devices_label">Hearing devices</string>
+ <!-- QuickSettings: Quick Settings Hearing devices dialog title [CHAR LIMIT=30] -->
+ <string name="quick_settings_hearing_devices_dialog_title">Hearing devices</string>
+ <!-- QuickSettings: Hearing devices dialog pair new device [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_pair_hearing_devices">Pair new device</string>
+ <!-- QuickSettings: Content description of the hearing devices dialog pair new device [CHAR LIMIT=NONE] -->
+ <string name="accessibility_hearing_device_pair_new_device">Click to pair new device</string>
<!--- Title of dialog triggered if the microphone is disabled but an app tried to access it. [CHAR LIMIT=150] -->
<string name="sensor_privacy_start_use_mic_dialog_title">Unblock device microphone?</string>
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 4733d06..6fbf3dd 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -16,42 +16,106 @@
package com.android.systemui.accessibility.hearingaid;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.view.LayoutInflater;
+import android.view.View.Visibility;
+import android.widget.Button;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
+import dagger.assisted.Assisted;
import dagger.assisted.AssistedFactory;
import dagger.assisted.AssistedInject;
/**
* Dialog for showing hearing devices controls.
*/
-public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate{
+public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate {
private final SystemUIDialog.Factory mSystemUIDialogFactory;
+ private final DialogTransitionAnimator mDialogTransitionAnimator;
+ private final ActivityStarter mActivityStarter;
+ private final boolean mShowPairNewDevice;
private SystemUIDialog mDialog;
+ private Button mPairButton;
/** Factory to create a {@link HearingDevicesDialogDelegate} dialog instance. */
@AssistedFactory
public interface Factory {
/** Create a {@link HearingDevicesDialogDelegate} instance */
- HearingDevicesDialogDelegate create();
+ HearingDevicesDialogDelegate create(
+ boolean showPairNewDevice);
}
@AssistedInject
public HearingDevicesDialogDelegate(
- SystemUIDialog.Factory systemUIDialogFactory) {
+ @Assisted boolean showPairNewDevice,
+ SystemUIDialog.Factory systemUIDialogFactory,
+ ActivityStarter activityStarter,
+ DialogTransitionAnimator dialogTransitionAnimator) {
+ mShowPairNewDevice = showPairNewDevice;
mSystemUIDialogFactory = systemUIDialogFactory;
+ mActivityStarter = activityStarter;
+ mDialogTransitionAnimator = dialogTransitionAnimator;
}
@Override
public SystemUIDialog createDialog() {
SystemUIDialog dialog = mSystemUIDialogFactory.create(this);
-
- if (mDialog != null) {
- mDialog.dismiss();
- }
+ dismissDialogIfExists();
mDialog = dialog;
return dialog;
}
+
+ @Override
+ public void beforeCreate(@NonNull SystemUIDialog dialog, @Nullable Bundle savedInstanceState) {
+ dialog.setTitle(R.string.quick_settings_hearing_devices_dialog_title);
+ dialog.setView(LayoutInflater.from(dialog.getContext()).inflate(
+ R.layout.hearing_devices_tile_dialog, null));
+ dialog.setPositiveButton(
+ R.string.quick_settings_done,
+ /* onClick = */ null,
+ /* dismissOnClick = */ true
+ );
+ }
+
+ @Override
+ public void onCreate(@NonNull SystemUIDialog dialog, @Nullable Bundle savedInstanceState) {
+ mPairButton = dialog.requireViewById(R.id.pair_new_device_button);
+
+ setupPairNewDeviceButton(dialog, mShowPairNewDevice ? VISIBLE : GONE);
+ }
+
+ private void setupPairNewDeviceButton(SystemUIDialog dialog, @Visibility int visibility) {
+ if (visibility == VISIBLE) {
+ mPairButton.setOnClickListener(v -> {
+ dismissDialogIfExists();
+ final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0,
+ mDialogTransitionAnimator.createActivityTransitionController(dialog));
+ });
+ } else {
+ mPairButton.setVisibility(GONE);
+ }
+ }
+
+ private void dismissDialogIfExists() {
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
index c83043e..623b40f 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManager.java
@@ -16,10 +16,14 @@
package com.android.systemui.accessibility.hearingaid;
+import android.bluetooth.BluetoothDevice;
import android.util.Log;
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.android.internal.jank.InteractionJankMonitor;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.dagger.SysUISingleton;
@@ -39,13 +43,16 @@
private SystemUIDialog mDialog;
private final DialogTransitionAnimator mDialogTransitionAnimator;
private final HearingDevicesDialogDelegate.Factory mDialogFactory;
+ private final LocalBluetoothManager mLocalBluetoothManager;
@Inject
public HearingDevicesDialogManager(
DialogTransitionAnimator dialogTransitionAnimator,
- HearingDevicesDialogDelegate.Factory dialogFactory) {
+ HearingDevicesDialogDelegate.Factory dialogFactory,
+ @Nullable LocalBluetoothManager localBluetoothManager) {
mDialogTransitionAnimator = dialogTransitionAnimator;
mDialogFactory = dialogFactory;
+ mLocalBluetoothManager = localBluetoothManager;
}
/**
@@ -60,13 +67,13 @@
}
destroyDialog();
}
- mDialog = mDialogFactory.create().createDialog();
+
+ mDialog = mDialogFactory.create(!isAnyBondedHearingDevice()).createDialog();
if (view != null) {
mDialogTransitionAnimator.showFromView(mDialog, view,
new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG),
- true);
+ INTERACTION_JANK_TAG), /* animateBackgroundBoundsChange= */ true);
} else {
mDialog.show();
}
@@ -76,4 +83,17 @@
mDialog.dismiss();
mDialog = null;
}
+
+ private boolean isAnyBondedHearingDevice() {
+ if (mLocalBluetoothManager == null) {
+ return false;
+ }
+ if (!mLocalBluetoothManager.getBluetoothAdapter().isEnabled()) {
+ return false;
+ }
+
+ return mLocalBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy().stream()
+ .anyMatch(device -> device.isHearingAidDevice()
+ && device.getBondState() != BluetoothDevice.BOND_NONE);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
index 38e3171..46010d5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
@@ -21,16 +21,24 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.Intent;
+import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
+import android.view.View;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.model.SysUiState;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
@@ -38,6 +46,7 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -58,12 +67,46 @@
private SysUiState mSysUiState;
@Mock
private DialogTransitionAnimator mDialogTransitionAnimator;
+ @Mock
+ private ActivityStarter mActivityStarter;
private SystemUIDialog mDialog;
private HearingDevicesDialogDelegate mDialogDelegate;
@Before
public void setUp() {
- mDialogDelegate = new HearingDevicesDialogDelegate(mSystemUIDialogFactory);
+ when(mSysUiState.setFlag(anyInt(), anyBoolean())).thenReturn(mSysUiState);
+
+ setUpPairNewDeviceDialog();
+
+ when(mSystemUIDialogFactory.create(any(SystemUIDialog.Delegate.class)))
+ .thenReturn(mDialog);
+ }
+
+ @Test
+ public void createDialog_dialogShown() {
+ assertThat(mDialogDelegate.createDialog()).isEqualTo(mDialog);
+ }
+
+ @Test
+ public void clickPairNewDeviceButton_intentActionMatch() {
+ mDialog.show();
+
+ getPairNewDeviceButton(mDialog).performClick();
+
+ ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+ verify(mActivityStarter).postStartActivityDismissingKeyguard(intentCaptor.capture(),
+ anyInt(), any());
+ assertThat(intentCaptor.getValue().getAction()).isEqualTo(
+ Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS);
+ }
+
+ private void setUpPairNewDeviceDialog() {
+ mDialogDelegate = new HearingDevicesDialogDelegate(
+ true,
+ mSystemUIDialogFactory,
+ mActivityStarter,
+ mDialogTransitionAnimator
+ );
mDialog = new SystemUIDialog(
mContext,
0,
@@ -74,13 +117,9 @@
mDialogTransitionAnimator,
mDialogDelegate
);
-
- when(mSystemUIDialogFactory.create(any(SystemUIDialog.Delegate.class)))
- .thenReturn(mDialog);
}
- @Test
- public void createDialog_dialogShown() {
- assertThat(mDialogDelegate.createDialog()).isEqualTo(mDialog);
+ private View getPairNewDeviceButton(SystemUIDialog dialog) {
+ return dialog.requireViewById(R.id.pair_new_device_button);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java
new file mode 100644
index 0000000..abc12ed
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogManagerTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 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.systemui.accessibility.hearingaid;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothDevice;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+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 java.util.ArrayList;
+import java.util.List;
+
+/** Tests for {@link HearingDevicesDialogManager}. */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class HearingDevicesDialogManagerTest extends SysuiTestCase {
+
+ @Rule
+ public MockitoRule mockito = MockitoJUnit.rule();
+
+ private final View mView = new View(mContext);
+ private final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<>();
+ @Mock
+ private DialogTransitionAnimator mDialogTransitionAnimator;
+ @Mock
+ private HearingDevicesDialogDelegate.Factory mDialogFactory;
+ @Mock
+ private HearingDevicesDialogDelegate mDialogDelegate;
+ @Mock
+ private SystemUIDialog mDialog;
+ @Mock
+ private LocalBluetoothManager mLocalBluetoothManager;
+ @Mock
+ private LocalBluetoothAdapter mLocalBluetoothAdapter;
+ @Mock
+ private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager;
+ @Mock
+ private CachedBluetoothDevice mCachedDevice;
+
+ private HearingDevicesDialogManager mManager;
+
+ @Before
+ public void setUp() {
+ when(mDialogFactory.create(anyBoolean())).thenReturn(mDialogDelegate);
+ when(mDialogDelegate.createDialog()).thenReturn(mDialog);
+ when(mLocalBluetoothManager.getBluetoothAdapter()).thenReturn(mLocalBluetoothAdapter);
+ when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(
+ mCachedBluetoothDeviceManager);
+ when(mCachedBluetoothDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
+
+ mManager = new HearingDevicesDialogManager(
+ mDialogTransitionAnimator,
+ mDialogFactory,
+ mLocalBluetoothManager
+ );
+ }
+
+ @Test
+ public void showDialog_bluetoothDisable_showPairNewDeviceTrue() {
+ when(mLocalBluetoothAdapter.isEnabled()).thenReturn(false);
+
+ mManager.showDialog(mView);
+
+ verify(mDialogFactory).create(eq(true));
+ }
+
+ @Test
+ public void showDialog_containsHearingAid_showPairNewDeviceFalse() {
+ when(mLocalBluetoothAdapter.isEnabled()).thenReturn(true);
+ when(mCachedDevice.isHearingAidDevice()).thenReturn(true);
+ when(mCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+ mCachedDevices.add(mCachedDevice);
+
+ mManager.showDialog(mView);
+
+ verify(mDialogFactory).create(eq(false));
+ }
+}