diff options
7 files changed, 327 insertions, 17 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index d91b051c671a..7b3dee7ea8e7 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -1266,6 +1266,22 @@ public final class Settings { "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 000000000000..8ceda9851314 --- /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 b0d98e7c09d0..71353b6774af 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 4733d06234f7..6fbf3dd0d63f 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); + 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(); } - mDialog = dialog; - - return dialog; } } 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 c83043e77b0e..623b40f144eb 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 @@ public class HearingDevicesDialogManager { 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 @@ public class HearingDevicesDialogManager { } 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 @@ public class HearingDevicesDialogManager { 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 38e31713e20c..46010d5f8c73 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.android.systemui.statusbar.phone.SystemUIDialog.DEFAULT_DISMIS 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.Before; 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 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { 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 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { 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 000000000000..abc12ede0a67 --- /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)); + } +} |