Add Hearing devices related tools
* Use config_quickSettingsHearingDevicesRelatedToolName and config_quickSettingsHearingDevicesRelatedToolIcon to control what tools need to be shown here.
* If tool icon is not provided, will use icon in PackageManager instead.
Bug: 341648471
Test: atest HearingDevicesToolItemParserTest HearingDevicesDialogDelegateTest HearingDevicesToolItemListTest
Flag: com.android.systemui.Flags.hearing_devices_dialog_related_tools
Change-Id: I0827e38f858da2ad657c2840d0f017a3db29a29c
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index 55edff6..d201071 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -81,3 +81,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "hearing_devices_dialog_related_tools"
+ namespace: "accessibility"
+ description: "Shows the related tools for hearing devices dialog."
+ bug: "341648471"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml b/packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml
new file mode 100644
index 0000000..627b92b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@android:color/transparent"/>
+ <corners android:radius="@dimen/hearing_devices_preset_spinner_background_radius"/>
+ <stroke
+ android:width="1dp"
+ android:color="?androidprv:attr/textColorTertiary" />
+ </shape>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
index 2bf6f80..4a7bef9 100644
--- a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
@@ -36,9 +36,8 @@
style="@style/BluetoothTileDialog.Device"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:minHeight="@dimen/hearing_devices_preset_spinner_height"
android:layout_marginTop="@dimen/hearing_devices_layout_margin"
- android:layout_marginBottom="@dimen/hearing_devices_layout_margin"
+ android:minHeight="@dimen/hearing_devices_preset_spinner_height"
android:gravity="center_vertical"
android:background="@drawable/hearing_devices_preset_spinner_background"
android:popupBackground="@drawable/hearing_devices_preset_spinner_popup_background"
@@ -54,9 +53,10 @@
android:visibility="gone"/>
<androidx.constraintlayout.widget.Barrier
- android:id="@+id/device_barrier"
+ android:id="@+id/device_function_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ app:barrierAllowsGoneWidgets="false"
app:barrierDirection="bottom"
app:constraint_referenced_ids="device_list,preset_spinner" />
@@ -71,7 +71,8 @@
android:contentDescription="@string/accessibility_hearing_device_pair_new_device"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@id/device_barrier"
+ app:layout_constraintTop_toBottomOf="@id/device_function_barrier"
+ app:layout_constraintBottom_toTopOf="@id/related_tools_scroll"
android:drawableStart="@drawable/ic_add"
android:drawablePadding="20dp"
android:drawableTint="?android:attr/textColorPrimary"
@@ -83,4 +84,32 @@
android:maxLines="1"
android:ellipsize="end" />
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/device_barrier"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:barrierAllowsGoneWidgets="false"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="device_function_barrier, pair_new_device_button" />
+
+ <HorizontalScrollView
+ android:id="@+id/related_tools_scroll"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/bluetooth_dialog_layout_margin"
+ android:layout_marginEnd="@dimen/bluetooth_dialog_layout_margin"
+ android:layout_marginTop="@dimen/hearing_devices_layout_margin"
+ android:scrollbars="none"
+ android:fillViewport="true"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/preset_spinner">
+ <LinearLayout
+ android:id="@+id/related_tools_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+ </LinearLayout>
+ </HorizontalScrollView>
+
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/hearing_tool_item.xml b/packages/SystemUI/res/layout/hearing_tool_item.xml
new file mode 100644
index 0000000..84462d0
--- /dev/null
+++ b/packages/SystemUI/res/layout/hearing_tool_item.xml
@@ -0,0 +1,53 @@
+<!--
+ 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/tool_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="center"
+ android:focusable="true"
+ android:clickable="true"
+ android:layout_weight="1">
+ <FrameLayout
+ android:id="@+id/icon_frame"
+ android:layout_width="@dimen/hearing_devices_tool_icon_frame_width"
+ android:layout_height="@dimen/hearing_devices_tool_icon_frame_height"
+ android:background="@drawable/qs_hearing_devices_related_tools_background"
+ android:focusable="false" >
+ <ImageView
+ android:id="@+id/tool_icon"
+ android:layout_width="@dimen/hearing_devices_tool_icon_size"
+ android:layout_height="@dimen/hearing_devices_tool_icon_size"
+ android:layout_gravity="center"
+ android:scaleType="fitCenter"
+ android:focusable="false" />
+ </FrameLayout>
+ <TextView
+ android:id="@+id/tool_name"
+ android:textDirection="locale"
+ android:textAlignment="center"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/hearing_devices_layout_margin"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textSize="12sp"
+ android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+ android:focusable="false" />
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 638785402..fb88364 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -125,6 +125,20 @@
<!-- Use collapsed layout for media player in landscape QQS -->
<bool name="config_quickSettingsMediaLandscapeCollapsed">true</bool>
+ <!-- For hearing devices related tool list. Need to be in ComponentName format (package/class).
+ Should be activity to be launched.
+ Already contains tool that holds intent: "com.android.settings.action.live_caption".
+ Maximum number is 3. -->
+ <string-array name="config_quickSettingsHearingDevicesRelatedToolName" translatable="false">
+ </string-array>
+
+ <!-- The drawable resource names. If provided, it will replace the corresponding icons in
+ config_quickSettingsHearingDevicesRelatedToolName. Can be empty to use original icons.
+ Already contains tool that holds intent: "com.android.settings.action.live_caption".
+ Maximum number is 3. -->
+ <string-array name="config_quickSettingsHearingDevicesRelatedToolIcon" translatable="false">
+ </string-array>
+
<!-- Show indicator for Wifi on but not connected. -->
<bool name="config_showWifiIndicatorWhenEnabled">false</bool>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 7d7a5d4..edd3d77 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1778,6 +1778,9 @@
<dimen name="hearing_devices_preset_spinner_text_padding_vertical">15dp</dimen>
<dimen name="hearing_devices_preset_spinner_arrow_size">24dp</dimen>
<dimen name="hearing_devices_preset_spinner_background_radius">28dp</dimen>
+ <dimen name="hearing_devices_tool_icon_frame_width">94dp</dimen>
+ <dimen name="hearing_devices_tool_icon_frame_height">64dp</dimen>
+ <dimen name="hearing_devices_tool_icon_size">28dp</dimen>
<!-- Height percentage of the parent container occupied by the communal view -->
<item name="communal_source_height_percentage" format="float" type="dimen">0.80</item>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 6f2806d..c038a82 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -917,6 +917,8 @@
<string name="hearing_devices_presets_error">Couldn\'t update preset</string>
<!-- QuickSettings: Title for hearing aids presets. Preset is a set of hearing aid settings. User can apply different settings in different environments (e.g. Outdoor, Restaurant, Home) [CHAR LIMIT=40]-->
<string name="hearing_devices_preset_label">Preset</string>
+ <!-- QuickSettings: Tool name for hearing devices dialog related tools [CHAR LIMIT=40]-->
+ <string name="live_caption_title">Live Caption</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 28dd233..961d6aa 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -25,10 +25,14 @@
import android.bluetooth.BluetoothHapPresetInfo;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
import android.media.AudioManager;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.Visibility;
@@ -36,7 +40,10 @@
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
import android.widget.Spinner;
+import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -69,6 +76,7 @@
import dagger.assisted.AssistedFactory;
import dagger.assisted.AssistedInject;
+import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -78,12 +86,15 @@
*/
public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
HearingDeviceItemCallback, BluetoothCallback {
-
+ private static final String TAG = "HearingDevicesDialogDelegate";
@VisibleForTesting
static final String ACTION_BLUETOOTH_DEVICE_DETAILS =
"com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS";
private static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args";
private static final String KEY_BLUETOOTH_ADDRESS = "device_address";
+ @VisibleForTesting
+ static final Intent LIVE_CAPTION_INTENT = new Intent(
+ "com.android.settings.action.live_caption");
private final SystemUIDialog.Factory mSystemUIDialogFactory;
private final DialogTransitionAnimator mDialogTransitionAnimator;
private final ActivityStarter mActivityStarter;
@@ -102,6 +113,7 @@
private Spinner mPresetSpinner;
private ArrayAdapter<String> mPresetInfoAdapter;
private Button mPairButton;
+ private LinearLayout mRelatedToolsContainer;
private final HearingDevicesPresetsController.PresetCallback mPresetCallback =
new HearingDevicesPresetsController.PresetCallback() {
@Override
@@ -253,10 +265,14 @@
mPairButton = dialog.requireViewById(R.id.pair_new_device_button);
mDeviceList = dialog.requireViewById(R.id.device_list);
mPresetSpinner = dialog.requireViewById(R.id.preset_spinner);
+ mRelatedToolsContainer = dialog.requireViewById(R.id.related_tools_container);
setupDeviceListView(dialog);
setupPresetSpinner(dialog);
setupPairNewDeviceButton(dialog, mShowPairNewDevice ? VISIBLE : GONE);
+ if (com.android.systemui.Flags.hearingDevicesDialogRelatedTools()) {
+ setupRelatedToolsView(dialog);
+ }
}
@Override
@@ -351,6 +367,34 @@
}
}
+ private void setupRelatedToolsView(SystemUIDialog dialog) {
+ final Context context = dialog.getContext();
+ final List<ToolItem> toolItemList = new ArrayList<>();
+ final String[] toolNameArray;
+ final String[] toolIconArray;
+
+ ToolItem preInstalledItem = getLiveCaption(context);
+ if (preInstalledItem != null) {
+ toolItemList.add(preInstalledItem);
+ }
+ try {
+ toolNameArray = context.getResources().getStringArray(
+ R.array.config_quickSettingsHearingDevicesRelatedToolName);
+ toolIconArray = context.getResources().getStringArray(
+ R.array.config_quickSettingsHearingDevicesRelatedToolIcon);
+ toolItemList.addAll(
+ HearingDevicesToolItemParser.parseStringArray(context, toolNameArray,
+ toolIconArray));
+ } catch (Resources.NotFoundException e) {
+ Log.i(TAG, "No hearing devices related tool config resource");
+ }
+ final int listSize = toolItemList.size();
+ for (int i = 0; i < listSize; i++) {
+ View view = createHearingToolView(context, toolItemList.get(i));
+ mRelatedToolsContainer.addView(view);
+ }
+ }
+
private void refreshPresetInfoAdapter(List<BluetoothHapPresetInfo> presetInfos,
int activePresetIndex) {
mPresetInfoAdapter.clear();
@@ -400,6 +444,40 @@
return null;
}
+ @NonNull
+ private View createHearingToolView(Context context, ToolItem item) {
+ View view = LayoutInflater.from(context).inflate(R.layout.hearing_tool_item,
+ mRelatedToolsContainer, false);
+ ImageView icon = view.requireViewById(R.id.tool_icon);
+ TextView text = view.requireViewById(R.id.tool_name);
+ view.setContentDescription(item.getToolName());
+ icon.setImageDrawable(item.getToolIcon());
+ text.setText(item.getToolName());
+ Intent intent = item.getToolIntent();
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ view.setOnClickListener(
+ v -> {
+ dismissDialogIfExists();
+ mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0,
+ mDialogTransitionAnimator.createActivityTransitionController(view));
+ });
+ return view;
+ }
+
+ private ToolItem getLiveCaption(Context context) {
+ final PackageManager packageManager = context.getPackageManager();
+ LIVE_CAPTION_INTENT.setPackage(packageManager.getSystemCaptionsServicePackageName());
+ final List<ResolveInfo> resolved = packageManager.queryIntentActivities(LIVE_CAPTION_INTENT,
+ /* flags= */ 0);
+ if (!resolved.isEmpty()) {
+ return new ToolItem(context.getString(R.string.live_caption_title),
+ context.getDrawable(R.drawable.ic_volume_odi_captions),
+ LIVE_CAPTION_INTENT);
+ }
+
+ return null;
+ }
+
private void dismissDialogIfExists() {
if (mDialog != null) {
mDialog.dismiss();
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java
new file mode 100644
index 0000000..2006726
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java
@@ -0,0 +1,129 @@
+/*
+ * 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 android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility class for managing and parsing tool items related to hearing devices.
+ */
+public class HearingDevicesToolItemParser {
+ private static final String TAG = "HearingDevicesToolItemParser";
+ private static final String SPLIT_DELIMITER = "/";
+ private static final String RES_TYPE = "drawable";
+ @VisibleForTesting
+ static final int MAX_NUM = 3;
+
+ /**
+ * Parses the string arrays to create a list of {@link ToolItem}.
+ *
+ * This method validates the structure of {@code toolNameArray} and {@code toolIconArray}.
+ * If {@code toolIconArray} is empty or mismatched in length with {@code toolNameArray}, the
+ * icon from {@link ActivityInfo#loadIcon(PackageManager)} will be used instead.
+ *
+ * @param context A valid context.
+ * @param toolNameArray An array of tool names in the format of {@link ComponentName}.
+ * @param toolIconArray An optional array of resource names for tool icons (can be empty).
+ * @return A list of {@link ToolItem} or an empty list if there are errors during parsing.
+ */
+ public static ImmutableList<ToolItem> parseStringArray(Context context, String[] toolNameArray,
+ String[] toolIconArray) {
+ if (toolNameArray.length == 0) {
+ Log.i(TAG, "Empty hearing device related tool name in array.");
+ return ImmutableList.of();
+ }
+ // For the performance concern, especially `getIdentifier` in `parseValidIcon`, we will
+ // limit the maximum number.
+ String[] nameArrayCpy = Arrays.copyOfRange(toolNameArray, 0,
+ Math.min(toolNameArray.length, MAX_NUM));
+ String[] iconArrayCpy = Arrays.copyOfRange(toolIconArray, 0,
+ Math.min(toolIconArray.length, MAX_NUM));
+
+ final PackageManager packageManager = context.getPackageManager();
+ final ImmutableList.Builder<ToolItem> toolItemList = ImmutableList.builder();
+ final List<ActivityInfo> activityInfoList = parseValidActivityInfo(context, nameArrayCpy);
+ final List<Drawable> iconList = parseValidIcon(context, iconArrayCpy);
+ final int size = activityInfoList.size();
+ // Only use custom icon if provided icon's list size is equal to provided name's list size.
+ final boolean useCustomIcons = (size == iconList.size());
+
+ for (int i = 0; i < size; i++) {
+ toolItemList.add(new ToolItem(
+ activityInfoList.get(i).loadLabel(packageManager).toString(),
+ useCustomIcons ? iconList.get(i)
+ : activityInfoList.get(i).loadIcon(packageManager),
+ new Intent(Intent.ACTION_MAIN).setComponent(
+ activityInfoList.get(i).getComponentName())
+ ));
+ }
+
+ return toolItemList.build();
+ }
+
+ private static List<ActivityInfo> parseValidActivityInfo(Context context,
+ String[] toolNameArray) {
+ final PackageManager packageManager = context.getPackageManager();
+ final List<ActivityInfo> activityInfoList = new ArrayList<>();
+ for (String toolName : toolNameArray) {
+ String[] nameParts = toolName.split(SPLIT_DELIMITER);
+ if (nameParts.length == 2) {
+ ComponentName componentName = ComponentName.unflattenFromString(toolName);
+ try {
+ ActivityInfo activityInfo = packageManager.getActivityInfo(
+ componentName, /* flags= */ 0);
+ activityInfoList.add(activityInfo);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Unable to find hearing device related tool: "
+ + componentName.flattenToString());
+ }
+ } else {
+ Log.e(TAG, "Malformed hearing device related tool name item in array: "
+ + toolName);
+ }
+ }
+ return activityInfoList;
+ }
+
+ private static List<Drawable> parseValidIcon(Context context, String[] toolIconArray) {
+ final List<Drawable> drawableList = new ArrayList<>();
+ for (String icon : toolIconArray) {
+ int resId = context.getResources().getIdentifier(icon, RES_TYPE,
+ context.getPackageName());
+ try {
+ drawableList.add(context.getDrawable(resId));
+ } catch (Resources.NotFoundException e) {
+ Log.e(TAG, "Resource does not exist: " + icon);
+ }
+ }
+ return drawableList;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt
new file mode 100644
index 0000000..66bb2b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt
@@ -0,0 +1,26 @@
+/*
+ * 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 android.content.Intent
+import android.graphics.drawable.Drawable
+
+data class ToolItem(
+ var toolName: String = "",
+ var toolIcon: Drawable,
+ var toolIntent: Intent,
+)
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 8895a5e..0db0de2 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
@@ -16,6 +16,7 @@
package com.android.systemui.accessibility.hearingaid;
+import static com.android.systemui.accessibility.hearingaid.HearingDevicesDialogDelegate.LIVE_CAPTION_INTENT;
import static com.android.systemui.statusbar.phone.SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK;
import static com.google.common.truth.Truth.assertThat;
@@ -24,16 +25,24 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.ComponentName;
import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.os.Handler;
+import android.platform.test.annotations.EnableFlags;
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
+import android.widget.LinearLayout;
import androidx.test.filters.SmallTest;
@@ -44,6 +53,7 @@
import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.bluetooth.qsdialog.DeviceItem;
@@ -54,6 +64,7 @@
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -75,6 +86,10 @@
public MockitoRule mockito = MockitoJUnit.rule();
private static final String DEVICE_ADDRESS = "AA:BB:CC:DD:EE:FF";
+ private static final String TEST_PKG = "pkg";
+ private static final String TEST_CLS = "cls";
+ private static final ComponentName TEST_COMPONENT = new ComponentName(TEST_PKG, TEST_CLS);
+ private static final String TEST_LABEL = "label";
@Mock
private SystemUIDialog.Factory mSystemUIDialogFactory;
@@ -104,6 +119,12 @@
private CachedBluetoothDevice mCachedDevice;
@Mock
private DeviceItem mHearingDeviceItem;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private ActivityInfo mActivityInfo;
+ @Mock
+ private Drawable mDrawable;
private SystemUIDialog mDialog;
private HearingDevicesDialogDelegate mDialogDelegate;
private TestableLooper mTestableLooper;
@@ -122,6 +143,7 @@
when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState);
when(mCachedDevice.getAddress()).thenReturn(DEVICE_ADDRESS);
when(mHearingDeviceItem.getCachedBluetoothDevice()).thenReturn(mCachedDevice);
+ mContext.setMockPackageManager(mPackageManager);
setUpPairNewDeviceDialog();
@@ -170,6 +192,45 @@
verify(mCachedDevice).disconnect();
}
+ @Test
+ @EnableFlags(Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS)
+ public void showDialog_hasLiveCaption_noRelatedToolsInConfig_showOneRelatedTool() {
+ when(mPackageManager.queryIntentActivities(
+ eq(LIVE_CAPTION_INTENT), anyInt())).thenReturn(
+ List.of(new ResolveInfo()));
+ mContext.getOrCreateTestableResources().addOverride(
+ R.array.config_quickSettingsHearingDevicesRelatedToolName, new String[]{});
+
+ setUpPairNewDeviceDialog();
+ mDialog.show();
+
+ LinearLayout relatedToolsView = (LinearLayout) getRelatedToolsView(mDialog);
+ assertThat(relatedToolsView.getChildCount()).isEqualTo(1);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_HEARING_DEVICES_DIALOG_RELATED_TOOLS)
+ public void showDialog_hasLiveCaption_oneRelatedToolInConfig_showTwoRelatedTools()
+ throws PackageManager.NameNotFoundException {
+ when(mPackageManager.queryIntentActivities(
+ eq(LIVE_CAPTION_INTENT), anyInt())).thenReturn(
+ List.of(new ResolveInfo()));
+ mContext.getOrCreateTestableResources().addOverride(
+ R.array.config_quickSettingsHearingDevicesRelatedToolName,
+ new String[]{TEST_PKG + "/" + TEST_CLS});
+ when(mPackageManager.getActivityInfo(eq(TEST_COMPONENT), anyInt())).thenReturn(
+ mActivityInfo);
+ when(mActivityInfo.loadLabel(mPackageManager)).thenReturn(TEST_LABEL);
+ when(mActivityInfo.loadIcon(mPackageManager)).thenReturn(mDrawable);
+ when(mActivityInfo.getComponentName()).thenReturn(TEST_COMPONENT);
+
+ setUpPairNewDeviceDialog();
+ mDialog.show();
+
+ LinearLayout relatedToolsView = (LinearLayout) getRelatedToolsView(mDialog);
+ assertThat(relatedToolsView.getChildCount()).isEqualTo(2);
+ }
+
private void setUpPairNewDeviceDialog() {
mDialogDelegate = new HearingDevicesDialogDelegate(
mContext,
@@ -219,4 +280,18 @@
private View getPairNewDeviceButton(SystemUIDialog dialog) {
return dialog.requireViewById(R.id.pair_new_device_button);
}
+
+ private View getRelatedToolsView(SystemUIDialog dialog) {
+ return dialog.requireViewById(R.id.related_tools_container);
+ }
+
+ @After
+ public void reset() {
+ if (mDialogDelegate != null) {
+ mDialogDelegate = null;
+ }
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java
new file mode 100644
index 0000000..7172923
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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 com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import static java.util.Collections.emptyList;
+
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+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.List;
+
+/**
+ * Tests for {@link HearingDevicesToolItemParser}.
+ */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class HearingDevicesToolItemParserTest extends SysuiTestCase {
+ @Rule
+ public MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private ActivityInfo mActivityInfo;
+ @Mock
+ private Drawable mDrawable;
+ private static final String TEST_PKG = "pkg";
+ private static final String TEST_CLS = "cls";
+ private static final ComponentName TEST_COMPONENT = new ComponentName(TEST_PKG, TEST_CLS);
+ private static final String TEST_NO_EXIST_PKG = "NoPkg";
+ private static final String TEST_NO_EXIST_CLS = "NoCls";
+ private static final ComponentName TEST_NO_EXIST_COMPONENT = new ComponentName(
+ TEST_NO_EXIST_PKG, TEST_NO_EXIST_CLS);
+
+ private static final String TEST_LABEL = "label";
+
+ @Before
+ public void setUp() throws PackageManager.NameNotFoundException {
+ mContext.setMockPackageManager(mPackageManager);
+
+ when(mPackageManager.getActivityInfo(eq(TEST_COMPONENT), anyInt())).thenReturn(
+ mActivityInfo);
+ when(mPackageManager.getActivityInfo(eq(TEST_NO_EXIST_COMPONENT), anyInt())).thenThrow(
+ new PackageManager.NameNotFoundException());
+ when(mActivityInfo.loadLabel(mPackageManager)).thenReturn(TEST_LABEL);
+ when(mActivityInfo.loadIcon(mPackageManager)).thenReturn(mDrawable);
+ when(mActivityInfo.getComponentName()).thenReturn(TEST_COMPONENT);
+ }
+
+ @Test
+ public void parseStringArray_noString_emptyResult() {
+ assertThat(HearingDevicesToolItemParser.parseStringArray(mContext, new String[]{},
+ new String[]{})).isEqualTo(emptyList());
+ }
+
+ @Test
+ public void parseStringArray_oneToolName_oneExpectedToolItem() {
+ String[] toolName = new String[]{TEST_PKG + "/" + TEST_CLS};
+
+ List<ToolItem> toolItemList = HearingDevicesToolItemParser.parseStringArray(mContext,
+ toolName, new String[]{});
+
+ assertThat(toolItemList.size()).isEqualTo(1);
+ assertThat(toolItemList.get(0).getToolName()).isEqualTo(TEST_LABEL);
+ assertThat(toolItemList.get(0).getToolIntent().getComponent()).isEqualTo(TEST_COMPONENT);
+ }
+
+ @Test
+ public void parseStringArray_fourToolName_maxThreeToolItem() {
+ String componentNameString = TEST_PKG + "/" + TEST_CLS;
+ String[] fourToolName =
+ new String[]{componentNameString, componentNameString, componentNameString,
+ componentNameString};
+
+ List<ToolItem> toolItemList = HearingDevicesToolItemParser.parseStringArray(mContext,
+ fourToolName, new String[]{});
+ assertThat(toolItemList.size()).isEqualTo(HearingDevicesToolItemParser.MAX_NUM);
+ }
+
+ @Test
+ public void parseStringArray_oneWrongFormatToolName_noToolItem() {
+ String[] wrongFormatToolName = new String[]{TEST_PKG};
+
+ List<ToolItem> toolItemList = HearingDevicesToolItemParser.parseStringArray(mContext,
+ wrongFormatToolName, new String[]{});
+ assertThat(toolItemList.size()).isEqualTo(0);
+ }
+
+ @Test
+ public void parseStringArray_oneNotExistToolName_noToolItem() {
+ String[] notExistToolName = new String[]{TEST_NO_EXIST_PKG + "/" + TEST_NO_EXIST_CLS};
+
+ List<ToolItem> toolItemList = HearingDevicesToolItemParser.parseStringArray(mContext,
+ notExistToolName, new String[]{});
+ assertThat(toolItemList.size()).isEqualTo(0);
+ }
+}