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