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));
+    }
+}