[Audiosharing] Migrate feature from overlay to Settings

Bug: 340379827
Test: atest
Change-Id: I3a88ac1d2f575f3be1f26f617479bbfd25cf6a8e
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index fd9f2e5..57fd25f3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -5194,6 +5194,56 @@
             android:theme="@style/Theme.SpaLib.Dialog">
         </activity>
 
+        <activity
+            android:name="com.android.settings.connecteddevice.audiosharing.AudioSharingActivity"
+            android:label="@string/audio_sharing_title"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+                android:value="com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment"/>
+        </activity>
+
+        <activity
+            android:name="com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeActivity"
+            android:permission="android.permission.BLUETOOTH_CONNECT"
+            android:screenOrientation="portrait"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="android.settings.BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamConfirmDialogActivity"
+            android:exported="true"
+            android:theme="@style/Transparent"
+            android:configChanges="orientation|keyboardHidden|screenSize">
+            <intent-filter android:priority="1">
+                <action android:name="android.settings.AUDIO_STREAM_DIALOG" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+                android:value="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamConfirmDialog" />
+        </activity>
+
+        <service
+            android:name="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService"
+            android:foregroundServiceType="mediaPlayback"
+            android:enabled="true"
+            android:exported="false" />
+
+        <receiver android:name="com.android.settings.connecteddevice.audiosharing.AudioSharingReceiver"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STATE_CHANGE" />
+                <action android:name="com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STOP" />
+            </intent-filter>
+        </receiver>
+
         <!-- This is the longest AndroidManifest.xml ever. -->
     </application>
 </manifest>
diff --git a/res-product/values/strings.xml b/res-product/values/strings.xml
index c9dc248..a42153e 100644
--- a/res-product/values/strings.xml
+++ b/res-product/values/strings.xml
@@ -745,4 +745,17 @@
     <string name="spatial_audio_speaker" product="tablet">Tablet speakers</string>
     <!-- Output device type for the phone speaker that is available for spatializer effect. [CHAR LIMIT=NONE]-->
     <string name="spatial_audio_speaker" product="device">Device speakers</string>
+
+    <!-- Content for audio sharing share dialog [CHAR LIMIT=none]-->
+    <string name="audio_sharing_dialog_share_content" product="default">This phone\'s music and videos will play on both pairs of headphones</string>
+    <string name="audio_sharing_dialog_share_content" product="tablet">This tablet\'s music and videos will play on both pairs of headphones</string>
+    <string name="audio_sharing_dialog_share_content" product="device">This device\'s music and videos will play on both pairs of headphones</string>
+    <!-- Content for audio sharing share dialog with more devices [CHAR LIMIT=none]-->
+    <string name="audio_sharing_dialog_share_more_content" product="default">This phone\'s music and videos will play on the headphones you connect</string>
+    <string name="audio_sharing_dialog_share_more_content" product="tablet">This tablet\'s music and videos will play on the headphones you connect</string>
+    <string name="audio_sharing_dialog_share_more_content" product="device">This device\'s music and videos will play on the headphones you connect</string>
+    <!-- Le audio streams no le device dialog subtitle [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_no_le_device_subtitle" product="default">To listen to an audio stream, first connect headphones that support LE Audio to this phone.</string>
+    <string name="audio_streams_dialog_no_le_device_subtitle" product="tablet">To listen to an audio stream, first connect headphones that support LE Audio to this tablet.</string>
+    <string name="audio_streams_dialog_no_le_device_subtitle" product="device">To listen to an audio stream, first connect headphones that support LE Audio to this device.</string>
 </resources>
diff --git a/res/drawable/audio_sharing_guidance.png b/res/drawable/audio_sharing_guidance.png
new file mode 100644
index 0000000..c0ab637
--- /dev/null
+++ b/res/drawable/audio_sharing_guidance.png
Binary files differ
diff --git a/res/drawable/audio_sharing_rounded_bg.xml b/res/drawable/audio_sharing_rounded_bg.xml
new file mode 100644
index 0000000..db1e1bb
--- /dev/null
+++ b/res/drawable/audio_sharing_rounded_bg.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="?android:colorButtonNormal" />
+    <corners android:radius="12dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/audio_sharing_rounded_bg_ripple.xml b/res/drawable/audio_sharing_rounded_bg_ripple.xml
new file mode 100644
index 0000000..18696c6
--- /dev/null
+++ b/res/drawable/audio_sharing_rounded_bg_ripple.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:drawable="@drawable/audio_sharing_rounded_bg"/>
+</ripple>
\ No newline at end of file
diff --git a/res/drawable/ic_audio_calls_and_alarms.xml b/res/drawable/ic_audio_calls_and_alarms.xml
new file mode 100644
index 0000000..5da27c6
--- /dev/null
+++ b/res/drawable/ic_audio_calls_and_alarms.xml
@@ -0,0 +1,32 @@
+<!--
+  Copyright (C) 2018 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    android:tint="?android:attr/colorControlNormal">
+    <path
+        android:pathData="M3,15V9H7L12,4V20L7,15H3ZM10,15.17V8.83L7.83,11H5V13H7.83L10,15.17Z"
+        android:fillType="evenOdd"
+        android:fillColor="?android:attr/colorPrimary"/>
+    <path
+        android:pathData="M16.5,12C16.5,10.23 15.48,8.71 14,7.97V16.02C15.48,15.29 16.5,13.77 16.5,12Z"
+        android:fillColor="?android:attr/colorPrimary"/>
+    <path
+        android:pathData="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.85 14,18.71V20.77C18.01,19.86 21,16.28 21,12C21,7.72 18.01,4.14 14,3.23Z"
+        android:fillColor="?android:attr/colorPrimary"/>
+</vector>
diff --git a/res/drawable/ic_audio_play_sample.xml b/res/drawable/ic_audio_play_sample.xml
new file mode 100644
index 0000000..3666c22
--- /dev/null
+++ b/res/drawable/ic_audio_play_sample.xml
@@ -0,0 +1,32 @@
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?android:attr/colorControlNormal">
+  <path
+      android:pathData="M14,8C9.6,8 6,11.6 6,16H8C8,12.7 10.7,10 14,10V8Z"
+      android:fillColor="#4E4639"/>
+  <path
+      android:pathData="M14,6V4C7.4,4 2,9.4 2,16H4C4,10.5 8.5,6 14,6Z"
+      android:fillColor="#4E4639"/>
+  <path
+      android:pathData="M16,4V12.6C15.4,12.3 14.7,12 14,12C11.8,12 10,13.8 10,16C10,18.2 11.8,20 14,20C16.2,20 18,18.2 18,16V7H22V4H16ZM14,18C12.9,18 12,17.1 12,16C12,14.9 12.9,14 14,14C15.1,14 16,14.9 16,16C16,17.1 15.1,18 14,18Z"
+      android:fillColor="#4E4639"/>
+</vector>
diff --git a/res/layout/audio_sharing_device_item.xml b/res/layout/audio_sharing_device_item.xml
new file mode 100644
index 0000000..04ecdd7
--- /dev/null
+++ b/res/layout/audio_sharing_device_item.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <Button
+        android:id="@+id/device_button"
+        style="@style/SettingsLibActionButton"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="4dp"
+        android:background="@drawable/audio_sharing_rounded_bg_ripple"
+        android:textAlignment="center" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/res/layout/audio_sharing_password_dialog.xml b/res/layout/audio_sharing_password_dialog.xml
new file mode 100644
index 0000000..f1a78bc
--- /dev/null
+++ b/res/layout/audio_sharing_password_dialog.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2022 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.
+  -->
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginTop="48dp"
+    android:layout_marginBottom="48dp"
+    android:overScrollMode="ifContentScrolls">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <EditText
+            android:id="@android:id/edit"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="20dp"
+            android:layout_marginEnd="20dp"
+            android:minHeight="48dp" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/audio_sharing_stream_password_checkbox_text"
+                style="?android:attr/textAppearanceSmall"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="24dp"
+                android:layout_marginTop="24dp"
+                android:layout_weight="1"
+                android:text="@string/audio_streams_no_password_summary"
+                android:textColor="?android:attr/textColorSecondary" />
+
+            <CheckBox
+                android:id="@+id/audio_sharing_stream_password_checkbox"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="24dp"
+                android:layout_marginEnd="20dp" />
+        </LinearLayout>
+
+        <TextView
+            android:id="@android:id/message"
+            style="?android:attr/textAppearanceSmall"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="24dp"
+            android:layout_marginTop="24dp"
+            android:layout_marginEnd="24dp"
+            android:layout_marginBottom="24dp"
+            android:text="@string/audio_streams_main_page_password_dialog_cannot_edit"
+            android:textColor="?android:attr/textColorSecondary" />
+
+    </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/res/layout/dialog_custom_body_audio_sharing.xml b/res/layout/dialog_custom_body_audio_sharing.xml
new file mode 100644
index 0000000..388a4941
--- /dev/null
+++ b/res/layout/dialog_custom_body_audio_sharing.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:paddingHorizontal="?android:dialogPreferredPadding"
+    android:paddingBottom="?android:dialogPreferredPadding">
+
+    <TextView
+        android:id="@+id/description_text"
+        style="@style/DeviceAudioSharingText"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:paddingBottom="24dp"
+        android:visibility="gone" />
+
+    <ImageView
+        android:id="@+id/description_image"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:contentDescription="@null"
+        android:visibility="gone" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/device_btn_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:nestedScrollingEnabled="false"
+        android:overScrollMode="never"
+        android:visibility="gone" />
+
+    <Button
+        android:id="@+id/positive_btn"
+        style="@style/SettingsLibActionButton"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="4dp"
+        android:background="@drawable/audio_sharing_rounded_bg_ripple"
+        android:visibility="gone" />
+
+    <Button
+        android:id="@+id/negative_btn"
+        style="@style/SettingsLibActionButton"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:layout_marginTop="4dp"
+        android:background="@drawable/audio_sharing_rounded_bg_ripple"
+        android:visibility="gone" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/dialog_custom_title_audio_sharing.xml b/res/layout/dialog_custom_title_audio_sharing.xml
new file mode 100644
index 0000000..86e0010
--- /dev/null
+++ b/res/layout/dialog_custom_title_audio_sharing.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:padding="?android:dialogPreferredPadding">
+
+    <ImageView
+        android:id="@+id/title_icon"
+        android:layout_width="28dp"
+        android:layout_height="28dp"
+        android:layout_gravity="center"
+        android:contentDescription="@null"
+        android:tint="?android:attr/colorControlNormal" />
+
+    <TextView
+        android:id="@+id/title_text"
+        style="@android:style/TextAppearance.DeviceDefault.Headline"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:maxLines="2"
+        android:paddingTop="14dp"
+        android:textAlignment="center"
+        android:textSize="24sp" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/preference_widget_lock.xml b/res/layout/preference_widget_lock.xml
new file mode 100644
index 0000000..6ef088f
--- /dev/null
+++ b/res/layout/preference_widget_lock.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+
+<ImageView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/lock_icon"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_gravity="center"
+    android:paddingStart="24dp"
+    android:paddingEnd="24dp"
+    android:background="?android:attr/selectableItemBackground"
+    android:scaleType="center"
+    android:src="@drawable/ic_lock_closed"
+    android:importantForAccessibility="no" />
+
diff --git a/res/layout/qrcode_scanner_fragment.xml b/res/layout/qrcode_scanner_fragment.xml
index d402dc3..d24e7f7 100644
--- a/res/layout/qrcode_scanner_fragment.xml
+++ b/res/layout/qrcode_scanner_fragment.xml
@@ -35,8 +35,8 @@
             android:gravity="center"
             android:orientation="vertical">
             <TextView
+                android:id="@android:id/summary"
                 style="@style/QrCodeScanner"
-                android:text="Scan an audio stream QR code to listen with the active LE device"
                 android:gravity="center"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index e957ea5..dcfc410 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -488,4 +488,7 @@
     <dimen name="contrast_button_text_size">14sp</dimen>
     <dimen name="contrast_button_text_spacing">4dp</dimen>
     <dimen name="contrast_button_horizontal_spacing">16dp</dimen>
+
+    <dimen name="audio_streams_qrcode_size">264dp</dimen>
+    <dimen name="audio_streams_qrcode_preview_radius">30dp</dimen>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f88b036..c8d2226 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -303,9 +303,6 @@
     <!-- Name shown in the title of individual stylus preference in the connected devices page [CHAR LIMIT=60] -->
     <string name="stylus_connected_devices_title">Stylus</string>
 
-    <!--Text that appears when scanning for nearby audio streams is finished and no streams were found [CHAR LIMIT=40]-->
-    <string name="audio_streams_empty">No nearby audio streams were found.</string>
-
     <!-- Date & time settings screen title -->
     <string name="date_and_time">Date &amp; time</string>
 
@@ -7315,8 +7312,6 @@
     <string name="help_url_insecure_vpn" translatable="false"></string>
     <!-- url for learning more about IT admin policy disabling -->
     <string name="help_url_action_disabled_by_it_admin" translatable="false"></string>
-    <!-- url for learning more about bluetooth audio sharing -->
-    <string name="help_url_audio_sharing" translatable="false"></string>
 
     <!-- User account title [CHAR LIMIT=30] -->
     <string name="user_account_title">Account for content</string>
@@ -13270,4 +13265,198 @@
 
     <!-- Title for System dashboard fragment -->
     <string name="device_diagnostics_title">Device diagnostics</string>
+
+    <!-- Title for audio sharing page [CHAR LIMIT=none]-->
+    <string name="audio_sharing_title">Audio sharing</string>
+    <!-- Title for audio sharing primary switch [CHAR LIMIT=none]-->
+    <string name="audio_sharing_switch_title">Share audio</string>
+    <!-- Title for calls and alarms device on audio sharing page [CHAR LIMIT=none]-->
+    <string name="calls_and_alarms_device_title">Calls and alarms</string>
+    <!-- Description for audio sharing page [CHAR LIMIT=none]-->
+    <string name="audio_sharing_description">Let people listen to your media along with you. Listeners need their own LE Audio headphones.</string>
+    <!-- Title for audio sharing device group [CHAR LIMIT=none]-->
+    <string name="audio_sharing_device_group_title">Active media devices</string>
+    <!-- Title for call audio on audio sharing page [CHAR LIMIT=none]-->
+    <string name="audio_sharing_call_audio_title">Call audio</string>
+    <!-- Description for call audio on audio sharing page [CHAR LIMIT=none]-->
+    <string name="audio_sharing_call_audio_description">Play only on <xliff:g example="My buds" id="device_name">%1$s</xliff:g></string>
+    <!-- Title for play test sound on audio sharing page [CHAR LIMIT=none]-->
+    <string name="audio_sharing_test_sound_title">Play a test sound</string>
+    <!-- Description for play test sound on audio sharing page [CHAR LIMIT=none]-->
+    <string name="audio_sharing_test_sound_description">Everyone listening should hear it</string>
+    <!-- Title for stream settings group on audio sharing page [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stream_settings_title">Audio stream settings</string>
+    <!-- Title for stream name on audio sharing page, under stream settings group [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stream_name_title">Name</string>
+    <!-- Title for stream password on audio sharing page, under stream settings group [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stream_password_title">Password</string>
+    <!-- Title for stream compatibility on audio sharing page, under stream settings group [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stream_compatibility_title">Improve compatibility</string>
+    <!-- Description for stream compatibility on audio sharing page [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stream_compatibility_description">Helps some devices, like hearing aids, connect by reducing audio quality</string>
+    <!-- Description for stream compatibility on audio sharing page when audio sharing is on [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stream_compatibility_disabled_description">Turns off the audio sharing to config the compatibility</string>
+    <!-- Title for nearby audio group on audio sharing page [CHAR LIMIT=none]-->
+    <string name="audio_sharing_nearby_audio_title">Listen to nearby audio</string>
+    <!-- Description for audio sharing page footer [CHAR LIMIT=none]-->
+    <string name="audio_sharing_footer_description">Audio sharing supports Auracast™</string>
+    <!-- Title for stream name dialog [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stream_name_dialog_title">Audio stream name</string>
+    <!-- Title for stream password dialog [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stream_password_dialog_title">Audio stream password</string>
+    <!-- Title for media device group during audio sharing [CHAR LIMIT=none]-->
+    <string name="audio_sharing_media_device_group_title">Other media devices</string>
+    <!-- Summary for audio sharing on [CHAR LIMIT=none]-->
+    <string name="audio_sharing_summary_on">On</string>
+    <!-- Summary for audio sharing off [CHAR LIMIT=none]-->
+    <string name="audio_sharing_summary_off">Off</string>
+    <!-- Title for audio sharing share dialog [CHAR LIMIT=none]-->
+    <string name="audio_sharing_share_dialog_title">Share your audio</string>
+    <!-- Subtitle for audio sharing share dialog [CHAR LIMIT=none]-->
+    <string name="audio_sharing_share_dialog_subtitle"><xliff:g example="My buds1" id="device_name1">%1$s</xliff:g> and <xliff:g example="My buds2" id="device_name2">%2$s</xliff:g></string>
+    <!-- Text for audio sharing share button [CHAR LIMIT=none]-->
+    <string name="audio_sharing_share_button_label">Share audio</string>
+    <!-- Text for audio sharing no thanks button [CHAR LIMIT=none]-->
+    <string name="audio_sharing_no_thanks_button_label">No thanks</string>
+    <!-- Title for audio sharing share dialog with one device [CHAR LIMIT=none]-->
+    <string name="audio_sharing_share_with_dialog_title">Share audio with <xliff:g example="My buds" id="device_name">%1$s</xliff:g>?</string>
+    <!-- Title for audio sharing share dialog with more devices [CHAR LIMIT=none]-->
+    <string name="audio_sharing_share_with_more_dialog_title">Share audio with another device</string>
+    <!-- Text for audio sharing share with button [CHAR LIMIT=none]-->
+    <string name="audio_sharing_share_with_button_label">Share with <xliff:g example="My buds" id="device_name">%1$s</xliff:g></string>
+    <!-- Text for audio sharing close button [CHAR LIMIT=none]-->
+    <string name="audio_sharing_close_button_label">Close</string>
+    <!-- Content for audio sharing share dialog with no device, ask users to connect device  [CHAR LIMIT=none]-->
+    <string name="audio_sharing_dialog_connect_device_content">Connect another pair of compatible headphones, or share your stream\'s name and password with the other person</string>
+    <!-- Content for audio sharing share dialog with no device, ask users to pair device  [CHAR LIMIT=none]-->
+    <string name="audio_sharing_dialog_pair_device_content">Pair another set of compatible headphones, or share your audio stream QR code with the other person</string>
+    <!-- Text for sharing audio sharing state [CHAR LIMIT=none]-->
+    <string name="audio_sharing_sharing_label">Sharing audio</string>
+    <!-- Text for audio sharing pair button [CHAR LIMIT=none]-->
+    <string name="audio_sharing_pair_button_label">Pair new device</string>
+    <!-- Text for audio sharing qrcode button [CHAR LIMIT=none]-->
+    <string name="audio_sharing_qrcode_button_label">Show QR code</string>
+    <!-- Title for audio sharing notification [CHAR LIMIT=none]-->
+    <string name="audio_sharing_notification_title">You\'re sharing audio</string>
+    <!-- Content for audio sharing notification [CHAR LIMIT=none]-->
+    <string name="audio_sharing_notification_content">People listening can hear your media. They won\'t hear calls.</string>
+    <!-- Text for audio sharing stop button [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stop_button_label">Stop sharing</string>
+    <!-- Text for audio sharing settings button [CHAR LIMIT=none]-->
+    <string name="audio_sharing_settings_button_label">Settings</string>
+    <!-- Title for audio sharing disconnect dialog [CHAR LIMIT=none]-->
+    <string name="audio_sharing_disconnect_dialog_title">Choose a device to disconnect</string>
+    <!-- Content for audio sharing disconnect dialog [CHAR LIMIT=none]-->
+    <string name="audio_sharing_dialog_disconnect_content">Only 2 devices can share audio at a time</string>
+    <!-- Text for audio sharing disconnect device button [CHAR LIMIT=none]-->
+    <string name="audio_sharing_disconnect_device_button_label">Disconnect <xliff:g example="My buds" id="device_name">%1$s</xliff:g></string>
+    <!-- Title for audio sharing stop dialog [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stop_dialog_title">Connect <xliff:g example="My buds" id="device_name">%1$s</xliff:g> ?</string>
+    <!-- Content for audio sharing stop dialog [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stop_dialog_content">You\'ll stop sharing audio with <xliff:g example="My buds" id="device_name">%1$s</xliff:g></string>
+    <!-- Content for audio sharing stop dialog with two devices [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stop_dialog_with_two_content">You\'ll stop sharing audio with <xliff:g example="My buds1" id="device_name1">%1$s</xliff:g> and <xliff:g example="My buds2" id="device_name2">%2$s</xliff:g></string>
+    <!-- Content for audio sharing stop dialog with more devices  [CHAR LIMIT=none]-->
+    <string name="audio_sharing_stop_dialog_with_more_content">You\'ll stop sharing audio with the connected headphones</string>
+    <!-- Text for audio sharing connect button [CHAR LIMIT=none]-->
+    <string name="audio_sharing_connect_button_label">Connect</string>
+    <!-- Text for sharing audio stop state [CHAR LIMIT=none]-->
+    <string name="audio_sharing_sharing_stopped_label">Audio sharing stopped</string>
+    <!-- Title for audio sharing confirm dialog [CHAR LIMIT=none]-->
+    <string name="audio_sharing_confirm_dialog_title">Connect a compatible device</string>
+    <!-- Content for audio sharing confirm dialog [CHAR LIMIT=none]-->
+    <string name="audio_sharing_comfirm_dialog_content">To start sharing audio, first connect LE Audio headphones to your phone</string>
+
+    <!-- Title for audio streams preference category [CHAR LIMIT=none]-->
+    <string name="audio_streams_category_title">Connect to a LE audio stream</string>
+    <!-- Title for audio streams preference [CHAR LIMIT=none]-->
+    <string name="audio_streams_pref_title">Nearby audio streams</string>
+    <!-- Title for audio streams page [CHAR LIMIT=none]-->
+    <string name="audio_streams_title">Audio streams</string>
+    <!-- Summary for QR code scanning in audio streams page [CHAR LIMIT=none]-->
+    <string name="audio_streams_qr_code_summary">Connect to an audio stream using QR code</string>
+    <!--Text that appears when scanning for nearby audio streams is finished and no streams were found [CHAR LIMIT=40]-->
+    <string name="audio_streams_empty">No nearby audio streams were found.</string>
+    <!-- Disconnect from an audio stream [CHAR LIMIT=none]-->
+    <string name="audio_streams_disconnect">Disconnect</string>
+    <!-- Connect an audio stream [CHAR LIMIT=none]-->
+    <string name="audio_streams_connect">Connect</string>
+    <!-- Hint for QR code process failure [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_qr_code_is_not_valid_format">QR code isn\u0027t a valid format</string>
+    <!-- Le audio QR code scanner sub-title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_qr_code_scanner">To start listening, center the QR code below</string>
+    <!-- The preference summary when add source response is bad code [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_add_source_bad_code_state_summary">Check password and try again</string>
+    <!-- The preference summary when add source response results in general failure [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_add_source_failed_state_summary">Can\u0027t connect. Try again.</string>
+    <!-- The preference summary when waiting for add source response [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_add_source_wait_for_response_summary">Connecting\u2026</string>
+    <!-- The preference summary when waiting for sync [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_wait_for_sync_state_summary">Scanning\u2026</string>
+    <!-- Le audio streams audio lost dialog title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_stream_is_not_available">Audio stream isn\u0027t available</string>
+    <!-- Le audio streams audio lost dialog subtitle [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_is_not_playing">This audio stream isn\u0027t playing anything right now</string>
+    <!-- Le audio streams dialog close [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_close">Close</string>
+    <!-- Le audio streams dialog listen [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_listen">Listen</string>
+    <!-- Le audio streams dialog retry button [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_retry">Try again</string>
+    <!-- Le audio streams confirmation dialog title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_listen_to_audio_stream">Listen to audio stream</string>
+    <!-- Le audio streams confirmation dialog subtitle [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_control_volume">The audio stream will play on <xliff:g example="LE headset" id="device_name">%1$s</xliff:g>. Use this device to control the volume.</string>
+    <!-- Le audio streams failure dialog title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_cannot_listen">Can\u0027t listen to audio stream</string>
+    <!-- Le audio streams confirm dialog default device [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_default_device">connected compatible headphones</string>
+    <!-- Le audio streams activity title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_activity_title">Broadcasts</string>
+    <!-- Le audio streams no password summary [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_no_password_summary">No password</string>
+    <!-- Le audio streams failure dialog subtitle [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_learn_more">Learn more</string>
+    <!-- Le audio streams failure dialog subtitle [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_cannot_play">Can\u0027t play this audio stream on <xliff:g example="LE headset" id="device_name">%1$s</xliff:g>.</string>
+    <!-- The preference summary when add source succeed [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_listening_now">Listening now</string>
+    <!-- Le audio streams service notification leave broadcast text [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_media_service_notification_leave_broadcast_text">Stop listening</string>
+    <!-- Le audio streams no le device dialog title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_no_le_device_title">Connect compatible headphones</string>
+    <!-- Le audio streams no le device dialog button [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_dialog_no_le_device_button">Connect a device</string>
+    <!-- Le audio streams detail page title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_detail_page_title">Audio stream details</string>
+    <!-- Le audio streams qr code page title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_qr_code_page_title">Audio stream QR code</string>
+    <!-- Le audio streams qr code page password text [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_qr_code_page_password">Password: <xliff:g example="123" id="password">%1$s</xliff:g></string>
+    <!-- Le audio streams qr code page description [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_qr_code_page_description">To listen to <xliff:g example="Local Music" id="stream_name">%1$s</xliff:g>, other people can connect compatible headphones to their Android device. They can then scan this QR code.</string>
+    <!-- Le audio streams main page title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_main_page_title">Find an audio stream</string>
+    <!-- Le audio streams main page subtitle [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_main_page_subtitle">Listen to a device that\u0027s sharing audio or to a nearby Auracast broadcast</string>
+    <!-- Le audio streams main page device preference title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_main_page_device_title">Your audio device</string>
+    <!-- Le audio streams main page device preference no device summary [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_main_page_no_device_summary">Connect compatible headphones</string>
+    <!-- Le audio streams main page scanning section title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_main_page_scan_section_title">Audio streams nearby</string>
+    <!-- Le audio streams main page scan qr code preference title [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_main_page_scan_qr_code_title">Scan QR code</string>
+    <!-- Le audio streams main page scan qr code preference summary [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_main_page_scan_qr_code_summary">Start listening by scanning a stream\u0027s QR code</string>
+    <!-- Le audio streams main page password dialog join button [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_main_page_password_dialog_join_button">Listen to stream</string>
+    <!-- Le audio streams main page qr code scanner summary [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_main_page_qr_code_scanner_summary">Scan an audio stream QR code to listen with <xliff:g example="LE headset" id="device_name">%1$s</xliff:g></string>
+    <!-- Le audio streams password dialog  [CHAR LIMIT=NONE] -->
+    <string name="audio_streams_main_page_password_dialog_cannot_edit">Can\u0027t edit password while sharing. To change the password, first turn off audio sharing.</string>
+
+
+    <!-- url for learning more about bluetooth audio sharing -->
+    <string name="help_url_audio_sharing" translatable="false"></string>
 </resources>
diff --git a/res/xml/bluetooth_audio_streams_dialog.xml b/res/xml/bluetooth_audio_streams_dialog.xml
new file mode 100644
index 0000000..8b20a14
--- /dev/null
+++ b/res/xml/bluetooth_audio_streams_dialog.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:id="@+id/dialog_bg"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:paddingStart="25dp"
+        android:paddingEnd="25dp"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="25dp"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/dialog_icon"
+                android:layout_width="30dp"
+                android:layout_height="30dp"
+                android:layout_marginTop="24dp"
+                android:layout_gravity="center"
+                android:src="@drawable/ic_bt_le_audio_sharing"/>
+
+            <TextView
+                android:id="@+id/dialog_title"
+                android:textAppearance="@android:style/TextAppearance.DeviceDefault.Headline"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="15dp"
+                android:gravity="center"
+                android:layout_gravity="center"/>
+
+            <TextView
+                android:id="@+id/dialog_subtitle"
+                android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
+                android:textStyle="bold"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="15dp"
+                android:gravity="center"
+                android:layout_gravity="center"
+                android:visibility="gone"/>
+
+            <TextView
+                android:id="@+id/dialog_subtitle_2"
+                android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="15dp"
+                android:gravity="center"
+                android:layout_gravity="center"
+                android:visibility="gone"/>
+        </LinearLayout>
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="@dimen/broadcast_dialog_margin">
+            <Button
+                android:id="@+id/left_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:visibility="gone"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                style="@style/BroadcastActionButton"/>
+            <Button
+                android:id="@+id/right_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:visibility="gone"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                style="@style/BroadcastActionButton"/>
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </LinearLayout>
+</FrameLayout>
\ No newline at end of file
diff --git a/res/xml/bluetooth_audio_streams_qr_code.xml b/res/xml/bluetooth_audio_streams_qr_code.xml
new file mode 100644
index 0000000..a098845
--- /dev/null
+++ b/res/xml/bluetooth_audio_streams_qr_code.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:clipToPadding="false"
+        android:paddingLeft="25dp"
+        android:paddingRight="25dp"
+        android:gravity="center_horizontal"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@android:id/summary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="start"
+            android:textSize="15sp"
+            android:textColor="?android:attr/textColorPrimary"/>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:paddingTop="70dp">
+
+            <ImageView
+                android:id="@+id/qrcode_view"
+                android:layout_width="@dimen/qrcode_size"
+                android:layout_height="@dimen/qrcode_size"
+                android:src="@android:color/transparent"/>
+
+            <TextView
+                android:id="@+id/password"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textSize="15sp"
+                android:textColor="?android:attr/textColorPrimary"/>
+        </LinearLayout>
+
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/xml/bluetooth_le_audio_sharing.xml b/res/xml/bluetooth_le_audio_sharing.xml
new file mode 100644
index 0000000..8ba6c07
--- /dev/null
+++ b/res/xml/bluetooth_le_audio_sharing.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:title="@string/audio_sharing_title">
+
+    <com.android.settingslib.widget.TopIntroPreference
+        android:key="audio_sharing_top_intro"
+        android:title="@string/audio_sharing_description"
+        settings:searchable="false" />
+
+    <PreferenceCategory
+        android:key="audio_sharing_device_volume_group"
+        android:title="@string/audio_sharing_device_group_title"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingDeviceVolumeGroupController" />
+
+    <Preference
+        android:icon="@drawable/ic_audio_calls_and_alarms"
+        android:key="calls_and_alarms"
+        android:summary=""
+        android:title="@string/audio_sharing_call_audio_title"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.CallsAndAlarmsPreferenceController" />
+
+    <Preference
+        android:icon="@drawable/ic_audio_play_sample"
+        android:key="audio_sharing_play_sound"
+        android:summary="@string/audio_sharing_test_sound_description"
+        android:title="@string/audio_sharing_test_sound_title"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPlaySoundPreferenceController" />
+
+    <PreferenceCategory
+        android:key="audio_sharing_stream_settings_category"
+        android:title="@string/audio_sharing_stream_settings_title"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.StreamSettingsCategoryController">
+
+        <com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreference
+            android:key="audio_sharing_stream_name"
+            android:title="@string/audio_sharing_stream_name_title"
+            settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreferenceController" />
+
+        <com.android.settings.connecteddevice.audiosharing.AudioSharingPasswordPreference
+            android:dialogLayout="@layout/audio_sharing_password_dialog"
+            android:key="audio_sharing_stream_password"
+            android:summary="********"
+            android:title="@string/audio_sharing_stream_password_title"
+            settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPasswordPreferenceController" />
+
+        <SwitchPreferenceCompat
+            android:key="audio_sharing_stream_compatibility"
+            android:title="@string/audio_sharing_stream_compatibility_title"
+            settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingCompatibilityPreferenceController" />
+    </PreferenceCategory>
+
+    <PreferenceCategory
+        android:key="audio_streams_settings_category"
+        android:title="@string/audio_streams_category_title"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController">
+
+        <Preference
+            android:fragment="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsDashboardFragment"
+            android:icon="@drawable/ic_chevron_right_24dp"
+            android:key="audio_streams_settings"
+            android:title="@string/audio_streams_pref_title" />
+
+    </PreferenceCategory>
+</PreferenceScreen>
\ No newline at end of file
diff --git a/res/xml/bluetooth_le_audio_stream_details_fragment.xml b/res/xml/bluetooth_le_audio_stream_details_fragment.xml
new file mode 100644
index 0000000..883681a
--- /dev/null
+++ b/res/xml/bluetooth_le_audio_stream_details_fragment.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:title="@string/audio_streams_detail_page_title">
+
+    <com.android.settingslib.widget.LayoutPreference
+        android:key="audio_stream_header"
+        android:layout="@layout/settings_entity_header"
+        android:selectable="false"
+        settings:allowDividerBelow="true"
+        settings:searchable="false"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController" />
+
+    <com.android.settingslib.widget.ActionButtonsPreference
+        android:key="audio_stream_button"
+        settings:allowDividerBelow="true"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamButtonController" />
+
+</PreferenceScreen>
diff --git a/res/xml/bluetooth_le_audio_streams.xml b/res/xml/bluetooth_le_audio_streams.xml
new file mode 100644
index 0000000..db4bd85
--- /dev/null
+++ b/res/xml/bluetooth_le_audio_streams.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:title="@string/audio_streams_main_page_title">
+
+    <com.android.settingslib.widget.TopIntroPreference
+        android:key="audio_streams_top_intro"
+        android:title="@string/audio_streams_main_page_subtitle"
+        settings:searchable="false" />
+
+    <Preference
+        android:key="audio_streams_active_device"
+        android:title="@string/audio_streams_main_page_device_title"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsActiveDeviceController" />
+
+    <com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryPreference
+        android:key="audio_streams_nearby_category"
+        android:title="@string/audio_streams_main_page_scan_section_title"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController">
+        <Preference
+            android:icon="@drawable/ic_add_24dp"
+            android:key="audio_streams_scan_qr_code"
+            android:order="0"
+            android:summary="@string/audio_streams_main_page_scan_qr_code_summary"
+            android:title="@string/audio_streams_main_page_scan_qr_code_title"
+            settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController" />
+    </com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryPreference>
+
+</PreferenceScreen>
\ No newline at end of file
diff --git a/res/xml/connected_devices.xml b/res/xml/connected_devices.xml
index 40ab145..95aa877 100644
--- a/res/xml/connected_devices.xml
+++ b/res/xml/connected_devices.xml
@@ -27,8 +27,22 @@
         settings:controller="com.android.settings.slices.SlicePreferenceController" />
 
     <PreferenceCategory
+        android:key="audio_sharing_device_list"
+        android:title="@string/audio_sharing_device_group_title"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingDevicePreferenceController">
+        <Preference
+            android:fragment="com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment"
+            android:icon="@drawable/ic_bt_le_audio_sharing"
+            android:key="connected_device_audio_sharing_settings"
+            android:order="100"
+            android:title="@string/audio_sharing_title"
+            settings:searchable="false" />
+    </PreferenceCategory>
+
+    <PreferenceCategory
         android:key="available_device_list"
-        android:title="@string/connected_device_media_device_title"/>
+        android:title="@string/connected_device_media_device_title"
+        settings:controller="com.android.settings.connecteddevice.AvailableMediaDeviceGroupController" />
 
     <PreferenceCategory
         android:key="connected_device_list"
diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml
index 68b4c04..779555b 100644
--- a/res/xml/connected_devices_advanced.xml
+++ b/res/xml/connected_devices_advanced.xml
@@ -26,6 +26,15 @@
         android:order="-10"
         android:title="@string/bluetooth_settings_title" />
 
+    <Preference
+        android:fragment="com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment"
+        android:icon="@drawable/ic_bt_le_audio_sharing"
+        android:key="audio_sharing_settings"
+        android:order="-9"
+        android:title="@string/audio_sharing_title"
+        settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPreferenceController"
+        settings:searchable="true" />
+
     <com.android.settingslib.RestrictedPreference
         android:fragment="com.android.settings.connecteddevice.NfcAndPaymentFragment"
         android:icon="@drawable/ic_nfc"
diff --git a/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java b/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java
index 0ee3986..b361bd2 100644
--- a/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java
+++ b/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdater.java
@@ -23,7 +23,8 @@
 import androidx.preference.Preference;
 
 import com.android.settings.connecteddevice.DevicePreferenceCallback;
-import com.android.settings.overlay.FeatureFactory;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settingslib.bluetooth.BluetoothUtils;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 
@@ -76,11 +77,17 @@
             // It would show in Available Devices group if the audio sharing flag is disabled or
             // the device is not in the audio sharing session.
             if (cachedDevice.isConnectedLeAudioDevice()) {
-                boolean isAudioSharingFilterMatched =
-                        FeatureFactory.getFeatureFactory()
-                                .getAudioSharingFeatureProvider()
-                                .isAudioSharingFilterMatched(cachedDevice, mLocalManager);
-                if (!isAudioSharingFilterMatched) {
+                if (AudioSharingUtils.isFeatureEnabled()
+                        && BluetoothUtils.hasConnectedBroadcastSource(
+                                cachedDevice, mLocalBtManager)) {
+                    Log.d(
+                            TAG,
+                            "Filter out device : "
+                                    + cachedDevice.getName()
+                                    + ", it is in audio sharing.");
+                    return false;
+
+                } else {
                     Log.d(
                             TAG,
                             "isFilterMatched() device : "
@@ -88,13 +95,6 @@
                                     + ", the LE Audio profile is connected and not in sharing "
                                     + "if broadcast enabled.");
                     return true;
-                } else {
-                    Log.d(
-                            TAG,
-                            "Filter out device : "
-                                    + cachedDevice.getName()
-                                    + ", it is in audio sharing.");
-                    return false;
                 }
             }
 
diff --git a/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java b/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java
index 56ef4b0..89759b7 100644
--- a/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java
+++ b/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupController.java
@@ -17,6 +17,10 @@
 
 import static com.android.settingslib.Utils.isAudioModeOngoingCall;
 
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 import android.content.pm.PackageManager;
@@ -38,13 +42,18 @@
 import com.android.settings.bluetooth.BluetoothDevicePreference;
 import com.android.settings.bluetooth.BluetoothDeviceUpdater;
 import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingDialogHandler;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 import com.android.settings.core.BasePreferenceController;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settingslib.bluetooth.BluetoothCallback;
 import com.android.settingslib.bluetooth.BluetoothUtils;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
-import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 
 /**
  * Controller to maintain the {@link androidx.preference.PreferenceGroup} for all available media
@@ -57,23 +66,78 @@
     private static final String TAG = "AvailableMediaDeviceGroupController";
     private static final String KEY = "available_device_list";
 
+    private final Executor mExecutor;
+    @VisibleForTesting @Nullable LocalBluetoothManager mLocalBluetoothManager;
     @VisibleForTesting @Nullable PreferenceGroup mPreferenceGroup;
-    @VisibleForTesting LocalBluetoothManager mLocalBluetoothManager;
     @Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
     @Nullable private FragmentManager mFragmentManager;
+    @Nullable private AudioSharingDialogHandler mDialogHandler;
+    private BluetoothLeBroadcastAssistant.Callback mAssistantCallback =
+            new BluetoothLeBroadcastAssistant.Callback() {
+                @Override
+                public void onSearchStarted(int reason) {}
 
-    public AvailableMediaDeviceGroupController(
-            Context context,
-            @Nullable DashboardFragment fragment,
-            @Nullable Lifecycle lifecycle) {
+                @Override
+                public void onSearchStartFailed(int reason) {}
+
+                @Override
+                public void onSearchStopped(int reason) {}
+
+                @Override
+                public void onSearchStopFailed(int reason) {}
+
+                @Override
+                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
+
+                @Override
+                public void onSourceAdded(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceAddFailed(
+                        @NonNull BluetoothDevice sink,
+                        @NonNull BluetoothLeBroadcastMetadata source,
+                        int reason) {}
+
+                @Override
+                public void onSourceModified(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceModifyFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceRemoved(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {
+                    Log.d(TAG, "onSourceRemoved: update media device list.");
+                    if (mBluetoothDeviceUpdater != null) {
+                        mBluetoothDeviceUpdater.forceUpdate();
+                    }
+                }
+
+                @Override
+                public void onSourceRemoveFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onReceiveStateChanged(
+                        @NonNull BluetoothDevice sink,
+                        int sourceId,
+                        @NonNull BluetoothLeBroadcastReceiveState state) {
+                    if (BluetoothUtils.isConnected(state)) {
+                        Log.d(TAG, "onReceiveStateChanged: synced, update media device list.");
+                        if (mBluetoothDeviceUpdater != null) {
+                            mBluetoothDeviceUpdater.forceUpdate();
+                        }
+                    }
+                }
+            };
+
+    public AvailableMediaDeviceGroupController(Context context) {
         super(context, KEY);
-        if (fragment != null) {
-            init(fragment);
-        }
-        if (lifecycle != null) {
-            lifecycle.addObserver(this);
-        }
         mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+        mExecutor = Executors.newSingleThreadExecutor();
     }
 
     @Override
@@ -82,6 +146,21 @@
             Log.e(TAG, "onStart() Bluetooth is not supported on this device");
             return;
         }
+        if (AudioSharingUtils.isFeatureEnabled()) {
+            LocalBluetoothLeBroadcastAssistant assistant =
+                    mLocalBluetoothManager
+                            .getProfileManager()
+                            .getLeAudioBroadcastAssistantProfile();
+            if (assistant != null) {
+                if (DEBUG) {
+                    Log.d(TAG, "onStart() Register callbacks for assistant.");
+                }
+                assistant.registerServiceCallBack(mExecutor, mAssistantCallback);
+            }
+            if (mDialogHandler != null) {
+                mDialogHandler.registerCallbacks(mExecutor);
+            }
+        }
         mLocalBluetoothManager.getEventManager().registerCallback(this);
         if (mBluetoothDeviceUpdater != null) {
             mBluetoothDeviceUpdater.registerCallback();
@@ -95,6 +174,21 @@
             Log.e(TAG, "onStop() Bluetooth is not supported on this device");
             return;
         }
+        if (AudioSharingUtils.isFeatureEnabled()) {
+            LocalBluetoothLeBroadcastAssistant assistant =
+                    mLocalBluetoothManager
+                            .getProfileManager()
+                            .getLeAudioBroadcastAssistantProfile();
+            if (assistant != null) {
+                if (DEBUG) {
+                    Log.d(TAG, "onStop() Register callbacks for assistant.");
+                }
+                assistant.unregisterServiceCallBack(mAssistantCallback);
+            }
+            if (mDialogHandler != null) {
+                mDialogHandler.unregisterCallbacks();
+            }
+        }
         if (mBluetoothDeviceUpdater != null) {
             mBluetoothDeviceUpdater.unregisterCallback();
         }
@@ -155,7 +249,11 @@
     public void onDeviceClick(Preference preference) {
         final CachedBluetoothDevice cachedDevice =
                 ((BluetoothDevicePreference) preference).getBluetoothDevice();
-        cachedDevice.setActive();
+        if (AudioSharingUtils.isFeatureEnabled() && mDialogHandler != null) {
+            mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ true);
+        } else {
+            cachedDevice.setActive();
+        }
     }
 
     public void init(DashboardFragment fragment) {
@@ -165,6 +263,9 @@
                         fragment.getContext(),
                         AvailableMediaDeviceGroupController.this,
                         fragment.getMetricsCategory());
+        if (AudioSharingUtils.isFeatureEnabled()) {
+            mDialogHandler = new AudioSharingDialogHandler(mContext, fragment);
+        }
     }
 
     @VisibleForTesting
@@ -177,6 +278,11 @@
         mBluetoothDeviceUpdater = bluetoothDeviceUpdater;
     }
 
+    @VisibleForTesting
+    public void setDialogHandler(AudioSharingDialogHandler dialogHandler) {
+        mDialogHandler = dialogHandler;
+    }
+
     @Override
     public void onAudioModeChanged() {
         updateTitle();
diff --git a/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java b/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java
index 04ba5d2..27001d6 100644
--- a/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java
@@ -22,12 +22,13 @@
 import android.text.TextUtils;
 import android.util.Log;
 
-import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
 import com.android.settings.R;
 import com.android.settings.SettingsActivity;
 import com.android.settings.Utils;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingDevicePreferenceController;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 import com.android.settings.core.SettingsUIDeviceConfig;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settings.overlay.FeatureFactory;
@@ -35,13 +36,8 @@
 import com.android.settings.search.BaseSearchIndexProvider;
 import com.android.settings.slices.SlicePreferenceController;
 import com.android.settingslib.bluetooth.HearingAidStatsLogUtils;
-import com.android.settingslib.core.AbstractPreferenceController;
-import com.android.settingslib.core.lifecycle.Lifecycle;
 import com.android.settingslib.search.SearchIndexable;
 
-import java.util.ArrayList;
-import java.util.List;
-
 @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
 public class ConnectedDeviceDashboardFragment extends DashboardFragment {
 
@@ -91,6 +87,10 @@
                             + ", action : "
                             + action);
         }
+        if (AudioSharingUtils.isFeatureEnabled()) {
+            use(AudioSharingDevicePreferenceController.class).init(this);
+        }
+        use(AvailableMediaDeviceGroupController.class).init(this);
         use(ConnectedDeviceGroupController.class).init(this);
         use(PreviouslyConnectedDevicePreferenceController.class).init(this);
         use(SlicePreferenceController.class)
@@ -112,31 +112,6 @@
         }
     }
 
-    @Override
-    protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
-        return buildPreferenceControllers(context, /* fragment= */ this, getSettingsLifecycle());
-    }
-
-    private static List<AbstractPreferenceController> buildPreferenceControllers(
-            Context context,
-            @Nullable ConnectedDeviceDashboardFragment fragment,
-            @Nullable Lifecycle lifecycle) {
-        final List<AbstractPreferenceController> controllers = new ArrayList<>();
-        AbstractPreferenceController availableMediaController =
-                FeatureFactory.getFeatureFactory()
-                        .getAudioSharingFeatureProvider()
-                        .createAvailableMediaDeviceGroupController(context, fragment, lifecycle);
-        controllers.add(availableMediaController);
-        AbstractPreferenceController audioSharingController =
-                FeatureFactory.getFeatureFactory()
-                        .getAudioSharingFeatureProvider()
-                        .createAudioSharingDevicePreferenceController(context, fragment, lifecycle);
-        if (audioSharingController != null) {
-            controllers.add(audioSharingController);
-        }
-        return controllers;
-    }
-
     @VisibleForTesting
     boolean isAlwaysDiscoverable(String callingAppPackageName, String action) {
         return TextUtils.equals(SLICE_ACTION, action)
@@ -147,12 +122,5 @@
 
     /** For Search. */
     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
-            new BaseSearchIndexProvider(R.xml.connected_devices) {
-                @Override
-                public List<AbstractPreferenceController> createPreferenceControllers(
-                        Context context) {
-                    return buildPreferenceControllers(
-                            context, /* fragment= */ null, /* lifecycle= */ null);
-                }
-            };
+            new BaseSearchIndexProvider(R.xml.connected_devices);
 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingActivity.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingActivity.java
new file mode 100644
index 0000000..1ec53f9
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import android.os.Bundle;
+
+import com.android.settings.SettingsActivity;
+
+public class AudioSharingActivity extends SettingsActivity {
+    @Override
+    protected void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        if (!AudioSharingUtils.isFeatureEnabled()) {
+            finish();
+        }
+    }
+
+    @Override
+    protected boolean isValidFragment(String fragmentName) {
+        return AudioSharingDashboardFragment.class.getName().equals(fragmentName);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBasePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBasePreferenceController.java
new file mode 100644
index 0000000..e933e41
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBasePreferenceController.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+public abstract class AudioSharingBasePreferenceController extends BasePreferenceController
+        implements DefaultLifecycleObserver {
+    private static final String TAG = "AudioSharingBasePreferenceController";
+
+    private final BluetoothAdapter mBluetoothAdapter;
+    @Nullable private final LocalBluetoothManager mBtManager;
+    @Nullable private final LocalBluetoothProfileManager mProfileManager;
+    @Nullable protected final LocalBluetoothLeBroadcast mBroadcast;
+    @Nullable protected Preference mPreference;
+
+    public AudioSharingBasePreferenceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        mBtManager = Utils.getLocalBtManager(context);
+        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
+        mBroadcast = mProfileManager == null ? null : mProfileManager.getLeAudioBroadcastProfile();
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreference = screen.findPreference(getPreferenceKey());
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        updateVisibility();
+    }
+
+    /** Update the visibility of the preference. */
+    protected void updateVisibility() {
+        if (mPreference == null) {
+            Log.d(TAG, "Skip updateVisibility, null preference");
+            return;
+        }
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            if (!isAvailable()) {
+                                Log.w(TAG, "Skip updateVisibility, unavailable preference");
+                                AudioSharingUtils.postOnMainThread(
+                                        mContext,
+                                        () -> { // Check nullability to pass NullAway check
+                                            if (mPreference != null) {
+                                                mPreference.setVisible(false);
+                                            }
+                                        });
+                                return;
+                            }
+                            boolean isBtOn = isBluetoothStateOn();
+                            boolean isProfileReady =
+                                    AudioSharingUtils.isAudioSharingProfileReady(mProfileManager);
+                            boolean isBroadcasting = isBroadcasting();
+                            boolean isVisible = isBtOn && isProfileReady && isBroadcasting;
+                            Log.d(
+                                    TAG,
+                                    "updateVisibility, isBtOn = "
+                                            + isBtOn
+                                            + ", isProfileReady = "
+                                            + isProfileReady
+                                            + ", isBroadcasting = "
+                                            + isBroadcasting);
+                            AudioSharingUtils.postOnMainThread(
+                                    mContext,
+                                    () -> { // Check nullability to pass NullAway check
+                                        if (mPreference != null) {
+                                            mPreference.setVisible(isVisible);
+                                        }
+                                    });
+                        });
+    }
+
+    /**
+     * Triggered when {@link AudioSharingDashboardFragment} receive onAudioSharingProfilesConnected
+     * callbacks.
+     */
+    protected void onAudioSharingProfilesConnected() {}
+
+    protected boolean isBroadcasting() {
+        return mBroadcast != null && mBroadcast.isEnabled(null);
+    }
+
+    protected boolean isBluetoothStateOn() {
+        return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBluetoothDeviceUpdater.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBluetoothDeviceUpdater.java
new file mode 100644
index 0000000..50517fb
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBluetoothDeviceUpdater.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import com.android.settings.bluetooth.BluetoothDeviceUpdater;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+public class AudioSharingBluetoothDeviceUpdater extends BluetoothDeviceUpdater
+        implements Preference.OnPreferenceClickListener {
+
+    private static final String TAG = "AudioSharingBluetoothDeviceUpdater";
+
+    private static final String PREF_KEY = "audio_sharing_bt";
+
+    @Nullable private LocalBluetoothManager mLocalBtManager;
+
+    public AudioSharingBluetoothDeviceUpdater(
+            Context context,
+            DevicePreferenceCallback devicePreferenceCallback,
+            int metricsCategory) {
+        super(context, devicePreferenceCallback, metricsCategory);
+        mLocalBtManager = Utils.getLocalBluetoothManager(context);
+    }
+
+    @Override
+    public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) {
+        boolean isFilterMatched = false;
+        if (isDeviceConnected(cachedDevice) && isDeviceInCachedDevicesList(cachedDevice)) {
+            // If device is LE audio device and has a broadcast source,
+            // it would show in audio sharing devices group.
+            if (AudioSharingUtils.isFeatureEnabled()
+                    && cachedDevice.isConnectedLeAudioDevice()
+                    && BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, mLocalBtManager)) {
+                isFilterMatched = true;
+            }
+        }
+        Log.d(
+                TAG,
+                "isFilterMatched() device : "
+                        + cachedDevice.getName()
+                        + ", isFilterMatched : "
+                        + isFilterMatched);
+        return isFilterMatched;
+    }
+
+    @Override
+    public boolean onPreferenceClick(Preference preference) {
+        mMetricsFeatureProvider.logClickedPreference(preference, mMetricsCategory);
+        return true;
+    }
+
+    @Override
+    protected String getPreferenceKey() {
+        return PREF_KEY;
+    }
+
+    @Override
+    protected String getLogTag() {
+        return TAG;
+    }
+
+    @Override
+    protected void update(CachedBluetoothDevice cachedBluetoothDevice) {
+        super.update(cachedBluetoothDevice);
+        Log.d(TAG, "Map : " + mPreferenceMap);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceController.java
new file mode 100644
index 0000000..84b769d
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceController.java
@@ -0,0 +1,252 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.TwoStatePreference;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.TogglePreferenceController;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class AudioSharingCompatibilityPreferenceController extends TogglePreferenceController
+        implements DefaultLifecycleObserver, LocalBluetoothProfileManager.ServiceListener {
+
+    private static final String TAG = "AudioSharingCompatibilityPrefController";
+
+    private static final String PREF_KEY = "audio_sharing_stream_compatibility";
+
+    @Nullable private final LocalBluetoothManager mBtManager;
+    @Nullable private final LocalBluetoothProfileManager mProfileManager;
+    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+    @Nullable private TwoStatePreference mPreference;
+    private final Executor mExecutor;
+    private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
+
+    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+            new BluetoothLeBroadcast.Callback() {
+                @Override
+                public void onBroadcastStarted(int reason, int broadcastId) {
+                    Log.d(
+                            TAG,
+                            "onBroadcastStarted(), reason = "
+                                    + reason
+                                    + ", broadcastId = "
+                                    + broadcastId);
+                    updateEnabled();
+                }
+
+                @Override
+                public void onBroadcastStartFailed(int reason) {}
+
+                @Override
+                public void onBroadcastMetadataChanged(
+                        int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {}
+
+                @Override
+                public void onBroadcastStopped(int reason, int broadcastId) {
+                    Log.d(
+                            TAG,
+                            "onBroadcastStopped(), reason = "
+                                    + reason
+                                    + ", broadcastId = "
+                                    + broadcastId);
+                    updateEnabled();
+                }
+
+                @Override
+                public void onBroadcastStopFailed(int reason) {}
+
+                @Override
+                public void onBroadcastUpdated(int reason, int broadcastId) {}
+
+                @Override
+                public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
+
+                @Override
+                public void onPlaybackStarted(int reason, int broadcastId) {}
+
+                @Override
+                public void onPlaybackStopped(int reason, int broadcastId) {}
+            };
+
+    public AudioSharingCompatibilityPreferenceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mBtManager = Utils.getLocalBtManager(context);
+        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
+        mBroadcast = mProfileManager == null ? null : mProfileManager.getLeAudioBroadcastProfile();
+        mExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip register callbacks, feature not support");
+            return;
+        }
+        if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            Log.d(TAG, "Skip register callbacks, profile not ready");
+            if (mProfileManager != null) {
+                mProfileManager.addServiceListener(this);
+            }
+            return;
+        }
+        registerCallbacks();
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip unregister callbacks, feature not support");
+            return;
+        }
+        if (mProfileManager != null) {
+            mProfileManager.removeServiceListener(this);
+        }
+        if (mBroadcast == null || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            Log.d(TAG, "Skip unregister callbacks, profile not ready");
+            return;
+        }
+        if (mCallbacksRegistered.get()) {
+            Log.d(TAG, "Unregister callbacks");
+            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+            mCallbacksRegistered.set(false);
+        }
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreference = screen.findPreference(getPreferenceKey());
+        updateEnabled();
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return PREF_KEY;
+    }
+
+    @Override
+    public boolean isChecked() {
+        return mBroadcast != null && mBroadcast.getImproveCompatibility();
+    }
+
+    @Override
+    public boolean setChecked(boolean isChecked) {
+        if (mBroadcast == null || mBroadcast.getImproveCompatibility() == isChecked) {
+            if (mBroadcast != null) {
+                Log.d(TAG, "Skip setting improveCompatibility, unchanged");
+            }
+            return false;
+        }
+        mBroadcast.setImproveCompatibility(isChecked);
+        // TODO: call updateBroadcast once framework change ready.
+        return true;
+    }
+
+    @Override
+    public void onServiceConnected() {
+        if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            registerCallbacks();
+            AudioSharingUtils.postOnMainThread(
+                    mContext,
+                    () -> {
+                        if (mPreference != null) {
+                            updateState(mPreference);
+                        }
+                        updateEnabled();
+                    });
+            if (mProfileManager != null) {
+                mProfileManager.removeServiceListener(this);
+            }
+        }
+    }
+
+    @Override
+    public void onServiceDisconnected() {
+        // Do nothing
+    }
+
+    @Override
+    public int getSliceHighlightMenuRes() {
+        return 0;
+    }
+
+    /** Test only: set callbacks registration state for test setup. */
+    @VisibleForTesting
+    public void setCallbacksRegistered(boolean registered) {
+        mCallbacksRegistered.set(registered);
+    }
+
+    private void registerCallbacks() {
+        if (mBroadcast == null) {
+            Log.d(TAG, "Skip register callbacks, profile not ready");
+            return;
+        }
+        if (!mCallbacksRegistered.get()) {
+            Log.d(TAG, "Register callbacks");
+            mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
+            mCallbacksRegistered.set(true);
+        }
+    }
+
+    private void updateEnabled() {
+        int disabledDescriptionRes =
+                R.string.audio_sharing_stream_compatibility_disabled_description;
+        int descriptionRes = R.string.audio_sharing_stream_compatibility_description;
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            boolean isBroadcasting = AudioSharingUtils.isBroadcasting(mBtManager);
+                            AudioSharingUtils.postOnMainThread(
+                                    mContext,
+                                    () -> {
+                                        if (mPreference != null) {
+                                            mPreference.setEnabled(!isBroadcasting);
+                                            mPreference.setSummary(
+                                                    isBroadcasting
+                                                            ? mContext.getString(
+                                                                    disabledDescriptionRes)
+                                                            : mContext.getString(descriptionRes));
+                                        }
+                                    });
+                        });
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingConfirmDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingConfirmDialogFragment.java
new file mode 100644
index 0000000..9dd466d
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingConfirmDialogFragment.java
@@ -0,0 +1,71 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+public class AudioSharingConfirmDialogFragment extends InstrumentedDialogFragment {
+    private static final String TAG = "AudioSharingConfirmDialog";
+
+    @Override
+    public int getMetricsCategory() {
+        // TODO: add metrics category.
+        return 0;
+    }
+
+    /**
+     * Display the {@link AudioSharingConfirmDialogFragment} dialog.
+     *
+     * @param host The Fragment this dialog will be hosted.
+     */
+    public static void show(Fragment host) {
+        if (!AudioSharingUtils.isFeatureEnabled()) return;
+        FragmentManager manager = host.getChildFragmentManager();
+        AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
+        if (dialog != null) {
+            Log.d(TAG, "Dialog is showing, return.");
+            return;
+        }
+        Log.d(TAG, "Show up the confirm dialog.");
+        AudioSharingConfirmDialogFragment dialogFrag = new AudioSharingConfirmDialogFragment();
+        dialogFrag.show(manager, TAG);
+    }
+
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        AlertDialog dialog =
+                AudioSharingDialogFactory.newBuilder(getActivity())
+                        .setTitle(R.string.audio_sharing_confirm_dialog_title)
+                        .setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
+                        .setIsCustomBodyEnabled(true)
+                        .setCustomMessage(R.string.audio_sharing_comfirm_dialog_content)
+                        .setPositiveButton(com.android.settings.R.string.okay, (d, w) -> dismiss())
+                        .build();
+        dialog.setCanceledOnTouchOutside(true);
+        return dialog;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
new file mode 100644
index 0000000..275d197
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.widget.SettingsMainSwitchBar;
+
+public class AudioSharingDashboardFragment extends DashboardFragment
+        implements AudioSharingSwitchBarController.OnAudioSharingStateChangedListener {
+    private static final String TAG = "AudioSharingDashboardFrag";
+
+    SettingsMainSwitchBar mMainSwitchBar;
+    private AudioSharingSwitchBarController mSwitchBarController;
+    private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController;
+    private CallsAndAlarmsPreferenceController mCallsAndAlarmsPreferenceController;
+    private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController;
+    private AudioStreamsCategoryController mAudioStreamsCategoryController;
+
+    public AudioSharingDashboardFragment() {
+        super();
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.AUDIO_SHARING_SETTINGS;
+    }
+
+    @Override
+    protected String getLogTag() {
+        return TAG;
+    }
+
+    @Override
+    public int getHelpResource() {
+        return R.string.help_url_audio_sharing;
+    }
+
+    @Override
+    protected int getPreferenceScreenResId() {
+        return R.xml.bluetooth_le_audio_sharing;
+    }
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        mAudioSharingDeviceVolumeGroupController =
+                use(AudioSharingDeviceVolumeGroupController.class);
+        mAudioSharingDeviceVolumeGroupController.init(this);
+        mCallsAndAlarmsPreferenceController = use(CallsAndAlarmsPreferenceController.class);
+        mCallsAndAlarmsPreferenceController.init(this);
+        mAudioSharingPlaySoundPreferenceController =
+                use(AudioSharingPlaySoundPreferenceController.class);
+        mAudioStreamsCategoryController = use(AudioStreamsCategoryController.class);
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        // Assume we are in a SettingsActivity. This is only safe because we currently use
+        // SettingsActivity as base for all preference fragments.
+        final SettingsActivity activity = (SettingsActivity) getActivity();
+        mMainSwitchBar = activity.getSwitchBar();
+        mMainSwitchBar.setTitle(getText(R.string.audio_sharing_switch_title));
+        mSwitchBarController = new AudioSharingSwitchBarController(activity, mMainSwitchBar, this);
+        mSwitchBarController.init(this);
+        getSettingsLifecycle().addObserver(mSwitchBarController);
+        mMainSwitchBar.show();
+    }
+
+    @Override
+    public void onAudioSharingStateChanged() {
+        updateVisibilityForAttachedPreferences();
+    }
+
+    @Override
+    public void onAudioSharingProfilesConnected() {
+        onProfilesConnectedForAttachedPreferences();
+    }
+
+    private void updateVisibilityForAttachedPreferences() {
+        mAudioSharingDeviceVolumeGroupController.updateVisibility();
+        mCallsAndAlarmsPreferenceController.updateVisibility();
+        mAudioSharingPlaySoundPreferenceController.updateVisibility();
+        mAudioStreamsCategoryController.updateVisibility();
+    }
+
+    private void onProfilesConnectedForAttachedPreferences() {
+        mAudioSharingDeviceVolumeGroupController.onAudioSharingProfilesConnected();
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java
new file mode 100644
index 0000000..0b6b8c9
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceAdapter.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+
+import java.util.List;
+
+public class AudioSharingDeviceAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+    private static final String TAG = "AudioSharingDeviceAdapter";
+
+    private final Context mContext;
+    private final List<AudioSharingDeviceItem> mDevices;
+    private final OnClickListener mOnClickListener;
+    private final ActionType mType;
+
+    public AudioSharingDeviceAdapter(
+            @NonNull Context context,
+            @NonNull List<AudioSharingDeviceItem> devices,
+            @NonNull OnClickListener listener,
+            @NonNull ActionType type) {
+        mContext = context;
+        mDevices = devices;
+        mOnClickListener = listener;
+        mType = type;
+    }
+
+    /**
+     * The action type when user click on the item.
+     *
+     * <p>We choose the item text based on this type.
+     */
+    public enum ActionType {
+        // Click on the item will add the item to audio sharing
+        SHARE,
+        // Click on the item will remove the item from audio sharing
+        REMOVE,
+    }
+
+    private class AudioSharingDeviceViewHolder extends RecyclerView.ViewHolder {
+        private final Button mButtonView;
+
+        AudioSharingDeviceViewHolder(View view) {
+            super(view);
+            mButtonView = view.findViewById(R.id.device_button);
+        }
+
+        public void bindView(int position) {
+            if (mButtonView != null) {
+                String btnText = switch (mType) {
+                    case SHARE ->
+                            mContext.getString(
+                                    R.string.audio_sharing_share_with_button_label,
+                                    mDevices.get(position).getName());
+                    case REMOVE ->
+                            mContext.getString(
+                                    R.string.audio_sharing_disconnect_device_button_label,
+                                    mDevices.get(position).getName());
+                };
+                mButtonView.setText(btnText);
+                mButtonView.setOnClickListener(
+                        v -> mOnClickListener.onClick(mDevices.get(position)));
+            } else {
+                Log.w(TAG, "bind view skipped due to button view is null");
+            }
+        }
+    }
+
+    @Override
+    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        View view =
+                LayoutInflater.from(parent.getContext())
+                        .inflate(R.layout.audio_sharing_device_item, parent, false);
+        return new AudioSharingDeviceViewHolder(view);
+    }
+
+    @Override
+    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+        ((AudioSharingDeviceViewHolder) holder).bindView(position);
+    }
+
+    @Override
+    public int getItemCount() {
+        return mDevices.size();
+    }
+
+    public interface OnClickListener {
+        /** Called when an item has been clicked. */
+        void onClick(AudioSharingDeviceItem item);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItem.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItem.java
new file mode 100644
index 0000000..5998e30
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItem.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public final class AudioSharingDeviceItem implements Parcelable {
+    private final String mName;
+    private final int mGroupId;
+    private final boolean mIsActive;
+
+    public AudioSharingDeviceItem(String name, int groupId, boolean isActive) {
+        mName = name;
+        mGroupId = groupId;
+        mIsActive = isActive;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public int getGroupId() {
+        return mGroupId;
+    }
+
+    public boolean isActive() {
+        return mIsActive;
+    }
+
+    public AudioSharingDeviceItem(Parcel in) {
+        mName = in.readString();
+        mGroupId = in.readInt();
+        mIsActive = in.readBoolean();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mName);
+        dest.writeInt(mGroupId);
+        dest.writeBoolean(mIsActive);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Creator<AudioSharingDeviceItem> CREATOR =
+            new Creator<AudioSharingDeviceItem>() {
+                @Override
+                public AudioSharingDeviceItem createFromParcel(Parcel in) {
+                    return new AudioSharingDeviceItem(in);
+                }
+
+                @Override
+                public AudioSharingDeviceItem[] newArray(int size) {
+                    return new AudioSharingDeviceItem[size];
+                }
+            };
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java
new file mode 100644
index 0000000..51a8e11
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceController.java
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.SettingsActivity;
+import com.android.settings.bluetooth.BluetoothDeviceUpdater;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settingslib.bluetooth.A2dpProfile;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.HeadsetProfile;
+import com.android.settingslib.bluetooth.HearingAidProfile;
+import com.android.settingslib.bluetooth.LeAudioProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+
+import java.util.Locale;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class AudioSharingDevicePreferenceController extends BasePreferenceController
+        implements DefaultLifecycleObserver,
+                DevicePreferenceCallback,
+                BluetoothCallback,
+                LocalBluetoothProfileManager.ServiceListener {
+    private static final boolean DEBUG = BluetoothUtils.D;
+
+    private static final String TAG = "AudioSharingDevicePrefController";
+    private static final String KEY = "audio_sharing_device_list";
+    private static final String KEY_AUDIO_SHARING_SETTINGS =
+            "connected_device_audio_sharing_settings";
+
+    @Nullable private final LocalBluetoothManager mBtManager;
+    @Nullable private final CachedBluetoothDeviceManager mDeviceManager;
+    @Nullable private final BluetoothEventManager mEventManager;
+    @Nullable private final LocalBluetoothProfileManager mProfileManager;
+    @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
+    private final Executor mExecutor;
+    @Nullable private PreferenceGroup mPreferenceGroup;
+    @Nullable private Preference mAudioSharingSettingsPreference;
+    @Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
+    @Nullable private DashboardFragment mFragment;
+    @Nullable private AudioSharingDialogHandler mDialogHandler;
+    private AtomicBoolean mIntentHandled = new AtomicBoolean(false);
+
+    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+            new BluetoothLeBroadcastAssistant.Callback() {
+                @Override
+                public void onSearchStarted(int reason) {}
+
+                @Override
+                public void onSearchStartFailed(int reason) {}
+
+                @Override
+                public void onSearchStopped(int reason) {}
+
+                @Override
+                public void onSearchStopFailed(int reason) {}
+
+                @Override
+                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
+
+                @Override
+                public void onSourceAdded(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceAddFailed(
+                        @NonNull BluetoothDevice sink,
+                        @NonNull BluetoothLeBroadcastMetadata source,
+                        int reason) {
+                    AudioSharingUtils.toastMessage(
+                            mContext,
+                            String.format(
+                                    Locale.US,
+                                    "Fail to add source to %s reason %d",
+                                    sink.getAddress(),
+                                    reason));
+                }
+
+                @Override
+                public void onSourceModified(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceModifyFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceRemoved(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {
+                    Log.d(TAG, "onSourceRemoved: update sharing device list.");
+                    if (mBluetoothDeviceUpdater != null) {
+                        mBluetoothDeviceUpdater.forceUpdate();
+                    }
+                }
+
+                @Override
+                public void onSourceRemoveFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {
+                    AudioSharingUtils.toastMessage(
+                            mContext,
+                            String.format(
+                                    Locale.US,
+                                    "Fail to remove source from %s reason %d",
+                                    sink.getAddress(),
+                                    reason));
+                }
+
+                @Override
+                public void onReceiveStateChanged(
+                        @NonNull BluetoothDevice sink,
+                        int sourceId,
+                        @NonNull BluetoothLeBroadcastReceiveState state) {
+                    if (BluetoothUtils.isConnected(state)) {
+                        Log.d(TAG, "onSourceAdded: update sharing device list.");
+                        if (mBluetoothDeviceUpdater != null) {
+                            mBluetoothDeviceUpdater.forceUpdate();
+                        }
+                        if (mDeviceManager != null && mDialogHandler != null) {
+                            CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(sink);
+                            if (cachedDevice != null) {
+                                mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
+                            }
+                        }
+                    }
+                }
+            };
+
+    public AudioSharingDevicePreferenceController(Context context) {
+        super(context, KEY);
+        mBtManager = Utils.getLocalBtManager(mContext);
+        mEventManager = mBtManager == null ? null : mBtManager.getEventManager();
+        mDeviceManager = mBtManager == null ? null : mBtManager.getCachedDeviceManager();
+        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
+        mAssistant =
+                mProfileManager == null
+                        ? null
+                        : mProfileManager.getLeAudioBroadcastAssistantProfile();
+        mExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip onStart(), feature is not supported.");
+            return;
+        }
+        if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)
+                && mProfileManager != null) {
+            Log.d(TAG, "Register profile service listener");
+            mProfileManager.addServiceListener(this);
+        }
+        if (mEventManager == null
+                || mAssistant == null
+                || mDialogHandler == null
+                || mBluetoothDeviceUpdater == null) {
+            Log.d(TAG, "Skip onStart(), profile is not ready.");
+            return;
+        }
+        Log.d(TAG, "onStart() Register callbacks.");
+        mEventManager.registerCallback(this);
+        mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+        mDialogHandler.registerCallbacks(mExecutor);
+        mBluetoothDeviceUpdater.registerCallback();
+        mBluetoothDeviceUpdater.refreshPreference();
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip onStop(), feature is not supported.");
+            return;
+        }
+        if (mProfileManager != null) {
+            mProfileManager.removeServiceListener(this);
+        }
+        if (mEventManager == null
+                || mAssistant == null
+                || mDialogHandler == null
+                || mBluetoothDeviceUpdater == null) {
+            Log.d(TAG, "Skip onStop(), profile is not ready.");
+            return;
+        }
+        Log.d(TAG, "onStop() Unregister callbacks.");
+        mEventManager.unregisterCallback(this);
+        mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+        mDialogHandler.unregisterCallbacks();
+        mBluetoothDeviceUpdater.unregisterCallback();
+    }
+
+    @Override
+    public void onServiceConnected() {
+        if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            if (mProfileManager != null) {
+                mProfileManager.removeServiceListener(this);
+            }
+            if (!mIntentHandled.get()) {
+                Log.d(TAG, "onServiceConnected: handleDeviceClickFromIntent");
+                handleDeviceClickFromIntent();
+                mIntentHandled.set(true);
+            }
+        }
+    }
+
+    @Override
+    public void onServiceDisconnected() {
+        // Do nothing
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreferenceGroup = screen.findPreference(KEY);
+        if (mPreferenceGroup != null) {
+            mAudioSharingSettingsPreference =
+                    mPreferenceGroup.findPreference(KEY_AUDIO_SHARING_SETTINGS);
+            mPreferenceGroup.setVisible(false);
+        }
+        if (mAudioSharingSettingsPreference != null) {
+            mAudioSharingSettingsPreference.setVisible(false);
+        }
+
+        if (isAvailable()) {
+            if (mBluetoothDeviceUpdater != null) {
+                mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
+                mBluetoothDeviceUpdater.forceUpdate();
+            }
+            if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+                if (!mIntentHandled.get()) {
+                    Log.d(TAG, "displayPreference: profile ready, handleDeviceClickFromIntent");
+                    handleDeviceClickFromIntent();
+                    mIntentHandled.set(true);
+                }
+            }
+        }
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AudioSharingUtils.isFeatureEnabled() && mBluetoothDeviceUpdater != null
+                ? AVAILABLE_UNSEARCHABLE
+                : UNSUPPORTED_ON_DEVICE;
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY;
+    }
+
+    @Override
+    public void onDeviceAdded(Preference preference) {
+        if (mPreferenceGroup != null) {
+            if (mPreferenceGroup.getPreferenceCount() == 1) {
+                mPreferenceGroup.setVisible(true);
+                if (mAudioSharingSettingsPreference != null) {
+                    mAudioSharingSettingsPreference.setVisible(true);
+                }
+            }
+            mPreferenceGroup.addPreference(preference);
+        }
+    }
+
+    @Override
+    public void onDeviceRemoved(Preference preference) {
+        if (mPreferenceGroup != null) {
+            mPreferenceGroup.removePreference(preference);
+            if (mPreferenceGroup.getPreferenceCount() == 1) {
+                mPreferenceGroup.setVisible(false);
+                if (mAudioSharingSettingsPreference != null) {
+                    mAudioSharingSettingsPreference.setVisible(false);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onProfileConnectionStateChanged(
+            @NonNull CachedBluetoothDevice cachedDevice,
+            @ConnectionState int state,
+            int bluetoothProfile) {
+        if (mDialogHandler == null || mAssistant == null || mFragment == null) {
+            Log.d(TAG, "Ignore onProfileConnectionStateChanged, not init correctly");
+            return;
+        }
+        if (!isMediaDevice(cachedDevice)) {
+            Log.d(TAG, "Ignore onProfileConnectionStateChanged, not a media device");
+            return;
+        }
+        // Close related dialogs if the BT remote device is disconnected.
+        if (state == BluetoothAdapter.STATE_DISCONNECTED) {
+            boolean isLeAudioSupported = AudioSharingUtils.isLeAudioSupported(cachedDevice);
+            if (isLeAudioSupported
+                    && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
+                mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
+                return;
+            }
+            if (!isLeAudioSupported && !cachedDevice.isConnected()) {
+                mDialogHandler.closeOpeningDialogsForNonLeaDevice(cachedDevice);
+                return;
+            }
+        }
+        if (state != BluetoothAdapter.STATE_CONNECTED || !cachedDevice.getDevice().isConnected()) {
+            Log.d(TAG, "Ignore onProfileConnectionStateChanged, not connected state");
+            return;
+        }
+        handleOnProfileStateChanged(cachedDevice, bluetoothProfile);
+    }
+
+    /**
+     * Initialize the controller.
+     *
+     * @param fragment The fragment to provide the context and metrics category for {@link
+     *     AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
+     */
+    public void init(DashboardFragment fragment) {
+        mFragment = fragment;
+        mBluetoothDeviceUpdater =
+                new AudioSharingBluetoothDeviceUpdater(
+                        fragment.getContext(),
+                        AudioSharingDevicePreferenceController.this,
+                        fragment.getMetricsCategory());
+        mDialogHandler = new AudioSharingDialogHandler(mContext, fragment);
+    }
+
+    @VisibleForTesting
+    public void setBluetoothDeviceUpdater(@Nullable BluetoothDeviceUpdater bluetoothDeviceUpdater) {
+        mBluetoothDeviceUpdater = bluetoothDeviceUpdater;
+    }
+
+    @VisibleForTesting
+    public void setDialogHandler(@Nullable AudioSharingDialogHandler dialogHandler) {
+        mDialogHandler = dialogHandler;
+    }
+
+    @VisibleForTesting
+    public void setHostFragment(@Nullable DashboardFragment fragment) {
+        mFragment = fragment;
+    }
+
+    /** Test only: set intent handle state for test. */
+    @VisibleForTesting
+    public void setIntentHandled(boolean handled) {
+        mIntentHandled.set(handled);
+    }
+
+    private void handleOnProfileStateChanged(
+            @NonNull CachedBluetoothDevice cachedDevice, int bluetoothProfile) {
+        boolean isLeAudioSupported = AudioSharingUtils.isLeAudioSupported(cachedDevice);
+        // For eligible (LE audio) remote device, we only check its connected LE audio assistant
+        // profile.
+        if (isLeAudioSupported
+                && bluetoothProfile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
+            Log.d(
+                    TAG,
+                    "Ignore onProfileConnectionStateChanged, not the le assistant profile for"
+                            + " le audio device");
+            return;
+        }
+        boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile);
+        // For ineligible (non LE audio) remote device, we only check its first connected profile.
+        if (!isLeAudioSupported && !isFirstConnectedProfile) {
+            Log.d(
+                    TAG,
+                    "Ignore onProfileConnectionStateChanged, not the first connected profile for"
+                            + " non le audio device");
+            return;
+        }
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "Start handling onProfileConnectionStateChanged for "
+                            + cachedDevice.getDevice().getAnonymizedAddress());
+        }
+        // Check nullability to pass NullAway check
+        if (mDialogHandler != null) {
+            mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ false);
+        }
+    }
+
+    private boolean isMediaDevice(CachedBluetoothDevice cachedDevice) {
+        return cachedDevice.getConnectableProfiles().stream()
+                .anyMatch(
+                        profile ->
+                                profile instanceof A2dpProfile
+                                        || profile instanceof HearingAidProfile
+                                        || profile instanceof LeAudioProfile
+                                        || profile instanceof HeadsetProfile);
+    }
+
+    private boolean isFirstConnectedProfile(
+            CachedBluetoothDevice cachedDevice, int bluetoothProfile) {
+        return cachedDevice.getProfiles().stream()
+                .noneMatch(
+                        profile ->
+                                profile.getProfileId() != bluetoothProfile
+                                        && profile.getConnectionStatus(cachedDevice.getDevice())
+                                                == BluetoothProfile.STATE_CONNECTED);
+    }
+
+    /**
+     * Handle device click triggered by intent.
+     *
+     * <p>When user click device from BT QS dialog, BT QS will send intent to open {@link
+     * com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment} and handle device
+     * click event under some conditions.
+     *
+     * <p>This method will be called when displayPreference if the audio sharing profiles are ready.
+     * If the profiles are not ready when the preference display, this method will be called when
+     * onServiceConnected.
+     */
+    private void handleDeviceClickFromIntent() {
+        if (mFragment == null
+                || mFragment.getActivity() == null
+                || mFragment.getActivity().getIntent() == null) {
+            Log.d(TAG, "Skip handleDeviceClickFromIntent, fragment intent is null");
+            return;
+        }
+        Intent intent = mFragment.getActivity().getIntent();
+        Bundle args = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
+        BluetoothDevice device =
+                args == null
+                        ? null
+                        : args.getParcelable(EXTRA_BLUETOOTH_DEVICE, BluetoothDevice.class);
+        CachedBluetoothDevice cachedDevice =
+                (device == null || mDeviceManager == null)
+                        ? null
+                        : mDeviceManager.findDevice(device);
+        if (cachedDevice == null) {
+            Log.d(TAG, "Skip handleDeviceClickFromIntent, device is null");
+            return;
+        }
+        // Check nullability to pass NullAway check
+        if (device != null && !device.isConnected()) {
+            Log.d(TAG, "handleDeviceClickFromIntent: connect");
+            cachedDevice.connect();
+        } else if (mDialogHandler != null) {
+            Log.d(TAG, "handleDeviceClickFromIntent: trigger dialog handler");
+            mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ true);
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeControlUpdater.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeControlUpdater.java
new file mode 100644
index 0000000..257ae77
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeControlUpdater.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.media.AudioManager;
+import android.util.Log;
+import android.widget.SeekBar;
+
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import com.android.settings.bluetooth.BluetoothDevicePreference;
+import com.android.settings.bluetooth.BluetoothDeviceUpdater;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+
+public class AudioSharingDeviceVolumeControlUpdater extends BluetoothDeviceUpdater
+        implements Preference.OnPreferenceClickListener {
+
+    private static final String TAG = "AudioSharingDeviceVolumeControlUpdater";
+
+    private static final String PREF_KEY = "audio_sharing_volume_control";
+
+    @Nullable private final LocalBluetoothManager mBtManager;
+    @Nullable private final VolumeControlProfile mVolumeControl;
+
+    public AudioSharingDeviceVolumeControlUpdater(
+            Context context,
+            DevicePreferenceCallback devicePreferenceCallback,
+            int metricsCategory) {
+        super(context, devicePreferenceCallback, metricsCategory);
+        mBtManager = Utils.getLocalBluetoothManager(context);
+        mVolumeControl =
+                mBtManager == null
+                        ? null
+                        : mBtManager.getProfileManager().getVolumeControlProfile();
+    }
+
+    @Override
+    public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) {
+        boolean isFilterMatched = false;
+        if (isDeviceConnected(cachedDevice) && isDeviceInCachedDevicesList(cachedDevice)) {
+            // If device is LE audio device and in a sharing session on current sharing device,
+            // it would show in volume control group.
+            if (cachedDevice.isConnectedLeAudioDevice()
+                    && AudioSharingUtils.isBroadcasting(mBtManager)
+                    && BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, mBtManager)) {
+                isFilterMatched = true;
+            }
+        }
+        Log.d(
+                TAG,
+                "isFilterMatched() device : "
+                        + cachedDevice.getName()
+                        + ", isFilterMatched : "
+                        + isFilterMatched);
+        return isFilterMatched;
+    }
+
+    @Override
+    public boolean onPreferenceClick(Preference preference) {
+        return true;
+    }
+
+    @Override
+    protected void addPreference(CachedBluetoothDevice cachedDevice) {
+        if (cachedDevice == null) return;
+        final BluetoothDevice device = cachedDevice.getDevice();
+        if (!mPreferenceMap.containsKey(device)) {
+            SeekBar.OnSeekBarChangeListener listener =
+                    new SeekBar.OnSeekBarChangeListener() {
+                        @Override
+                        public void onProgressChanged(
+                                SeekBar seekBar, int progress, boolean fromUser) {}
+
+                        @Override
+                        public void onStartTrackingTouch(SeekBar seekBar) {}
+
+                        @Override
+                        public void onStopTrackingTouch(SeekBar seekBar) {
+                            int progress = seekBar.getProgress();
+                            int groupId = AudioSharingUtils.getGroupId(cachedDevice);
+                            if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
+                                    && groupId
+                                            == AudioSharingUtils.getFallbackActiveGroupId(
+                                                    mContext)) {
+                                // Set media stream volume for primary buds, audio manager will
+                                // update all buds volume in the audio sharing.
+                                setAudioManagerStreamVolume(progress);
+                            } else {
+                                // Set buds volume for other buds.
+                                setDeviceVolume(cachedDevice, progress);
+                            }
+                        }
+                    };
+            AudioSharingDeviceVolumePreference vPreference =
+                    new AudioSharingDeviceVolumePreference(mPrefContext, cachedDevice);
+            vPreference.initialize();
+            vPreference.setOnSeekBarChangeListener(listener);
+            vPreference.setKey(getPreferenceKey());
+            vPreference.setIcon(com.android.settingslib.R.drawable.ic_bt_untethered_earbuds);
+            vPreference.setTitle(cachedDevice.getName());
+            mPreferenceMap.put(device, vPreference);
+            mDevicePreferenceCallback.onDeviceAdded(vPreference);
+        }
+    }
+
+    @Override
+    protected String getPreferenceKey() {
+        return PREF_KEY;
+    }
+
+    @Override
+    protected String getLogTag() {
+        return TAG;
+    }
+
+    @Override
+    protected void update(CachedBluetoothDevice cachedBluetoothDevice) {
+        super.update(cachedBluetoothDevice);
+        Log.d(TAG, "Map : " + mPreferenceMap);
+    }
+
+    @Override
+    protected void addPreference(
+            CachedBluetoothDevice cachedDevice, @BluetoothDevicePreference.SortType int type) {}
+
+    @Override
+    protected void launchDeviceDetails(Preference preference) {}
+
+    @Override
+    public void refreshPreference() {}
+
+    private void setDeviceVolume(CachedBluetoothDevice cachedDevice, int progress) {
+        if (mVolumeControl != null) {
+            mVolumeControl.setDeviceVolume(
+                    cachedDevice.getDevice(), progress, /* isGroupOp= */ true);
+        }
+    }
+
+    private void setAudioManagerStreamVolume(int progress) {
+        int seekbarRange =
+                AudioSharingDeviceVolumePreference.MAX_VOLUME
+                        - AudioSharingDeviceVolumePreference.MIN_VOLUME;
+        try {
+            AudioManager audioManager = mContext.getSystemService(AudioManager.class);
+            int streamVolumeRange =
+                    audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
+                            - audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
+            int volume = Math.round((float) progress * streamVolumeRange / seekbarRange);
+            audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Fail to setAudioManagerStreamVolumeForFallbackDevice, error = " + e);
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeGroupController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeGroupController.java
new file mode 100644
index 0000000..4a067ac
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeGroupController.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID;
+
+import android.annotation.IntRange;
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothVolumeControl;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.media.AudioManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.bluetooth.BluetoothDeviceUpdater;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class AudioSharingDeviceVolumeGroupController extends AudioSharingBasePreferenceController
+        implements DevicePreferenceCallback {
+    private static final String TAG = "AudioSharingDeviceVolumeGroupController";
+    private static final String KEY = "audio_sharing_device_volume_group";
+
+    @Nullable private final LocalBluetoothManager mBtManager;
+    @Nullable private final LocalBluetoothProfileManager mProfileManager;
+    @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Nullable private final VolumeControlProfile mVolumeControl;
+    @Nullable private final ContentResolver mContentResolver;
+    @Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
+    private final Executor mExecutor;
+    private final ContentObserver mSettingsObserver;
+    @Nullable private PreferenceGroup mPreferenceGroup;
+    private List<AudioSharingDeviceVolumePreference> mVolumePreferences = new ArrayList<>();
+    private Map<Integer, Integer> mValueMap = new HashMap<Integer, Integer>();
+    private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
+
+    private BluetoothVolumeControl.Callback mVolumeControlCallback =
+            new BluetoothVolumeControl.Callback() {
+                @Override
+                public void onVolumeOffsetChanged(
+                        @NonNull BluetoothDevice device, int volumeOffset) {}
+
+                @Override
+                public void onDeviceVolumeChanged(
+                        @NonNull BluetoothDevice device,
+                        @IntRange(from = -255, to = 255) int volume) {
+                    CachedBluetoothDevice cachedDevice =
+                            mBtManager == null
+                                    ? null
+                                    : mBtManager.getCachedDeviceManager().findDevice(device);
+                    if (cachedDevice == null) return;
+                    int groupId = AudioSharingUtils.getGroupId(cachedDevice);
+                    mValueMap.put(groupId, volume);
+                    for (AudioSharingDeviceVolumePreference preference : mVolumePreferences) {
+                        if (preference.getCachedDevice() != null
+                                && AudioSharingUtils.getGroupId(preference.getCachedDevice())
+                                        == groupId) {
+                            // If the callback return invalid volume, try to
+                            // get the volume from AudioManager.STREAM_MUSIC
+                            int finalVolume = getAudioVolumeIfNeeded(volume);
+                            Log.d(
+                                    TAG,
+                                    "onDeviceVolumeChanged: set volume to "
+                                            + finalVolume
+                                            + " for "
+                                            + device.getAnonymizedAddress());
+                            mContext.getMainExecutor()
+                                    .execute(() -> preference.setProgress(finalVolume));
+                            break;
+                        }
+                    }
+                }
+            };
+
+    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+            new BluetoothLeBroadcastAssistant.Callback() {
+                @Override
+                public void onSearchStarted(int reason) {}
+
+                @Override
+                public void onSearchStartFailed(int reason) {}
+
+                @Override
+                public void onSearchStopped(int reason) {}
+
+                @Override
+                public void onSearchStopFailed(int reason) {}
+
+                @Override
+                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
+
+                @Override
+                public void onSourceAdded(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceAddFailed(
+                        @NonNull BluetoothDevice sink,
+                        @NonNull BluetoothLeBroadcastMetadata source,
+                        int reason) {}
+
+                @Override
+                public void onSourceModified(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceModifyFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceRemoved(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {
+                    Log.d(TAG, "onSourceRemoved: update volume list.");
+                    if (mBluetoothDeviceUpdater != null) {
+                        mBluetoothDeviceUpdater.forceUpdate();
+                    }
+                }
+
+                @Override
+                public void onSourceRemoveFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onReceiveStateChanged(
+                        @NonNull BluetoothDevice sink,
+                        int sourceId,
+                        @NonNull BluetoothLeBroadcastReceiveState state) {
+                    if (BluetoothUtils.isConnected(state)) {
+                        Log.d(TAG, "onReceiveStateChanged: synced, update volume list.");
+                        if (mBluetoothDeviceUpdater != null) {
+                            mBluetoothDeviceUpdater.forceUpdate();
+                        }
+                    }
+                }
+            };
+
+    public AudioSharingDeviceVolumeGroupController(Context context) {
+        super(context, KEY);
+        mBtManager = Utils.getLocalBtManager(mContext);
+        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
+        mAssistant =
+                mProfileManager == null
+                        ? null
+                        : mProfileManager.getLeAudioBroadcastAssistantProfile();
+        mVolumeControl = mProfileManager == null ? null : mProfileManager.getVolumeControlProfile();
+        mExecutor = Executors.newSingleThreadExecutor();
+        mContentResolver = context.getContentResolver();
+        mSettingsObserver = new SettingsObserver();
+    }
+
+    private class SettingsObserver extends ContentObserver {
+        SettingsObserver() {
+            super(new Handler(Looper.getMainLooper()));
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            Log.d(TAG, "onChange, fallback device group id has been changed");
+            for (AudioSharingDeviceVolumePreference preference : mVolumePreferences) {
+                preference.setOrder(getPreferenceOrderForDevice(preference.getCachedDevice()));
+            }
+        }
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        super.onStart(owner);
+        registerCallbacks();
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        super.onStop(owner);
+        unregisterCallbacks();
+    }
+
+    @Override
+    public void onDestroy(@NonNull LifecycleOwner owner) {
+        mVolumePreferences.clear();
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+
+        mPreferenceGroup = screen.findPreference(KEY);
+        if (mPreferenceGroup != null) {
+            mPreferenceGroup.setVisible(false);
+        }
+
+        if (isAvailable() && mBluetoothDeviceUpdater != null) {
+            mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
+            mBluetoothDeviceUpdater.forceUpdate();
+        }
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY;
+    }
+
+    @Override
+    public void onDeviceAdded(Preference preference) {
+        if (mPreferenceGroup != null) {
+            if (mPreferenceGroup.getPreferenceCount() == 0) {
+                mPreferenceGroup.setVisible(true);
+            }
+            mPreferenceGroup.addPreference(preference);
+        }
+        if (preference instanceof AudioSharingDeviceVolumePreference) {
+            var volumePref = (AudioSharingDeviceVolumePreference) preference;
+            CachedBluetoothDevice cachedDevice = volumePref.getCachedDevice();
+            volumePref.setOrder(getPreferenceOrderForDevice(cachedDevice));
+            mVolumePreferences.add(volumePref);
+            if (volumePref.getProgress() > 0) return;
+            int volume = mValueMap.getOrDefault(AudioSharingUtils.getGroupId(cachedDevice), -1);
+            // If the volume is invalid, try to get the volume from AudioManager.STREAM_MUSIC
+            int finalVolume = getAudioVolumeIfNeeded(volume);
+            Log.d(
+                    TAG,
+                    "onDeviceAdded: set volume to "
+                            + finalVolume
+                            + " for "
+                            + cachedDevice.getDevice().getAnonymizedAddress());
+            AudioSharingUtils.postOnMainThread(mContext, () -> volumePref.setProgress(finalVolume));
+        }
+    }
+
+    @Override
+    public void onDeviceRemoved(Preference preference) {
+        if (mPreferenceGroup != null) {
+            mPreferenceGroup.removePreference(preference);
+            if (mPreferenceGroup.getPreferenceCount() == 0) {
+                mPreferenceGroup.setVisible(false);
+            }
+        }
+        if (preference instanceof AudioSharingDeviceVolumePreference) {
+            var volumePref = (AudioSharingDeviceVolumePreference) preference;
+            if (mVolumePreferences.contains(volumePref)) {
+                mVolumePreferences.remove(volumePref);
+            }
+            CachedBluetoothDevice device = volumePref.getCachedDevice();
+            Log.d(
+                    TAG,
+                    "onDeviceRemoved: "
+                            + (device == null
+                                    ? "null"
+                                    : device.getDevice().getAnonymizedAddress()));
+        }
+    }
+
+    @Override
+    public void updateVisibility() {
+        if (mPreferenceGroup != null && mPreferenceGroup.getPreferenceCount() == 0) {
+            mPreferenceGroup.setVisible(false);
+            return;
+        }
+        super.updateVisibility();
+    }
+
+    @Override
+    public void onAudioSharingProfilesConnected() {
+        registerCallbacks();
+    }
+
+    /**
+     * Initialize the controller.
+     *
+     * @param fragment The fragment to provide the context and metrics category for {@link
+     *     AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
+     */
+    public void init(DashboardFragment fragment) {
+        mBluetoothDeviceUpdater =
+                new AudioSharingDeviceVolumeControlUpdater(
+                        fragment.getContext(),
+                        AudioSharingDeviceVolumeGroupController.this,
+                        fragment.getMetricsCategory());
+    }
+
+    @VisibleForTesting
+    public void setDeviceUpdater(@Nullable AudioSharingDeviceVolumeControlUpdater updater) {
+        mBluetoothDeviceUpdater = updater;
+    }
+
+    /** Test only: set callback registration status in tests. */
+    @VisibleForTesting
+    public void setCallbacksRegistered(boolean registered) {
+        mCallbacksRegistered.set(registered);
+    }
+
+    /** Test only: set volume map in tests. */
+    @VisibleForTesting
+    public void setVolumeMap(@Nullable Map<Integer, Integer> map) {
+        mValueMap.clear();
+        mValueMap.putAll(map);
+    }
+
+    /** Test only: set value for private preferenceGroup in tests. */
+    @VisibleForTesting
+    public void setPreferenceGroup(@Nullable PreferenceGroup group) {
+        mPreferenceGroup = group;
+        mPreference = group;
+    }
+
+    @VisibleForTesting
+    ContentObserver getSettingsObserver() {
+        return mSettingsObserver;
+    }
+
+    private void registerCallbacks() {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip registerCallbacks(). Feature is not available.");
+            return;
+        }
+        if (mAssistant == null
+                || mVolumeControl == null
+                || mBluetoothDeviceUpdater == null
+                || mContentResolver == null
+                || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            Log.d(TAG, "Skip registerCallbacks(). Profile is not ready.");
+            return;
+        }
+        if (!mCallbacksRegistered.get()) {
+            Log.d(TAG, "registerCallbacks()");
+            mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+            mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback);
+            mBluetoothDeviceUpdater.registerCallback();
+            mContentResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID),
+                    false,
+                    mSettingsObserver);
+            mCallbacksRegistered.set(true);
+        }
+    }
+
+    private void unregisterCallbacks() {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip unregister callbacks. Feature is not available.");
+            return;
+        }
+        if (mAssistant == null
+                || mVolumeControl == null
+                || mBluetoothDeviceUpdater == null
+                || mContentResolver == null
+                || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            Log.d(TAG, "Skip unregisterCallbacks(). Profile is not ready.");
+            return;
+        }
+        if (mCallbacksRegistered.get()) {
+            Log.d(TAG, "unregisterCallbacks()");
+            mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+            mVolumeControl.unregisterCallback(mVolumeControlCallback);
+            mBluetoothDeviceUpdater.unregisterCallback();
+            mContentResolver.unregisterContentObserver(mSettingsObserver);
+            mValueMap.clear();
+            mCallbacksRegistered.set(false);
+        }
+    }
+
+    private int getAudioVolumeIfNeeded(int volume) {
+        if (volume >= 0) return volume;
+        try {
+            AudioManager audioManager = mContext.getSystemService(AudioManager.class);
+            int max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+            int min = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
+            return Math.round(
+                    audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) * 255f / (max - min));
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Fail to fetch current music stream volume, error = " + e);
+            return volume;
+        }
+    }
+
+    private int getPreferenceOrderForDevice(@NonNull CachedBluetoothDevice cachedDevice) {
+        int groupId = AudioSharingUtils.getGroupId(cachedDevice);
+        // The fallback device rank first among the audio sharing device list.
+        return (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
+                        && groupId == AudioSharingUtils.getFallbackActiveGroupId(mContext))
+                ? 0
+                : 1;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumePreference.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumePreference.java
new file mode 100644
index 0000000..01afc02
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumePreference.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.content.Context;
+import android.widget.SeekBar;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.widget.SeekBarPreference;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+
+public class AudioSharingDeviceVolumePreference extends SeekBarPreference {
+    public static final int MIN_VOLUME = 0;
+    public static final int MAX_VOLUME = 255;
+
+    private final CachedBluetoothDevice mCachedDevice;
+    @Nullable protected SeekBar mSeekBar;
+
+    public AudioSharingDeviceVolumePreference(
+            Context context, @NonNull CachedBluetoothDevice device) {
+        super(context);
+        setLayoutResource(R.layout.preference_volume_slider);
+        mCachedDevice = device;
+    }
+
+    @NonNull
+    public CachedBluetoothDevice getCachedDevice() {
+        return mCachedDevice;
+    }
+
+    /**
+     * Initialize {@link AudioSharingDeviceVolumePreference}.
+     *
+     * <p>Need to be called after creating the preference.
+     */
+    public void initialize() {
+        setMax(MAX_VOLUME);
+        setMin(MIN_VOLUME);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFactory.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFactory.java
new file mode 100644
index 0000000..165beae
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFactory.java
@@ -0,0 +1,348 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+import androidx.appcompat.app.AlertDialog;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+
+import javax.annotation.CheckReturnValue;
+
+public class AudioSharingDialogFactory {
+    private static final String TAG = "AudioSharingDialogFactory";
+
+    /**
+     * Initializes a builder for the dialog to be shown for audio sharing.
+     *
+     * @param context The {@link Context} that will be used to create the dialog.
+     * @return A configurable builder for the dialog.
+     */
+    @NonNull
+    public static AudioSharingDialogFactory.DialogBuilder newBuilder(@NonNull Context context) {
+        return new AudioSharingDialogFactory.DialogBuilder(context);
+    }
+
+    /** Builder class with configurable options for the dialog to be shown for audio sharing. */
+    public static class DialogBuilder {
+        private Context mContext;
+        private AlertDialog.Builder mBuilder;
+        private View mCustomTitle;
+        private View mCustomBody;
+        private boolean mIsCustomBodyEnabled;
+
+        /**
+         * Private constructor for the dialog builder class. Should not be invoked directly;
+         * instead, use {@link AudioSharingDialogFactory#newBuilder(Context)}.
+         *
+         * @param context The {@link Context} that will be used to create the dialog.
+         */
+        private DialogBuilder(@NonNull Context context) {
+            mContext = context;
+            mBuilder = new AlertDialog.Builder(context);
+            LayoutInflater inflater = LayoutInflater.from(mBuilder.getContext());
+            mCustomTitle =
+                    inflater.inflate(R.layout.dialog_custom_title_audio_sharing, /* root= */ null);
+            mCustomBody =
+                    inflater.inflate(R.layout.dialog_custom_body_audio_sharing, /* parent= */ null);
+        }
+
+        /**
+         * Sets title of the dialog custom title.
+         *
+         * @param titleRes Resource ID of the string to be used for the dialog title.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setTitle(@StringRes int titleRes) {
+            TextView title = mCustomTitle.findViewById(R.id.title_text);
+            title.setText(titleRes);
+            return this;
+        }
+
+        /**
+         * Sets title of the dialog custom title.
+         *
+         * @param titleText The text to be used for the title.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setTitle(@NonNull CharSequence titleText) {
+            TextView title = mCustomTitle.findViewById(R.id.title_text);
+            title.setText(titleText);
+            return this;
+        }
+
+        /**
+         * Sets the title icon of the dialog custom title.
+         *
+         * @param iconRes The text to be used for the title.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setTitleIcon(@DrawableRes int iconRes) {
+            ImageView icon = mCustomTitle.findViewById(R.id.title_icon);
+            icon.setImageResource(iconRes);
+            return this;
+        }
+
+        /**
+         * Sets the message body of the dialog.
+         *
+         * @param messageRes Resource ID of the string to be used for the message body.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setMessage(@StringRes int messageRes) {
+            mBuilder.setMessage(messageRes);
+            return this;
+        }
+
+        /**
+         * Sets the message body of the dialog.
+         *
+         * @param message The text to be used for the message body.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setMessage(@NonNull CharSequence message) {
+            mBuilder.setMessage(message);
+            return this;
+        }
+
+        /** Whether to use custom body. */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setIsCustomBodyEnabled(
+                boolean isCustomBodyEnabled) {
+            mIsCustomBodyEnabled = isCustomBodyEnabled;
+            return this;
+        }
+
+        /**
+         * Sets the custom image of the dialog custom body.
+         *
+         * @param iconRes The text to be used for the title.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setCustomImage(@DrawableRes int iconRes) {
+            ImageView image = mCustomBody.findViewById(R.id.description_image);
+            image.setImageResource(iconRes);
+            image.setVisibility(View.VISIBLE);
+            return this;
+        }
+
+        /**
+         * Sets the custom message of the dialog custom body.
+         *
+         * @param messageRes Resource ID of the string to be used for the message body.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setCustomMessage(@StringRes int messageRes) {
+            TextView subTitle = mCustomBody.findViewById(R.id.description_text);
+            subTitle.setText(messageRes);
+            subTitle.setVisibility(View.VISIBLE);
+            return this;
+        }
+
+        /**
+         * Sets the custom message of the dialog custom body.
+         *
+         * @param message The text to be used for the custom message body.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setCustomMessage(
+                @NonNull CharSequence message) {
+            TextView subTitle = mCustomBody.findViewById(R.id.description_text);
+            subTitle.setText(message);
+            subTitle.setVisibility(View.VISIBLE);
+            return this;
+        }
+
+        /**
+         * Sets the custom device actions of the dialog custom body.
+         *
+         * @param adapter The adapter for device items to build dialog actions.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setCustomDeviceActions(
+                @NonNull AudioSharingDeviceAdapter adapter) {
+            RecyclerView recyclerView = mCustomBody.findViewById(R.id.device_btn_list);
+            recyclerView.setAdapter(adapter);
+            recyclerView.setLayoutManager(
+                    new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false));
+            recyclerView.setVisibility(View.VISIBLE);
+            return this;
+        }
+
+        /**
+         * Sets the positive button label and listener for the dialog.
+         *
+         * @param labelRes Resource ID of the string to be used for the positive button label.
+         * @param listener The listener to be invoked when the positive button is pressed.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setPositiveButton(
+                @StringRes int labelRes, @NonNull DialogInterface.OnClickListener listener) {
+            mBuilder.setPositiveButton(labelRes, listener);
+            return this;
+        }
+
+        /**
+         * Sets the positive button label and listener for the dialog.
+         *
+         * @param label The text to be used for the positive button label.
+         * @param listener The listener to be invoked when the positive button is pressed.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setPositiveButton(
+                @NonNull CharSequence label, @NonNull DialogInterface.OnClickListener listener) {
+            mBuilder.setPositiveButton(label, listener);
+            return this;
+        }
+
+        /**
+         * Sets the custom positive button label and listener for the dialog custom body.
+         *
+         * @param labelRes Resource ID of the string to be used for the positive button label.
+         * @param listener The listener to be invoked when the positive button is pressed.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setCustomPositiveButton(
+                @StringRes int labelRes, @NonNull View.OnClickListener listener) {
+            Button positiveBtn = mCustomBody.findViewById(R.id.positive_btn);
+            positiveBtn.setText(labelRes);
+            positiveBtn.setOnClickListener(listener);
+            positiveBtn.setVisibility(View.VISIBLE);
+            return this;
+        }
+
+        /**
+         * Sets the custom positive button label and listener for the dialog custom body.
+         *
+         * @param label The text to be used for the positive button label.
+         * @param listener The listener to be invoked when the positive button is pressed.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setCustomPositiveButton(
+                @NonNull CharSequence label, @NonNull View.OnClickListener listener) {
+            Button positiveBtn = mCustomBody.findViewById(R.id.positive_btn);
+            positiveBtn.setText(label);
+            positiveBtn.setOnClickListener(listener);
+            positiveBtn.setVisibility(View.VISIBLE);
+            return this;
+        }
+
+        /**
+         * Sets the negative button label and listener for the dialog.
+         *
+         * @param labelRes Resource ID of the string to be used for the negative button label.
+         * @param listener The listener to be invoked when the negative button is pressed.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setNegativeButton(
+                @StringRes int labelRes, @NonNull DialogInterface.OnClickListener listener) {
+            mBuilder.setNegativeButton(labelRes, listener);
+            return this;
+        }
+
+        /**
+         * Sets the negative button label and listener for the dialog.
+         *
+         * @param label The text to be used for the negative button label.
+         * @param listener The listener to be invoked when the negative button is pressed.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setNegativeButton(
+                @NonNull CharSequence label, @NonNull DialogInterface.OnClickListener listener) {
+            mBuilder.setNegativeButton(label, listener);
+            return this;
+        }
+
+        /**
+         * Sets the custom negative button label and listener for the dialog custom body.
+         *
+         * @param labelRes Resource ID of the string to be used for the negative button label.
+         * @param listener The listener to be invoked when the negative button is pressed.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setCustomNegativeButton(
+                @StringRes int labelRes, @NonNull View.OnClickListener listener) {
+            Button negativeBtn = mCustomBody.findViewById(R.id.negative_btn);
+            negativeBtn.setText(labelRes);
+            negativeBtn.setOnClickListener(listener);
+            negativeBtn.setVisibility(View.VISIBLE);
+            return this;
+        }
+
+        /**
+         * Sets the custom negative button label and listener for the dialog custom body.
+         *
+         * @param label The text to be used for the negative button label.
+         * @param listener The listener to be invoked when the negative button is pressed.
+         * @return This builder.
+         */
+        @NonNull
+        public AudioSharingDialogFactory.DialogBuilder setCustomNegativeButton(
+                @NonNull CharSequence label, @NonNull View.OnClickListener listener) {
+            Button negativeBtn = mCustomBody.findViewById(R.id.negative_btn);
+            negativeBtn.setText(label);
+            negativeBtn.setOnClickListener(listener);
+            negativeBtn.setVisibility(View.VISIBLE);
+            return this;
+        }
+
+        /**
+         * Builds a dialog with the current configs.
+         *
+         * @return The dialog to be shown for audio sharing.
+         */
+        @NonNull
+        @CheckReturnValue
+        public AlertDialog build() {
+            if (mIsCustomBodyEnabled) {
+                mBuilder.setView(mCustomBody);
+            }
+            final AlertDialog dialog =
+                    mBuilder.setCustomTitle(mCustomTitle).setCancelable(false).create();
+            dialog.setCanceledOnTouchOutside(false);
+            return dialog;
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
new file mode 100644
index 0000000..6f7de8c
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+import com.google.common.collect.Iterables;
+
+import java.util.List;
+
+public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
+    private static final String TAG = "AudioSharingDialog";
+
+    private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items";
+
+    // The host creates an instance of this dialog fragment must implement this interface to receive
+    // event callbacks.
+    public interface DialogEventListener {
+        /**
+         * Called when users click the device item for sharing in the dialog.
+         *
+         * @param item The device item clicked.
+         */
+        void onItemClick(AudioSharingDeviceItem item);
+    }
+
+    @Nullable private static DialogEventListener sListener;
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.DIALOG_START_AUDIO_SHARING;
+    }
+
+    /**
+     * Display the {@link AudioSharingDialogFragment} dialog.
+     *
+     * @param host The Fragment this dialog will be hosted.
+     * @param deviceItems The connected device items eligible for audio sharing.
+     * @param listener The callback to handle the user action on this dialog.
+     */
+    public static void show(
+            @NonNull Fragment host,
+            @NonNull List<AudioSharingDeviceItem> deviceItems,
+            @NonNull DialogEventListener listener) {
+        if (!AudioSharingUtils.isFeatureEnabled()) return;
+        final FragmentManager manager = host.getChildFragmentManager();
+        sListener = listener;
+        AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
+        if (dialog != null) {
+            Log.d(TAG, "Dialog is showing, return.");
+            return;
+        }
+        Log.d(TAG, "Show up the dialog.");
+        final Bundle bundle = new Bundle();
+        bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
+        AudioSharingDialogFragment dialogFrag = new AudioSharingDialogFragment();
+        dialogFrag.setArguments(bundle);
+        dialogFrag.show(manager, TAG);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        Bundle arguments = requireArguments();
+        List<AudioSharingDeviceItem> deviceItems =
+                arguments.getParcelable(BUNDLE_KEY_DEVICE_ITEMS, List.class);
+        AudioSharingDialogFactory.DialogBuilder builder =
+                AudioSharingDialogFactory.newBuilder(getActivity())
+                        .setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
+                        .setIsCustomBodyEnabled(true);
+        if (deviceItems.isEmpty()) {
+            builder.setTitle(R.string.audio_sharing_share_dialog_title)
+                    .setCustomImage(R.drawable.audio_sharing_guidance)
+                    .setCustomMessage(R.string.audio_sharing_dialog_connect_device_content)
+                    .setNegativeButton(
+                            R.string.audio_sharing_close_button_label, (dig, which) -> dismiss());
+        } else if (deviceItems.size() == 1) {
+            AudioSharingDeviceItem deviceItem = Iterables.getOnlyElement(deviceItems);
+            builder.setTitle(
+                            getString(
+                                    R.string.audio_sharing_share_with_dialog_title,
+                                    deviceItem.getName()))
+                    .setCustomMessage(R.string.audio_sharing_dialog_share_content)
+                    .setCustomPositiveButton(
+                            R.string.audio_sharing_share_button_label,
+                            v -> {
+                                if (sListener != null) {
+                                    sListener.onItemClick(deviceItem);
+                                }
+                                dismiss();
+                            })
+                    .setCustomNegativeButton(
+                            R.string.audio_sharing_no_thanks_button_label, v -> dismiss());
+        } else {
+            builder.setTitle(R.string.audio_sharing_share_with_more_dialog_title)
+                    .setCustomMessage(R.string.audio_sharing_dialog_share_more_content)
+                    .setCustomDeviceActions(
+                            new AudioSharingDeviceAdapter(
+                                    getContext(),
+                                    deviceItems,
+                                    (AudioSharingDeviceItem item) -> {
+                                        if (sListener != null) {
+                                            sListener.onItemClick(item);
+                                        }
+                                        dismiss();
+                                    },
+                                    AudioSharingDeviceAdapter.ActionType.SHARE))
+                    .setCustomNegativeButton(com.android.settings.R.string.cancel, v -> dismiss());
+        }
+        return builder.build();
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandler.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandler.java
new file mode 100644
index 0000000..c329e82
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandler.java
@@ -0,0 +1,452 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import android.app.settings.SettingsEnums;
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+public class AudioSharingDialogHandler {
+    private static final String TAG = "AudioSharingDialogHandler";
+    private final Context mContext;
+    private final Fragment mHostFragment;
+    @Nullable private final LocalBluetoothManager mLocalBtManager;
+    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+    @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
+    private List<BluetoothDevice> mTargetSinks = new ArrayList<>();
+
+    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+            new BluetoothLeBroadcast.Callback() {
+                @Override
+                public void onBroadcastStarted(int reason, int broadcastId) {
+                    Log.d(
+                            TAG,
+                            "onBroadcastStarted(), reason = "
+                                    + reason
+                                    + ", broadcastId = "
+                                    + broadcastId);
+                }
+
+                @Override
+                public void onBroadcastStartFailed(int reason) {
+                    Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
+                    AudioSharingUtils.toastMessage(
+                            mContext, "Fail to start broadcast, reason " + reason);
+                }
+
+                @Override
+                public void onBroadcastMetadataChanged(
+                        int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {
+                    Log.d(
+                            TAG,
+                            "onBroadcastMetadataChanged(), broadcastId = "
+                                    + broadcastId
+                                    + ", metadata = "
+                                    + metadata);
+                }
+
+                @Override
+                public void onBroadcastStopped(int reason, int broadcastId) {
+                    Log.d(
+                            TAG,
+                            "onBroadcastStopped(), reason = "
+                                    + reason
+                                    + ", broadcastId = "
+                                    + broadcastId);
+                }
+
+                @Override
+                public void onBroadcastStopFailed(int reason) {
+                    Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
+                    AudioSharingUtils.toastMessage(
+                            mContext, "Fail to stop broadcast, reason " + reason);
+                }
+
+                @Override
+                public void onBroadcastUpdated(int reason, int broadcastId) {}
+
+                @Override
+                public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
+
+                @Override
+                public void onPlaybackStarted(int reason, int broadcastId) {
+                    Log.d(
+                            TAG,
+                            "onPlaybackStarted(), reason = "
+                                    + reason
+                                    + ", broadcastId = "
+                                    + broadcastId);
+                    if (!mTargetSinks.isEmpty()) {
+                        AudioSharingUtils.addSourceToTargetSinks(mTargetSinks, mLocalBtManager);
+                        new SubSettingLauncher(mContext)
+                                .setDestination(AudioSharingDashboardFragment.class.getName())
+                                .setSourceMetricsCategory(
+                                        (mHostFragment != null
+                                                        && mHostFragment
+                                                                instanceof DashboardFragment)
+                                                ? ((DashboardFragment) mHostFragment)
+                                                        .getMetricsCategory()
+                                                : SettingsEnums.PAGE_UNKNOWN)
+                                .launch();
+                        mTargetSinks = new ArrayList<>();
+                    }
+                }
+
+                @Override
+                public void onPlaybackStopped(int reason, int broadcastId) {}
+            };
+
+    public AudioSharingDialogHandler(@NonNull Context context, @NonNull Fragment fragment) {
+        mContext = context;
+        mHostFragment = fragment;
+        mLocalBtManager = Utils.getLocalBluetoothManager(context);
+        mBroadcast =
+                mLocalBtManager != null
+                        ? mLocalBtManager.getProfileManager().getLeAudioBroadcastProfile()
+                        : null;
+        mAssistant =
+                mLocalBtManager != null
+                        ? mLocalBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile()
+                        : null;
+    }
+
+    /** Register callbacks for dialog handler */
+    public void registerCallbacks(Executor executor) {
+        if (mBroadcast != null) {
+            mBroadcast.registerServiceCallBack(executor, mBroadcastCallback);
+        }
+    }
+
+    /** Unregister callbacks for dialog handler */
+    public void unregisterCallbacks() {
+        if (mBroadcast != null) {
+            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+        }
+    }
+
+    /** Handle dialog pop-up logic when device is connected. */
+    public void handleDeviceConnected(
+            @NonNull CachedBluetoothDevice cachedDevice, boolean userTriggered) {
+        String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress();
+        boolean isBroadcasting = isBroadcasting();
+        boolean isLeAudioSupported = AudioSharingUtils.isLeAudioSupported(cachedDevice);
+        if (!isLeAudioSupported) {
+            Log.d(TAG, "Handle non LE audio device connected, device = " + anonymizedAddress);
+            // Handle connected ineligible (non LE audio) remote device
+            handleNonLeAudioDeviceConnected(cachedDevice, isBroadcasting, userTriggered);
+        } else {
+            Log.d(TAG, "Handle LE audio device connected, device = " + anonymizedAddress);
+            // Handle connected eligible (LE audio) remote device
+            handleLeAudioDeviceConnected(cachedDevice, isBroadcasting, userTriggered);
+        }
+    }
+
+    private void handleNonLeAudioDeviceConnected(
+            @NonNull CachedBluetoothDevice cachedDevice,
+            boolean isBroadcasting,
+            boolean userTriggered) {
+        if (isBroadcasting) {
+            // Show stop audio sharing dialog when an ineligible (non LE audio) remote device
+            // connected during a sharing session.
+            Map<Integer, List<CachedBluetoothDevice>> groupedDevices =
+                    AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager);
+            List<AudioSharingDeviceItem> deviceItemsInSharingSession =
+                    AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
+                            mLocalBtManager, groupedDevices, /* filterByInSharing= */ true);
+            postOnMainThread(
+                    () -> {
+                        closeOpeningDialogsOtherThan(AudioSharingStopDialogFragment.tag());
+                        AudioSharingStopDialogFragment.show(
+                                mHostFragment,
+                                deviceItemsInSharingSession,
+                                cachedDevice,
+                                () -> {
+                                    cachedDevice.setActive();
+                                    AudioSharingUtils.stopBroadcasting(mLocalBtManager);
+                                });
+                    });
+        } else {
+            if (userTriggered) {
+                cachedDevice.setActive();
+            }
+            // Do nothing for ineligible (non LE audio) remote device when no sharing session.
+            Log.d(
+                    TAG,
+                    "Ignore onProfileConnectionStateChanged for non LE audio without"
+                            + " sharing session");
+        }
+    }
+
+    private void handleLeAudioDeviceConnected(
+            @NonNull CachedBluetoothDevice cachedDevice,
+            boolean isBroadcasting,
+            boolean userTriggered) {
+        Map<Integer, List<CachedBluetoothDevice>> groupedDevices =
+                AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager);
+        if (isBroadcasting) {
+            // If another device within the same is already in the sharing session, add source to
+            // the device automatically.
+            int groupId = AudioSharingUtils.getGroupId(cachedDevice);
+            if (groupedDevices.containsKey(groupId)
+                    && groupedDevices.get(groupId).stream()
+                            .anyMatch(
+                                    device ->
+                                            BluetoothUtils.hasConnectedBroadcastSource(
+                                                    device, mLocalBtManager))) {
+                Log.d(
+                        TAG,
+                        "Automatically add another device within the same group to the sharing: "
+                                + cachedDevice.getDevice().getAnonymizedAddress());
+                if (mAssistant != null && mBroadcast != null) {
+                    mAssistant.addSource(
+                            cachedDevice.getDevice(),
+                            mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
+                            /* isGroupOp= */ false);
+                }
+                return;
+            }
+
+            // Show audio sharing switch or join dialog according to device count in the sharing
+            // session.
+            List<AudioSharingDeviceItem> deviceItemsInSharingSession =
+                    AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
+                            mLocalBtManager, groupedDevices, /* filterByInSharing= */ true);
+            // Show audio sharing switch dialog when the third eligible (LE audio) remote device
+            // connected during a sharing session.
+            if (deviceItemsInSharingSession.size() >= 2) {
+                postOnMainThread(
+                        () -> {
+                            closeOpeningDialogsOtherThan(
+                                    AudioSharingDisconnectDialogFragment.tag());
+                            AudioSharingDisconnectDialogFragment.show(
+                                    mHostFragment,
+                                    deviceItemsInSharingSession,
+                                    cachedDevice,
+                                    (AudioSharingDeviceItem item) -> {
+                                        // Remove all sources from the device user clicked
+                                        removeSourceForGroup(item.getGroupId(), groupedDevices);
+                                        // Add current broadcast to the latest connected device
+                                        addSourceForGroup(groupId, groupedDevices);
+                                    });
+                        });
+            } else {
+                // Show audio sharing join dialog when the first or second eligible (LE audio)
+                // remote device connected during a sharing session.
+                postOnMainThread(
+                        () -> {
+                            closeOpeningDialogsOtherThan(AudioSharingJoinDialogFragment.tag());
+                            AudioSharingJoinDialogFragment.show(
+                                    mHostFragment,
+                                    deviceItemsInSharingSession,
+                                    cachedDevice,
+                                    new AudioSharingJoinDialogFragment.DialogEventListener() {
+                                        @Override
+                                        public void onShareClick() {
+                                            addSourceForGroup(groupId, groupedDevices);
+                                        }
+
+                                        @Override
+                                        public void onCancelClick() {}
+                                    });
+                        });
+            }
+        } else {
+            List<AudioSharingDeviceItem> deviceItems = new ArrayList<>();
+            for (List<CachedBluetoothDevice> devices : groupedDevices.values()) {
+                // Use random device in the group within the sharing session to represent the group.
+                CachedBluetoothDevice device = devices.get(0);
+                if (AudioSharingUtils.getGroupId(device)
+                        == AudioSharingUtils.getGroupId(cachedDevice)) {
+                    continue;
+                }
+                deviceItems.add(AudioSharingUtils.buildAudioSharingDeviceItem(device));
+            }
+            // Show audio sharing join dialog when the second eligible (LE audio) remote
+            // device connect and no sharing session.
+            if (deviceItems.size() == 1) {
+                postOnMainThread(
+                        () -> {
+                            closeOpeningDialogsOtherThan(AudioSharingJoinDialogFragment.tag());
+                            AudioSharingJoinDialogFragment.show(
+                                    mHostFragment,
+                                    deviceItems,
+                                    cachedDevice,
+                                    new AudioSharingJoinDialogFragment.DialogEventListener() {
+                                        @Override
+                                        public void onShareClick() {
+                                            mTargetSinks = new ArrayList<>();
+                                            for (List<CachedBluetoothDevice> devices :
+                                                    groupedDevices.values()) {
+                                                for (CachedBluetoothDevice device : devices) {
+                                                    mTargetSinks.add(device.getDevice());
+                                                }
+                                            }
+                                            Log.d(
+                                                    TAG,
+                                                    "Start broadcast with sinks: "
+                                                            + mTargetSinks.size());
+                                            if (mBroadcast != null) {
+                                                mBroadcast.startPrivateBroadcast();
+                                            }
+                                        }
+
+                                        @Override
+                                        public void onCancelClick() {
+                                            if (userTriggered) {
+                                                cachedDevice.setActive();
+                                            }
+                                        }
+                                    });
+                        });
+            } else if (userTriggered) {
+                cachedDevice.setActive();
+            }
+        }
+    }
+
+    private void closeOpeningDialogsOtherThan(String tag) {
+        if (mHostFragment == null) return;
+        List<Fragment> fragments = mHostFragment.getChildFragmentManager().getFragments();
+        for (Fragment fragment : fragments) {
+            if (fragment instanceof DialogFragment && !fragment.getTag().equals(tag)) {
+                Log.d(TAG, "Remove staled opening dialog " + fragment.getTag());
+                ((DialogFragment) fragment).dismiss();
+            }
+        }
+    }
+
+    /** Close opening dialogs for le audio device */
+    public void closeOpeningDialogsForLeaDevice(@NonNull CachedBluetoothDevice cachedDevice) {
+        if (mHostFragment == null) return;
+        int groupId = AudioSharingUtils.getGroupId(cachedDevice);
+        List<Fragment> fragments = mHostFragment.getChildFragmentManager().getFragments();
+        for (Fragment fragment : fragments) {
+            CachedBluetoothDevice device = getCachedBluetoothDeviceFromDialog(fragment);
+            if (device != null
+                    && groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
+                    && AudioSharingUtils.getGroupId(device) == groupId) {
+                Log.d(TAG, "Remove staled opening dialog for group " + groupId);
+                ((DialogFragment) fragment).dismiss();
+            }
+        }
+    }
+
+    /** Close opening dialogs for non le audio device */
+    public void closeOpeningDialogsForNonLeaDevice(@NonNull CachedBluetoothDevice cachedDevice) {
+        if (mHostFragment == null) return;
+        String address = cachedDevice.getAddress();
+        List<Fragment> fragments = mHostFragment.getChildFragmentManager().getFragments();
+        for (Fragment fragment : fragments) {
+            CachedBluetoothDevice device = getCachedBluetoothDeviceFromDialog(fragment);
+            if (device != null && address != null && address.equals(device.getAddress())) {
+                Log.d(
+                        TAG,
+                        "Remove staled opening dialog for device "
+                                + cachedDevice.getDevice().getAnonymizedAddress());
+                ((DialogFragment) fragment).dismiss();
+            }
+        }
+    }
+
+    @Nullable
+    private CachedBluetoothDevice getCachedBluetoothDeviceFromDialog(Fragment fragment) {
+        CachedBluetoothDevice device = null;
+        if (fragment instanceof AudioSharingJoinDialogFragment) {
+            device = ((AudioSharingJoinDialogFragment) fragment).getDevice();
+        } else if (fragment instanceof AudioSharingStopDialogFragment) {
+            device = ((AudioSharingStopDialogFragment) fragment).getDevice();
+        } else if (fragment instanceof AudioSharingDisconnectDialogFragment) {
+            device = ((AudioSharingDisconnectDialogFragment) fragment).getDevice();
+        }
+        return device;
+    }
+
+    private void removeSourceForGroup(
+            int groupId, Map<Integer, List<CachedBluetoothDevice>> groupedDevices) {
+        if (mAssistant == null) {
+            Log.d(TAG, "Fail to add source due to null profiles, group = " + groupId);
+            return;
+        }
+        if (!groupedDevices.containsKey(groupId)) {
+            Log.d(TAG, "Fail to remove source for group " + groupId);
+            return;
+        }
+        groupedDevices.get(groupId).stream()
+                .map(CachedBluetoothDevice::getDevice)
+                .filter(device -> device != null)
+                .forEach(
+                        device -> {
+                            for (BluetoothLeBroadcastReceiveState source :
+                                    mAssistant.getAllSources(device)) {
+                                mAssistant.removeSource(device, source.getSourceId());
+                            }
+                        });
+    }
+
+    private void addSourceForGroup(
+            int groupId, Map<Integer, List<CachedBluetoothDevice>> groupedDevices) {
+        if (mBroadcast == null || mAssistant == null) {
+            Log.d(TAG, "Fail to add source due to null profiles, group = " + groupId);
+            return;
+        }
+        if (!groupedDevices.containsKey(groupId)) {
+            Log.d(TAG, "Fail to add source due to invalid group id, group = " + groupId);
+            return;
+        }
+        groupedDevices.get(groupId).stream()
+                .map(CachedBluetoothDevice::getDevice)
+                .filter(device -> device != null)
+                .forEach(
+                        device ->
+                                mAssistant.addSource(
+                                        device,
+                                        mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
+                                        /* isGroupOp= */ false));
+    }
+
+    private void postOnMainThread(@NonNull Runnable runnable) {
+        mContext.getMainExecutor().execute(runnable);
+    }
+
+    private boolean isBroadcasting() {
+        return mBroadcast != null && mBroadcast.isEnabled(null);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHelper.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHelper.java
new file mode 100644
index 0000000..69001aa
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHelper.java
@@ -0,0 +1,62 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import android.graphics.Typeface;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+public class AudioSharingDialogHelper {
+    private static final String TAG = "AudioSharingDialogHelper";
+
+    /** Updates the alert dialog message style. */
+    public static void updateMessageStyle(@NonNull AlertDialog dialog) {
+        TextView messageView = dialog.findViewById(android.R.id.message);
+        if (messageView != null) {
+            Typeface typeface = Typeface.create(Typeface.DEFAULT_FAMILY, Typeface.NORMAL);
+            messageView.setTypeface(typeface);
+            messageView.setTextDirection(View.TEXT_DIRECTION_LOCALE);
+            messageView.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
+            messageView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
+        } else {
+            Log.w(TAG, "Fail to update dialog: message view is null");
+        }
+    }
+
+    /** Returns the alert dialog by tag if it is showing. */
+    @Nullable
+    public static AlertDialog getDialogIfShowing(
+            @NonNull FragmentManager manager, @NonNull String tag) {
+        Fragment dialog = manager.findFragmentByTag(tag);
+        return dialog != null
+                        && dialog instanceof DialogFragment
+                        && ((DialogFragment) dialog).getDialog() != null
+                        && ((DialogFragment) dialog).getDialog().isShowing()
+                        && ((DialogFragment) dialog).getDialog() instanceof AlertDialog
+                ? (AlertDialog) ((DialogFragment) dialog).getDialog()
+                : null;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
new file mode 100644
index 0000000..e859693
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+
+import java.util.List;
+import java.util.Locale;
+
+public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFragment {
+    private static final String TAG = "AudioSharingDisconnectDialog";
+
+    private static final String BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS =
+            "bundle_key_device_to_disconnect_items";
+    private static final String BUNDLE_KEY_NEW_DEVICE_NAME = "bundle_key_new_device_name";
+
+    // The host creates an instance of this dialog fragment must implement this interface to receive
+    // event callbacks.
+    public interface DialogEventListener {
+        /**
+         * Called when users click the device item to disconnect from the audio sharing in the
+         * dialog.
+         *
+         * @param item The device item clicked.
+         */
+        void onItemClick(AudioSharingDeviceItem item);
+    }
+
+    @Nullable private static DialogEventListener sListener;
+    @Nullable private static CachedBluetoothDevice sNewDevice;
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE;
+    }
+
+    /**
+     * Display the {@link AudioSharingDisconnectDialogFragment} dialog.
+     *
+     * <p>If the dialog is showing for the same group, update the dialog event listener.
+     *
+     * @param host The Fragment this dialog will be hosted.
+     * @param deviceItems The existing connected device items in audio sharing session.
+     * @param newDevice The latest connected device triggered this dialog.
+     * @param listener The callback to handle the user action on this dialog.
+     */
+    public static void show(
+            @NonNull Fragment host,
+            @NonNull List<AudioSharingDeviceItem> deviceItems,
+            @NonNull CachedBluetoothDevice newDevice,
+            @NonNull DialogEventListener listener) {
+        if (!AudioSharingUtils.isFeatureEnabled()) return;
+        FragmentManager manager = host.getChildFragmentManager();
+        AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
+        if (dialog != null) {
+            int newGroupId = AudioSharingUtils.getGroupId(newDevice);
+            if (sNewDevice != null && newGroupId == AudioSharingUtils.getGroupId(sNewDevice)) {
+                Log.d(
+                        TAG,
+                        String.format(
+                                Locale.US,
+                                "Dialog is showing for the same device group %d, "
+                                        + "update the content.",
+                                newGroupId));
+                sListener = listener;
+                sNewDevice = newDevice;
+                return;
+            } else {
+                Log.d(
+                        TAG,
+                        String.format(
+                                Locale.US,
+                                "Dialog is showing for new device group %d, "
+                                        + "dismiss current dialog.",
+                                newGroupId));
+                dialog.dismiss();
+            }
+        }
+        sListener = listener;
+        sNewDevice = newDevice;
+        Log.d(TAG, "Show up the dialog.");
+        final Bundle bundle = new Bundle();
+        bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
+        bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
+        AudioSharingDisconnectDialogFragment dialogFrag =
+                new AudioSharingDisconnectDialogFragment();
+        dialogFrag.setArguments(bundle);
+        dialogFrag.show(manager, TAG);
+    }
+
+    /** Return the tag of {@link AudioSharingDisconnectDialogFragment} dialog. */
+    public static @NonNull String tag() {
+        return TAG;
+    }
+
+    /** Get the latest connected device which triggers the dialog. */
+    public @Nullable CachedBluetoothDevice getDevice() {
+        return sNewDevice;
+    }
+
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        Bundle arguments = requireArguments();
+        List<AudioSharingDeviceItem> deviceItems =
+                arguments.getParcelable(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, List.class);
+        return AudioSharingDialogFactory.newBuilder(getActivity())
+                .setTitle(R.string.audio_sharing_disconnect_dialog_title)
+                .setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
+                .setIsCustomBodyEnabled(true)
+                .setCustomMessage(R.string.audio_sharing_dialog_disconnect_content)
+                .setCustomDeviceActions(
+                        new AudioSharingDeviceAdapter(
+                                getContext(),
+                                deviceItems,
+                                (AudioSharingDeviceItem item) -> {
+                                    if (sListener != null) {
+                                        sListener.onItemClick(item);
+                                    }
+                                    dismiss();
+                                },
+                                AudioSharingDeviceAdapter.ActionType.REMOVE))
+                .setCustomNegativeButton(com.android.settings.R.string.cancel, v -> dismiss())
+                .build();
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProvider.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProvider.java
deleted file mode 100644
index 50812e9..0000000
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProvider.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.settings.connecteddevice.audiosharing;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.settings.dashboard.DashboardFragment;
-import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
-import com.android.settingslib.core.AbstractPreferenceController;
-import com.android.settingslib.core.lifecycle.Lifecycle;
-
-/** Feature provider for the audio sharing related features, */
-public interface AudioSharingFeatureProvider {
-
-    /** Create audio sharing device preference controller. */
-    @Nullable
-    AbstractPreferenceController createAudioSharingDevicePreferenceController(
-            @NonNull Context context,
-            @Nullable DashboardFragment fragment,
-            @Nullable Lifecycle lifecycle);
-
-    /** Create available media device preference controller. */
-    AbstractPreferenceController createAvailableMediaDeviceGroupController(
-            @NonNull Context context,
-            @Nullable DashboardFragment fragment,
-            @Nullable Lifecycle lifecycle);
-
-    /**
-     * Check if the device match the audio sharing filter.
-     *
-     * <p>The filter is used to filter device in "Media devices" section.
-     */
-    boolean isAudioSharingFilterMatched(
-            @NonNull CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager);
-}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImpl.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImpl.java
deleted file mode 100644
index 96200db..0000000
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImpl.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.settings.connecteddevice.audiosharing;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.settings.connecteddevice.AvailableMediaDeviceGroupController;
-import com.android.settings.dashboard.DashboardFragment;
-import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
-import com.android.settingslib.core.AbstractPreferenceController;
-import com.android.settingslib.core.lifecycle.Lifecycle;
-
-public class AudioSharingFeatureProviderImpl implements AudioSharingFeatureProvider {
-
-    @Nullable
-    @Override
-    public AbstractPreferenceController createAudioSharingDevicePreferenceController(
-            @NonNull Context context,
-            @Nullable DashboardFragment fragment,
-            @Nullable Lifecycle lifecycle) {
-        return null;
-    }
-
-    @Override
-    public AbstractPreferenceController createAvailableMediaDeviceGroupController(
-            @NonNull Context context,
-            @Nullable DashboardFragment fragment,
-            @Nullable Lifecycle lifecycle) {
-        return new AvailableMediaDeviceGroupController(context, fragment, lifecycle);
-    }
-
-    @Override
-    public boolean isAudioSharingFilterMatched(
-            @NonNull CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
-        return false;
-    }
-}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java
new file mode 100644
index 0000000..0a5961d
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+
+import java.util.List;
+
+public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
+    private static final String TAG = "AudioSharingJoinDialog";
+
+    private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items";
+    private static final String BUNDLE_KEY_NEW_DEVICE_NAME = "bundle_key_new_device_name";
+
+    // The host creates an instance of this dialog fragment must implement this interface to receive
+    // event callbacks.
+    public interface DialogEventListener {
+        /** Called when users click the share audio button in the dialog. */
+        void onShareClick();
+
+        /** Called when users click the cancel button in the dialog. */
+        void onCancelClick();
+    }
+
+    @Nullable private static DialogEventListener sListener;
+    @Nullable private static CachedBluetoothDevice sNewDevice;
+
+    @Override
+    public int getMetricsCategory() {
+        return AudioSharingUtils.isBroadcasting(Utils.getLocalBtManager(getContext()))
+                ? SettingsEnums.DIALOG_START_AUDIO_SHARING
+                : SettingsEnums.DIALOG_START_AUDIO_SHARING;
+    }
+
+    /**
+     * Display the {@link AudioSharingJoinDialogFragment} dialog.
+     *
+     * <p>If the dialog is showing, update the dialog message and event listener.
+     *
+     * @param host The Fragment this dialog will be hosted.
+     * @param deviceItems The existing connected device items eligible for audio sharing.
+     * @param newDevice The latest connected device triggered this dialog.
+     * @param listener The callback to handle the user action on this dialog.
+     */
+    public static void show(
+            @NonNull Fragment host,
+            @NonNull List<AudioSharingDeviceItem> deviceItems,
+            @NonNull CachedBluetoothDevice newDevice,
+            @NonNull DialogEventListener listener) {
+        if (!AudioSharingUtils.isFeatureEnabled()) return;
+        final FragmentManager manager = host.getChildFragmentManager();
+        sListener = listener;
+        sNewDevice = newDevice;
+        AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
+        if (dialog != null) {
+            Log.d(TAG, "Dialog is showing, update the content.");
+            updateDialog(deviceItems, newDevice.getName(), dialog);
+        } else {
+            Log.d(TAG, "Show up the dialog.");
+            final Bundle bundle = new Bundle();
+            bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
+            bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
+            final AudioSharingJoinDialogFragment dialogFrag = new AudioSharingJoinDialogFragment();
+            dialogFrag.setArguments(bundle);
+            dialogFrag.show(manager, TAG);
+        }
+    }
+
+    /** Return the tag of {@link AudioSharingJoinDialogFragment} dialog. */
+    public static @NonNull String tag() {
+        return TAG;
+    }
+
+    /** Get the latest connected device which triggers the dialog. */
+    public @Nullable CachedBluetoothDevice getDevice() {
+        return sNewDevice;
+    }
+
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        Bundle arguments = requireArguments();
+        List<AudioSharingDeviceItem> deviceItems =
+                arguments.getParcelable(BUNDLE_KEY_DEVICE_ITEMS, List.class);
+        String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
+        AlertDialog dialog =
+                AudioSharingDialogFactory.newBuilder(getActivity())
+                        .setTitle(R.string.audio_sharing_share_dialog_title)
+                        .setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
+                        .setIsCustomBodyEnabled(true)
+                        .setCustomMessage(R.string.audio_sharing_dialog_share_content)
+                        .setCustomPositiveButton(
+                                R.string.audio_sharing_share_button_label,
+                                v -> {
+                                    if (sListener != null) {
+                                        sListener.onShareClick();
+                                    }
+                                    dismiss();
+                                })
+                        .setCustomNegativeButton(
+                                R.string.audio_sharing_no_thanks_button_label,
+                                v -> {
+                                    if (sListener != null) {
+                                        sListener.onCancelClick();
+                                    }
+                                    dismiss();
+                                })
+                        .build();
+        updateDialog(deviceItems, newDeviceName, dialog);
+        dialog.show();
+        AudioSharingDialogHelper.updateMessageStyle(dialog);
+        return dialog;
+    }
+
+    private static void updateDialog(
+            List<AudioSharingDeviceItem> deviceItems,
+            String newDeviceName,
+            @NonNull AlertDialog dialog) {
+        // Only dialog message can be updated when the dialog is showing.
+        // Thus we put the device name for sharing as the dialog message.
+        if (deviceItems.isEmpty()) {
+            dialog.setMessage(newDeviceName);
+        } else {
+            dialog.setMessage(
+                    dialog.getContext()
+                            .getString(
+                                    R.string.audio_sharing_share_dialog_subtitle,
+                                    deviceItems.get(0).getName(),
+                                    newDeviceName));
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
new file mode 100644
index 0000000..0bb6b60
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageButton;
+
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.widget.ValidatedEditTextPreference;
+
+public class AudioSharingNamePreference extends ValidatedEditTextPreference {
+    private static final String TAG = "AudioSharingNamePreference";
+    private boolean mShowQrCodeIcon = false;
+
+    public AudioSharingNamePreference(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        initialize();
+    }
+
+    public AudioSharingNamePreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        initialize();
+    }
+
+    public AudioSharingNamePreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initialize();
+    }
+
+    public AudioSharingNamePreference(Context context) {
+        super(context);
+        initialize();
+    }
+
+    private void initialize() {
+        setLayoutResource(
+                com.android.settingslib.widget.preference.twotarget.R.layout.preference_two_target);
+        setWidgetLayoutResource(R.layout.preference_widget_qrcode);
+    }
+
+    void setShowQrCodeIcon(boolean show) {
+        mShowQrCodeIcon = show;
+        notifyChanged();
+    }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
+        View divider =
+                holder.findViewById(
+                        com.android.settingslib.widget.preference.twotarget.R.id
+                                .two_target_divider);
+
+        if (shareButton != null && divider != null) {
+            if (mShowQrCodeIcon) {
+                configureVisibleStateForQrCodeIcon(shareButton, divider);
+            } else {
+                configureInvisibleStateForQrCodeIcon(shareButton, divider);
+            }
+        } else {
+            Log.w(TAG, "onBindViewHolder() : shareButton or divider is null!");
+        }
+    }
+
+    private void configureVisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
+        divider.setVisibility(View.VISIBLE);
+        shareButton.setVisibility(View.VISIBLE);
+        shareButton.setImageDrawable(getContext().getDrawable(R.drawable.ic_qrcode_24dp));
+        shareButton.setOnClickListener(unused -> launchAudioSharingQrCodeFragment());
+    }
+
+    private void configureInvisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
+        divider.setVisibility(View.INVISIBLE);
+        shareButton.setVisibility(View.INVISIBLE);
+        shareButton.setOnClickListener(null);
+    }
+
+    private void launchAudioSharingQrCodeFragment() {
+        new SubSettingLauncher(getContext())
+                .setTitleText(getContext().getString(R.string.audio_streams_qr_code_page_title))
+                .setDestination(AudioStreamsQrCodeFragment.class.getName())
+                .setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
+                .launch();
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
new file mode 100644
index 0000000..2ab7b80
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.isBroadcasting;
+
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.widget.ValidatedEditTextPreference;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class AudioSharingNamePreferenceController extends BasePreferenceController
+        implements ValidatedEditTextPreference.Validator,
+                Preference.OnPreferenceChangeListener,
+                DefaultLifecycleObserver,
+                LocalBluetoothProfileManager.ServiceListener {
+
+    private static final String TAG = "AudioSharingNamePreferenceController";
+    private static final boolean DEBUG = BluetoothUtils.D;
+    private static final String PREF_KEY = "audio_sharing_stream_name";
+
+    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+            new BluetoothLeBroadcast.Callback() {
+                @Override
+                public void onBroadcastMetadataChanged(
+                        int broadcastId, BluetoothLeBroadcastMetadata metadata) {
+                    if (DEBUG) {
+                        Log.d(
+                                TAG,
+                                "onBroadcastMetadataChanged() broadcastId : "
+                                        + broadcastId
+                                        + " metadata: "
+                                        + metadata);
+                    }
+                    updateQrCodeIcon(true);
+                }
+
+                @Override
+                public void onBroadcastStartFailed(int reason) {}
+
+                @Override
+                public void onBroadcastStarted(int reason, int broadcastId) {}
+
+                @Override
+                public void onBroadcastStopFailed(int reason) {}
+
+                @Override
+                public void onBroadcastStopped(int reason, int broadcastId) {
+                    if (DEBUG) {
+                        Log.d(
+                                TAG,
+                                "onBroadcastStopped() reason : "
+                                        + reason
+                                        + " broadcastId: "
+                                        + broadcastId);
+                    }
+                    updateQrCodeIcon(false);
+                }
+
+                @Override
+                public void onBroadcastUpdateFailed(int reason, int broadcastId) {
+                    Log.w(TAG, "onBroadcastUpdateFailed() reason : " + reason);
+                }
+
+                @Override
+                public void onBroadcastUpdated(int reason, int broadcastId) {
+                    if (DEBUG) {
+                        Log.d(TAG, "onBroadcastUpdated() reason : " + reason);
+                    }
+                }
+
+                @Override
+                public void onPlaybackStarted(int reason, int broadcastId) {}
+
+                @Override
+                public void onPlaybackStopped(int reason, int broadcastId) {}
+            };
+
+    @Nullable private final LocalBluetoothManager mBtManager;
+    @Nullable private final LocalBluetoothProfileManager mProfileManager;
+    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+    @Nullable private AudioSharingNamePreference mPreference;
+    private final Executor mExecutor;
+    private final AudioSharingNameTextValidator mAudioSharingNameTextValidator;
+    private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
+
+    public AudioSharingNamePreferenceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mBtManager = Utils.getLocalBluetoothManager(context);
+        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
+        mBroadcast =
+                (mProfileManager != null) ? mProfileManager.getLeAudioBroadcastProfile() : null;
+        mAudioSharingNameTextValidator = new AudioSharingNameTextValidator();
+        mExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip register callbacks, feature not support");
+            return;
+        }
+        if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            Log.d(TAG, "Skip register callbacks, profile not ready");
+            if (mProfileManager != null) {
+                mProfileManager.addServiceListener(this);
+            }
+            return;
+        }
+        registerCallbacks();
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip unregister callbacks, feature not support");
+            return;
+        }
+        if (mProfileManager != null) {
+            mProfileManager.removeServiceListener(this);
+        }
+        if (mBroadcast == null || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            Log.d(TAG, "Skip unregister callbacks, profile not ready");
+            return;
+        }
+        if (mCallbacksRegistered.get()) {
+            Log.d(TAG, "Unregister callbacks");
+            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+            mCallbacksRegistered.set(false);
+        }
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreference = screen.findPreference(getPreferenceKey());
+        if (mPreference != null) {
+            mPreference.setValidator(this);
+            updateBroadcastName();
+            updateQrCodeIcon(isBroadcasting(mBtManager));
+        }
+    }
+
+    @Override
+    public void onServiceConnected() {
+        if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            registerCallbacks();
+            updateBroadcastName();
+            updateQrCodeIcon(isBroadcasting(mBtManager));
+            if (mProfileManager != null) {
+                mProfileManager.removeServiceListener(this);
+            }
+        }
+    }
+
+    @Override
+    public void onServiceDisconnected() {
+        // Do nothing
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return PREF_KEY;
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        if (mPreference != null
+                && mPreference.getSummary() != null
+                && ((String) newValue).contentEquals(mPreference.getSummary())) {
+            return false;
+        }
+
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            if (mBroadcast != null) {
+                                mBroadcast.setProgramInfo((String) newValue);
+                                if (isBroadcasting(mBtManager)) {
+                                    mBroadcast.updateBroadcast();
+                                }
+                                updateBroadcastName();
+                            }
+                        });
+        return true;
+    }
+
+    private void registerCallbacks() {
+        if (mBroadcast == null) {
+            Log.d(TAG, "Skip register callbacks, profile not ready");
+            return;
+        }
+        if (!mCallbacksRegistered.get()) {
+            Log.d(TAG, "Register callbacks");
+            mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
+            mCallbacksRegistered.set(true);
+        }
+    }
+
+    private void updateBroadcastName() {
+        if (mPreference != null) {
+            var unused =
+                    ThreadUtils.postOnBackgroundThread(
+                            () -> {
+                                if (mBroadcast != null) {
+                                    String name = mBroadcast.getProgramInfo();
+                                    AudioSharingUtils.postOnMainThread(
+                                            mContext,
+                                            () -> {
+                                                if (mPreference != null) {
+                                                    mPreference.setText(name);
+                                                    mPreference.setSummary(name);
+                                                }
+                                            });
+                                }
+                            });
+        }
+    }
+
+    private void updateQrCodeIcon(boolean show) {
+        if (mPreference != null) {
+            AudioSharingUtils.postOnMainThread(
+                    mContext,
+                    () -> {
+                        if (mPreference != null) {
+                            mPreference.setShowQrCodeIcon(show);
+                        }
+                    });
+        }
+    }
+
+    @Override
+    public boolean isTextValid(String value) {
+        return mAudioSharingNameTextValidator.isTextValid(value);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
new file mode 100644
index 0000000..2022eb2
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import com.android.settings.widget.ValidatedEditTextPreference;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Validator for Audio Sharing Name, which should be a UTF-8 encoded string containing a minimum of
+ * 4 characters and a maximum of 32 human-readable characters.
+ */
+public class AudioSharingNameTextValidator implements ValidatedEditTextPreference.Validator {
+    private static final int MIN_LENGTH = 4;
+    private static final int MAX_LENGTH = 32;
+
+    @Override
+    public boolean isTextValid(String value) {
+        if (value == null || value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
+            return false;
+        }
+        return isValidUTF8(value);
+    }
+
+    private static boolean isValidUTF8(String value) {
+        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
+        String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
+        return value.equals(reconstructedString);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreference.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreference.java
new file mode 100644
index 0000000..e3bbfb7
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreference.java
@@ -0,0 +1,142 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.settings.R;
+import com.android.settings.widget.ValidatedEditTextPreference;
+import com.android.settingslib.utils.ColorUtil;
+
+public class AudioSharingPasswordPreference extends ValidatedEditTextPreference {
+    private static final String TAG = "AudioSharingPasswordPreference";
+    @Nullable private OnDialogEventListener mOnDialogEventListener;
+    @Nullable private EditText mEditText;
+    @Nullable private CheckBox mCheckBox;
+    @Nullable private View mDialogMessage;
+    private boolean mEditable = true;
+
+    interface OnDialogEventListener {
+        void onBindDialogView();
+
+        void onPreferenceDataChanged(@NonNull String editTextValue, boolean checkBoxValue);
+    }
+
+    void setOnDialogEventListener(OnDialogEventListener listener) {
+        mOnDialogEventListener = listener;
+    }
+
+    public AudioSharingPasswordPreference(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    public AudioSharingPasswordPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public AudioSharingPasswordPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AudioSharingPasswordPreference(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void onBindDialogView(View view) {
+        super.onBindDialogView(view);
+
+        mEditText = view.findViewById(android.R.id.edit);
+        mCheckBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox);
+        mDialogMessage = view.findViewById(android.R.id.message);
+
+        if (mEditText == null || mCheckBox == null || mDialogMessage == null) {
+            Log.w(TAG, "onBindDialogView() : Invalid layout");
+            return;
+        }
+
+        mCheckBox.setOnCheckedChangeListener((unused, checked) -> setEditTextEnabled(!checked));
+        if (mOnDialogEventListener != null) {
+            mOnDialogEventListener.onBindDialogView();
+        }
+    }
+
+    @Override
+    protected void onPrepareDialogBuilder(
+            AlertDialog.Builder builder, DialogInterface.OnClickListener listener) {
+        if (!mEditable) {
+            builder.setPositiveButton(null, null);
+        }
+    }
+
+    @Override
+    protected void onClick(DialogInterface dialog, int which) {
+        if (mEditText == null || mCheckBox == null) {
+            Log.w(TAG, "onClick() : Invalid layout");
+            return;
+        }
+
+        if (mOnDialogEventListener != null
+                && which == DialogInterface.BUTTON_POSITIVE
+                && mEditText.getText() != null) {
+            mOnDialogEventListener.onPreferenceDataChanged(
+                    mEditText.getText().toString(), mCheckBox.isChecked());
+        }
+    }
+
+    void setEditable(boolean editable) {
+        if (mEditText == null || mCheckBox == null || mDialogMessage == null) {
+            Log.w(TAG, "setEditable() : Invalid layout");
+            return;
+        }
+        mEditable = editable;
+        setEditTextEnabled(editable);
+        mCheckBox.setEnabled(editable);
+        mDialogMessage.setVisibility(editable ? GONE : VISIBLE);
+    }
+
+    void setChecked(boolean checked) {
+        if (mCheckBox == null) {
+            Log.w(TAG, "setChecked() : Invalid layout");
+            return;
+        }
+        mCheckBox.setChecked(checked);
+    }
+
+    private void setEditTextEnabled(boolean enabled) {
+        if (mEditText == null) {
+            Log.w(TAG, "setEditTextEnabled() : Invalid layout");
+            return;
+        }
+        mEditText.setEnabled(enabled);
+        mEditText.setAlpha(enabled ? 1.0f : ColorUtil.getDisabledAlpha(getContext()));
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
new file mode 100644
index 0000000..7c58c43
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.isBroadcasting;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.widget.ValidatedEditTextPreference;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.nio.charset.StandardCharsets;
+
+public class AudioSharingPasswordPreferenceController extends BasePreferenceController
+        implements ValidatedEditTextPreference.Validator,
+                AudioSharingPasswordPreference.OnDialogEventListener {
+
+    private static final String TAG = "AudioSharingPasswordPreferenceController";
+    private static final String PREF_KEY = "audio_sharing_stream_password";
+    private static final String SHARED_PREF_NAME = "audio_sharing_settings";
+    private static final String SHARED_PREF_KEY = "default_password";
+    @Nullable private final LocalBluetoothManager mBtManager;
+    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+    @Nullable private AudioSharingPasswordPreference mPreference;
+    private final AudioSharingPasswordValidator mAudioSharingPasswordValidator;
+
+    public AudioSharingPasswordPreferenceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mBtManager = Utils.getLocalBluetoothManager(context);
+        mBroadcast =
+                mBtManager != null
+                        ? mBtManager.getProfileManager().getLeAudioBroadcastProfile()
+                        : null;
+        mAudioSharingPasswordValidator = new AudioSharingPasswordValidator();
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreference = screen.findPreference(getPreferenceKey());
+        if (mPreference != null) {
+            mPreference.setValidator(this);
+            mPreference.setIsPassword(true);
+            mPreference.setDialogLayoutResource(R.layout.audio_sharing_password_dialog);
+            mPreference.setOnDialogEventListener(this);
+            updatePreference();
+        }
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return PREF_KEY;
+    }
+
+    @Override
+    public boolean isTextValid(String value) {
+        return mAudioSharingPasswordValidator.isTextValid(value);
+    }
+
+    @Override
+    public void onBindDialogView() {
+        if (mPreference == null || mBroadcast == null) {
+            return;
+        }
+        mPreference.setEditable(!isBroadcasting(mBtManager));
+        var password = mBroadcast.getBroadcastCode();
+        mPreference.setChecked(password == null || password.length == 0);
+    }
+
+    @Override
+    public void onPreferenceDataChanged(@NonNull String password, boolean isPublicBroadcast) {
+        if (mBroadcast == null || isBroadcasting(mBtManager)) {
+            Log.w(TAG, "onPreferenceDataChanged() changing password when broadcasting or null!");
+            return;
+        }
+        persistDefaultPassword(mContext, password);
+        mBroadcast.setBroadcastCode(isPublicBroadcast ? new byte[0] : password.getBytes());
+        updatePreference();
+    }
+
+    private void updatePreference() {
+        if (mBroadcast == null || mPreference == null) {
+            return;
+        }
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            byte[] password = mBroadcast.getBroadcastCode();
+                            boolean noPassword = (password == null || password.length == 0);
+                            String passwordToDisplay =
+                                    noPassword
+                                            ? getDefaultPassword(mContext)
+                                            : new String(password, StandardCharsets.UTF_8);
+                            String passwordSummary =
+                                    noPassword
+                                            ? mContext.getString(
+                                                    R.string.audio_streams_no_password_summary)
+                                            : "********";
+
+                            AudioSharingUtils.postOnMainThread(
+                                    mContext,
+                                    () -> {
+                                        // Check nullability to pass NullAway check
+                                        if (mPreference != null) {
+                                            mPreference.setText(passwordToDisplay);
+                                            mPreference.setSummary(passwordSummary);
+                                        }
+                                    });
+                        });
+    }
+
+    private static void persistDefaultPassword(Context context, String defaultPassword) {
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            if (getDefaultPassword(context).equals(defaultPassword)) {
+                                return;
+                            }
+
+                            SharedPreferences sharedPref =
+                                    context.getSharedPreferences(
+                                            SHARED_PREF_NAME, Context.MODE_PRIVATE);
+                            if (sharedPref == null) {
+                                Log.w(TAG, "persistDefaultPassword(): sharedPref is empty!");
+                                return;
+                            }
+
+                            SharedPreferences.Editor editor = sharedPref.edit();
+                            editor.putString(SHARED_PREF_KEY, defaultPassword);
+                            editor.apply();
+                        });
+    }
+
+    private static String getDefaultPassword(Context context) {
+        SharedPreferences sharedPref =
+                context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
+        if (sharedPref == null) {
+            Log.w(TAG, "getDefaultPassword(): sharedPref is empty!");
+            return "";
+        }
+
+        String value = sharedPref.getString(SHARED_PREF_KEY, "");
+        if (value != null && value.isEmpty()) {
+            Log.w(TAG, "getDefaultPassword(): default password is empty!");
+        }
+        return value;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java
new file mode 100644
index 0000000..dbb40ec
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidator.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import com.android.settings.widget.ValidatedEditTextPreference;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Validator for Audio Sharing Password, which should be a UTF-8 string that has at least 4 octets
+ * and should not exceed 16 octets.
+ */
+public class AudioSharingPasswordValidator implements ValidatedEditTextPreference.Validator {
+    private static final int MIN_OCTETS = 4;
+    private static final int MAX_OCTETS = 16;
+
+    @Override
+    public boolean isTextValid(String value) {
+        if (value == null
+                || getOctetsCount(value) < MIN_OCTETS
+                || getOctetsCount(value) > MAX_OCTETS) {
+            return false;
+        }
+
+        return isValidUTF8(value);
+    }
+
+    private static int getOctetsCount(String value) {
+        return value.getBytes(StandardCharsets.UTF_8).length;
+    }
+
+    private static boolean isValidUTF8(String value) {
+        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
+        String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
+        return value.equals(reconstructedString);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceController.java
new file mode 100644
index 0000000..e6e11af
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceController.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+
+public class AudioSharingPlaySoundPreferenceController
+        extends AudioSharingBasePreferenceController {
+
+    private static final String TAG = "AudioSharingPlaySoundPreferenceController";
+
+    private static final String PREF_KEY = "audio_sharing_play_sound";
+
+    private Ringtone mRingtone;
+
+    public AudioSharingPlaySoundPreferenceController(Context context) {
+        super(context, PREF_KEY);
+        mRingtone = RingtoneManager.getRingtone(context, getMediaVolumeUri());
+        if (mRingtone != null) {
+            mRingtone.setStreamType(AudioManager.STREAM_MUSIC);
+        }
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return (mRingtone != null && AudioSharingUtils.isFeatureEnabled())
+                ? AVAILABLE
+                : UNSUPPORTED_ON_DEVICE;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        if (mPreference != null) {
+            mPreference.setOnPreferenceClickListener(
+                    (v) -> {
+                        if (mRingtone == null) {
+                            Log.d(TAG, "Skip onClick due to ringtone is null");
+                            return true;
+                        }
+                        try {
+                            mRingtone.setAudioAttributes(
+                                    new AudioAttributes.Builder(mRingtone.getAudioAttributes())
+                                            .setFlags(AudioAttributes.FLAG_BYPASS_MUTE)
+                                            .addTag("VX_AOSP_SAMPLESOUND")
+                                            .build());
+                            if (!mRingtone.isPlaying()) {
+                                mRingtone.play();
+                            }
+                        } catch (Throwable e) {
+                            Log.w(TAG, "Fail to play sample, error = " + e);
+                        }
+                        return true;
+                    });
+        }
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        super.onStop(owner);
+        if (mRingtone != null && mRingtone.isPlaying()) {
+            mRingtone.stop();
+        }
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return PREF_KEY;
+    }
+
+    @VisibleForTesting
+    protected void setRingtone(Ringtone ringtone) {
+        mRingtone = ringtone;
+    }
+
+    private Uri getMediaVolumeUri() {
+        return Uri.parse(
+                ContentResolver.SCHEME_ANDROID_RESOURCE
+                        + "://"
+                        + mContext.getPackageName()
+                        + "/"
+                        + R.raw.media_volume);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceController.java
new file mode 100644
index 0000000..54eb722
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceController.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class AudioSharingPreferenceController extends BasePreferenceController
+        implements DefaultLifecycleObserver, BluetoothCallback {
+    private static final String TAG = "AudioSharingPreferenceController";
+
+    @Nullable private final LocalBluetoothManager mBtManager;
+    @Nullable private final BluetoothEventManager mEventManager;
+    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+    @Nullable private Preference mPreference;
+    private final Executor mExecutor;
+
+    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+            new BluetoothLeBroadcast.Callback() {
+                @Override
+                public void onBroadcastStarted(int reason, int broadcastId) {
+                    refreshSummary();
+                }
+
+                @Override
+                public void onBroadcastStartFailed(int reason) {}
+
+                @Override
+                public void onBroadcastMetadataChanged(
+                        int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {}
+
+                @Override
+                public void onBroadcastStopped(int reason, int broadcastId) {
+                    refreshSummary();
+                }
+
+                @Override
+                public void onBroadcastStopFailed(int reason) {}
+
+                @Override
+                public void onBroadcastUpdated(int reason, int broadcastId) {}
+
+                @Override
+                public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
+
+                @Override
+                public void onPlaybackStarted(int reason, int broadcastId) {}
+
+                @Override
+                public void onPlaybackStopped(int reason, int broadcastId) {}
+            };
+
+    public AudioSharingPreferenceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mBtManager = Utils.getLocalBtManager(context);
+        mEventManager = mBtManager == null ? null : mBtManager.getEventManager();
+        mBroadcast =
+                mBtManager == null
+                        ? null
+                        : mBtManager.getProfileManager().getLeAudioBroadcastProfile();
+        mExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip register callbacks, feature not support");
+            return;
+        }
+        if (mEventManager == null || mBroadcast == null) {
+            Log.d(TAG, "Skip register callbacks, profile not ready");
+            return;
+        }
+        mEventManager.registerCallback(this);
+        mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip unregister callbacks, feature not support");
+            return;
+        }
+        if (mEventManager == null || mBroadcast == null) {
+            Log.d(TAG, "Skip register callbacks, profile not ready");
+            return;
+        }
+        mEventManager.unregisterCallback(this);
+        mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+    }
+
+    @Override
+    public void displayPreference(@NonNull PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreference = screen.findPreference(getPreferenceKey());
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        return AudioSharingUtils.isBroadcasting(mBtManager)
+                ? mContext.getString(R.string.audio_sharing_summary_on)
+                : mContext.getString(R.string.audio_sharing_summary_off);
+    }
+
+    @Override
+    public void onBluetoothStateChanged(@AdapterState int bluetoothState) {
+        refreshSummary();
+    }
+
+    private void refreshSummary() {
+        if (mPreference == null) {
+            return;
+        }
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            final CharSequence summary = getSummary();
+                            AudioSharingUtils.postOnMainThread(
+                                    mContext,
+                                    () -> {
+                                        // Check nullability to pass NullAway check
+                                        if (mPreference != null) {
+                                            mPreference.setSummary(summary);
+                                        }
+                                    });
+                        });
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiver.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiver.java
new file mode 100644
index 0000000..eda4256
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiver.java
@@ -0,0 +1,149 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+public class AudioSharingReceiver extends BroadcastReceiver {
+    private static final String TAG = "AudioSharingNotification";
+    private static final String ACTION_LE_AUDIO_SHARING_SETTINGS =
+            "com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS";
+    private static final String ACTION_LE_AUDIO_SHARING_STOP =
+            "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STOP";
+    private static final String CHANNEL_ID = "bluetooth_notification_channel";
+    private static final int NOTIFICATION_ID =
+            com.android.settingslib.R.drawable.ic_bt_le_audio_sharing;
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (!AudioSharingUtils.isFeatureEnabled()) {
+            Log.w(TAG, "Skip handling received intent, flag is off.");
+            return;
+        }
+        String action = intent.getAction();
+        if (action == null) {
+            Log.w(TAG, "Received unexpected intent with null action.");
+            return;
+        }
+        switch (action) {
+            case LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_STATE_CHANGE:
+                int state =
+                        intent.getIntExtra(
+                                LocalBluetoothLeBroadcast.EXTRA_LE_AUDIO_SHARING_STATE, -1);
+                if (state == LocalBluetoothLeBroadcast.BROADCAST_STATE_ON) {
+                    showSharingNotification(context);
+                } else if (state == LocalBluetoothLeBroadcast.BROADCAST_STATE_OFF) {
+                    cancelSharingNotification(context);
+                } else {
+                    Log.w(
+                            TAG,
+                            "Skip handling ACTION_LE_AUDIO_SHARING_STATE_CHANGE, invalid extras.");
+                }
+                break;
+            case ACTION_LE_AUDIO_SHARING_STOP:
+                LocalBluetoothManager manager = Utils.getLocalBtManager(context);
+                AudioSharingUtils.stopBroadcasting(manager);
+                break;
+            default:
+                Log.w(TAG, "Received unexpected intent " + intent.getAction());
+        }
+    }
+
+    private void showSharingNotification(Context context) {
+        NotificationManager nm = context.getSystemService(NotificationManager.class);
+        if (nm.getNotificationChannel(CHANNEL_ID) == null) {
+            Log.d(TAG, "Create bluetooth notification channel");
+            NotificationChannel notificationChannel =
+                    new NotificationChannel(
+                            CHANNEL_ID,
+                            context.getString(com.android.settings.R.string.bluetooth),
+                            NotificationManager.IMPORTANCE_HIGH);
+            nm.createNotificationChannel(notificationChannel);
+        }
+        Intent stopIntent =
+                new Intent(ACTION_LE_AUDIO_SHARING_STOP).setPackage(context.getPackageName());
+        PendingIntent stopPendingIntent =
+                PendingIntent.getBroadcast(
+                        context,
+                        R.string.audio_sharing_stop_button_label,
+                        stopIntent,
+                        PendingIntent.FLAG_IMMUTABLE);
+        Intent settingsIntent =
+                new Intent(ACTION_LE_AUDIO_SHARING_SETTINGS).setPackage(context.getPackageName());
+        PendingIntent settingsPendingIntent =
+                PendingIntent.getActivity(
+                        context,
+                        R.string.audio_sharing_settings_button_label,
+                        settingsIntent,
+                        PendingIntent.FLAG_IMMUTABLE);
+        NotificationCompat.Action stopAction =
+                new NotificationCompat.Action.Builder(
+                                0,
+                                context.getString(R.string.audio_sharing_stop_button_label),
+                                stopPendingIntent)
+                        .build();
+        NotificationCompat.Action settingsAction =
+                new NotificationCompat.Action.Builder(
+                                0,
+                                context.getString(R.string.audio_sharing_settings_button_label),
+                                settingsPendingIntent)
+                        .build();
+        final Bundle extras = new Bundle();
+        extras.putString(
+                Notification.EXTRA_SUBSTITUTE_APP_NAME,
+                context.getString(R.string.audio_sharing_title));
+        NotificationCompat.Builder builder =
+                new NotificationCompat.Builder(context, CHANNEL_ID)
+                        .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
+                        .setLocalOnly(true)
+                        .setContentTitle(
+                                context.getString(R.string.audio_sharing_notification_title))
+                        .setContentText(
+                                context.getString(R.string.audio_sharing_notification_content))
+                        .setOngoing(true)
+                        .setSilent(true)
+                        .setColor(
+                                context.getColor(
+                                        com.android.internal.R.color
+                                                .system_notification_accent_color))
+                        .setContentIntent(settingsPendingIntent)
+                        .addAction(stopAction)
+                        .addAction(settingsAction)
+                        .addExtras(extras);
+        nm.notify(NOTIFICATION_ID, builder.build());
+    }
+
+    private void cancelSharingNotification(Context context) {
+        NotificationManager nm = context.getSystemService(NotificationManager.class);
+        nm.cancel(NOTIFICATION_ID);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java
new file mode 100644
index 0000000..affd54a
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+
+import com.google.common.collect.Iterables;
+
+import java.util.List;
+import java.util.Locale;
+
+public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
+    private static final String TAG = "AudioSharingStopDialog";
+
+    private static final String BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS =
+            "bundle_key_device_to_disconnect_items";
+    private static final String BUNDLE_KEY_NEW_DEVICE_NAME = "bundle_key_new_device_name";
+
+    // The host creates an instance of this dialog fragment must implement this interface to receive
+    // event callbacks.
+    public interface DialogEventListener {
+        /** Called when users click the stop sharing button in the dialog. */
+        void onStopSharingClick();
+    }
+
+    @Nullable private static DialogEventListener sListener;
+    @Nullable private static CachedBluetoothDevice sCachedDevice;
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.DIALOG_STOP_AUDIO_SHARING;
+    }
+
+    /**
+     * Display the {@link AudioSharingStopDialogFragment} dialog.
+     *
+     * <p>If the dialog is showing, update the dialog message and event listener.
+     *
+     * @param host The Fragment this dialog will be hosted.
+     * @param deviceItems The existing connected device items in audio sharing session.
+     * @param newDevice The latest connected device triggered this dialog.
+     * @param listener The callback to handle the user action on this dialog.
+     */
+    public static void show(
+            @NonNull Fragment host,
+            @NonNull List<AudioSharingDeviceItem> deviceItems,
+            @NonNull CachedBluetoothDevice newDevice,
+            @NonNull DialogEventListener listener) {
+        if (!AudioSharingUtils.isFeatureEnabled()) return;
+        final FragmentManager manager = host.getChildFragmentManager();
+        AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
+        if (dialog != null) {
+            int newGroupId = AudioSharingUtils.getGroupId(newDevice);
+            if (sCachedDevice != null
+                    && newGroupId == AudioSharingUtils.getGroupId(sCachedDevice)) {
+                Log.d(
+                        TAG,
+                        String.format(
+                                Locale.US,
+                                "Dialog is showing for the same device group %d, return.",
+                                newGroupId));
+                sListener = listener;
+                sCachedDevice = newDevice;
+                return;
+            } else {
+                Log.d(
+                        TAG,
+                        String.format(
+                                Locale.US,
+                                "Dialog is showing for new device group %d, "
+                                        + "dismiss current dialog.",
+                                newGroupId));
+                dialog.dismiss();
+            }
+        }
+        sListener = listener;
+        sCachedDevice = newDevice;
+        Log.d(TAG, "Show up the dialog.");
+        final Bundle bundle = new Bundle();
+        bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
+        bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
+        AudioSharingStopDialogFragment dialogFrag = new AudioSharingStopDialogFragment();
+        dialogFrag.setArguments(bundle);
+        dialogFrag.show(manager, TAG);
+    }
+
+    /** Return the tag of {@link AudioSharingStopDialogFragment} dialog. */
+    public static @NonNull String tag() {
+        return TAG;
+    }
+
+    /** Get the latest connected device which triggers the dialog. */
+    public @Nullable CachedBluetoothDevice getDevice() {
+        return sCachedDevice;
+    }
+
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        Bundle arguments = requireArguments();
+        List<AudioSharingDeviceItem> deviceItems =
+                arguments.getParcelable(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, List.class);
+        String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
+        String customMessage =
+                deviceItems.size() == 1
+                        ? getString(
+                                R.string.audio_sharing_stop_dialog_content,
+                                Iterables.getOnlyElement(deviceItems).getName())
+                        : (deviceItems.size() == 2
+                                ? getString(
+                                        R.string.audio_sharing_stop_dialog_with_two_content,
+                                        deviceItems.get(0).getName(),
+                                        deviceItems.get(1).getName())
+                                : getString(R.string.audio_sharing_stop_dialog_with_more_content));
+        AlertDialog dialog =
+                AudioSharingDialogFactory.newBuilder(getActivity())
+                        .setTitle(
+                                getString(R.string.audio_sharing_stop_dialog_title, newDeviceName))
+                        .setTitleIcon(com.android.settings.R.drawable.ic_warning_24dp)
+                        .setIsCustomBodyEnabled(true)
+                        .setCustomMessage(customMessage)
+                        .setPositiveButton(
+                                R.string.audio_sharing_connect_button_label,
+                                (dlg, which) -> {
+                                    if (sListener != null) {
+                                        sListener.onStopSharingClick();
+                                    }
+                                })
+                        .setNegativeButton(
+                                com.android.settings.R.string.cancel, (dlg, which) -> dismiss())
+                        .build();
+        dialog.show();
+        AudioSharingDialogHelper.updateMessageStyle(dialog);
+        return dialog;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java
new file mode 100644
index 0000000..df49de4
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java
@@ -0,0 +1,522 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.widget.SettingsMainSwitchBar;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+public class AudioSharingSwitchBarController extends BasePreferenceController
+        implements DefaultLifecycleObserver,
+                OnCheckedChangeListener,
+                LocalBluetoothProfileManager.ServiceListener {
+    private static final String TAG = "AudioSharingSwitchBarCtl";
+    private static final String PREF_KEY = "audio_sharing_main_switch";
+
+    interface OnAudioSharingStateChangedListener {
+        /**
+         * The callback which will be triggered when:
+         *
+         * <p>1. Bluetooth on/off state changes. 2. Broadcast and assistant profile
+         * connect/disconnect state changes. 3. Audio sharing start/stop state changes.
+         */
+        void onAudioSharingStateChanged();
+
+        /**
+         * The callback which will be triggered when:
+         *
+         * <p>Broadcast and assistant profile connected.
+         */
+        void onAudioSharingProfilesConnected();
+    }
+
+    private final SettingsMainSwitchBar mSwitchBar;
+    private final BluetoothAdapter mBluetoothAdapter;
+    @Nullable private final LocalBluetoothManager mBtManager;
+    @Nullable private final LocalBluetoothProfileManager mProfileManager;
+    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
+    @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Nullable private DashboardFragment mFragment;
+    private final Executor mExecutor;
+    private final OnAudioSharingStateChangedListener mListener;
+    private Map<Integer, List<CachedBluetoothDevice>> mGroupedConnectedDevices = new HashMap<>();
+    private List<BluetoothDevice> mTargetActiveSinks = new ArrayList<>();
+    private List<AudioSharingDeviceItem> mDeviceItemsForSharing = new ArrayList<>();
+    @VisibleForTesting IntentFilter mIntentFilter;
+    private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
+
+    @VisibleForTesting
+    BroadcastReceiver mReceiver =
+            new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    updateSwitch();
+                    mListener.onAudioSharingStateChanged();
+                }
+            };
+
+    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+            new BluetoothLeBroadcast.Callback() {
+                @Override
+                public void onBroadcastStarted(int reason, int broadcastId) {
+                    Log.d(
+                            TAG,
+                            "onBroadcastStarted(), reason = "
+                                    + reason
+                                    + ", broadcastId = "
+                                    + broadcastId);
+                    updateSwitch();
+                    mListener.onAudioSharingStateChanged();
+                }
+
+                @Override
+                public void onBroadcastStartFailed(int reason) {
+                    Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
+                    // TODO: handle broadcast start fail
+                    updateSwitch();
+                }
+
+                @Override
+                public void onBroadcastMetadataChanged(
+                        int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {
+                    Log.d(
+                            TAG,
+                            "onBroadcastMetadataChanged(), broadcastId = "
+                                    + broadcastId
+                                    + ", metadata = "
+                                    + metadata.getBroadcastName());
+                }
+
+                @Override
+                public void onBroadcastStopped(int reason, int broadcastId) {
+                    Log.d(
+                            TAG,
+                            "onBroadcastStopped(), reason = "
+                                    + reason
+                                    + ", broadcastId = "
+                                    + broadcastId);
+                    updateSwitch();
+                    mListener.onAudioSharingStateChanged();
+                }
+
+                @Override
+                public void onBroadcastStopFailed(int reason) {
+                    Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
+                    // TODO: handle broadcast stop fail
+                    updateSwitch();
+                }
+
+                @Override
+                public void onBroadcastUpdated(int reason, int broadcastId) {}
+
+                @Override
+                public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
+
+                @Override
+                public void onPlaybackStarted(int reason, int broadcastId) {
+                    Log.d(
+                            TAG,
+                            "onPlaybackStarted(), reason = "
+                                    + reason
+                                    + ", broadcastId = "
+                                    + broadcastId);
+                    handleOnBroadcastReady();
+                }
+
+                @Override
+                public void onPlaybackStopped(int reason, int broadcastId) {}
+            };
+
+    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+            new BluetoothLeBroadcastAssistant.Callback() {
+                @Override
+                public void onSearchStarted(int reason) {}
+
+                @Override
+                public void onSearchStartFailed(int reason) {}
+
+                @Override
+                public void onSearchStopped(int reason) {}
+
+                @Override
+                public void onSearchStopFailed(int reason) {}
+
+                @Override
+                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
+
+                @Override
+                public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
+                    Log.d(
+                            TAG,
+                            "onSourceAdded(), sink = "
+                                    + sink
+                                    + ", sourceId = "
+                                    + sourceId
+                                    + ", reason = "
+                                    + reason);
+                }
+
+                @Override
+                public void onSourceAddFailed(
+                        @NonNull BluetoothDevice sink,
+                        @NonNull BluetoothLeBroadcastMetadata source,
+                        int reason) {
+                    Log.d(
+                            TAG,
+                            "onSourceAddFailed(), sink = "
+                                    + sink
+                                    + ", source = "
+                                    + source
+                                    + ", reason = "
+                                    + reason);
+                    AudioSharingUtils.toastMessage(
+                            mContext,
+                            String.format(
+                                    Locale.US,
+                                    "Fail to add source to %s reason %d",
+                                    sink.getAddress(),
+                                    reason));
+                }
+
+                @Override
+                public void onSourceModified(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceModifyFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceRemoved(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceRemoveFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onReceiveStateChanged(
+                        BluetoothDevice sink,
+                        int sourceId,
+                        BluetoothLeBroadcastReceiveState state) {}
+            };
+
+    AudioSharingSwitchBarController(
+            Context context,
+            SettingsMainSwitchBar switchBar,
+            OnAudioSharingStateChangedListener listener) {
+        super(context, PREF_KEY);
+        mSwitchBar = switchBar;
+        mListener = listener;
+        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+        mBtManager = Utils.getLocalBtManager(context);
+        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
+        mBroadcast = mProfileManager == null ? null : mProfileManager.getLeAudioBroadcastProfile();
+        mAssistant =
+                mProfileManager == null
+                        ? null
+                        : mProfileManager.getLeAudioBroadcastAssistantProfile();
+        mExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip register callbacks. Feature is not available.");
+            return;
+        }
+        mContext.registerReceiver(mReceiver, mIntentFilter, Context.RECEIVER_EXPORTED_UNAUDITED);
+        updateSwitch();
+        if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            if (mProfileManager != null) {
+                mProfileManager.addServiceListener(this);
+            }
+            Log.d(TAG, "Skip register callbacks. Profile is not ready.");
+            return;
+        }
+        registerCallbacks();
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip unregister callbacks. Feature is not available.");
+            return;
+        }
+        mContext.unregisterReceiver(mReceiver);
+        if (mProfileManager != null) {
+            mProfileManager.removeServiceListener(this);
+        }
+        unregisterCallbacks();
+    }
+
+    @Override
+    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+        // Filter out unnecessary callbacks when switch is disabled.
+        if (!buttonView.isEnabled()) return;
+        if (isChecked) {
+            mSwitchBar.setEnabled(false);
+            boolean isBroadcasting = AudioSharingUtils.isBroadcasting(mBtManager);
+            if (mAssistant == null || mBroadcast == null || isBroadcasting) {
+                Log.d(TAG, "Skip startAudioSharing, already broadcasting or not support.");
+                mSwitchBar.setEnabled(true);
+                if (!isBroadcasting) {
+                    mSwitchBar.setChecked(false);
+                }
+                return;
+            }
+            if (mAssistant
+                    .getDevicesMatchingConnectionStates(
+                            new int[] {BluetoothProfile.STATE_CONNECTED})
+                    .isEmpty()) {
+                // Pop up dialog to ask users to connect at least one lea buds before audio sharing.
+                AudioSharingUtils.postOnMainThread(
+                        mContext,
+                        () -> {
+                            mSwitchBar.setEnabled(true);
+                            mSwitchBar.setChecked(false);
+                            if (mFragment != null) {
+                                AudioSharingConfirmDialogFragment.show(mFragment);
+                            }
+                        });
+                return;
+            }
+            startAudioSharing();
+        } else {
+            stopAudioSharing();
+        }
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+    }
+
+    @Override
+    public void onServiceConnected() {
+        Log.d(TAG, "onServiceConnected()");
+        if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            registerCallbacks();
+            updateSwitch();
+            mListener.onAudioSharingProfilesConnected();
+            mListener.onAudioSharingStateChanged();
+            if (mProfileManager != null) {
+                mProfileManager.removeServiceListener(this);
+            }
+        }
+    }
+
+    @Override
+    public void onServiceDisconnected() {
+        Log.d(TAG, "onServiceDisconnected()");
+        // Do nothing.
+    }
+
+    /**
+     * Initialize the controller.
+     *
+     * @param fragment The fragment to host the {@link AudioSharingSwitchBarController} dialog.
+     */
+    public void init(DashboardFragment fragment) {
+        this.mFragment = fragment;
+    }
+
+    /** Test only: set callback registration status in tests. */
+    @VisibleForTesting
+    public void setCallbacksRegistered(boolean registered) {
+        mCallbacksRegistered.set(registered);
+    }
+
+    private void registerCallbacks() {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip registerCallbacks(). Feature is not available.");
+            return;
+        }
+        if (mBroadcast == null || mAssistant == null) {
+            Log.d(TAG, "Skip registerCallbacks(). Profile not support on this device.");
+            return;
+        }
+        if (!mCallbacksRegistered.get()) {
+            Log.d(TAG, "registerCallbacks()");
+            mSwitchBar.addOnSwitchChangeListener(this);
+            mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
+            mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+            mCallbacksRegistered.set(true);
+        }
+    }
+
+    private void unregisterCallbacks() {
+        if (!isAvailable() || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            Log.d(TAG, "Skip unregisterCallbacks(). Feature is not available.");
+            return;
+        }
+        if (mBroadcast == null || mAssistant == null) {
+            Log.d(TAG, "Skip unregisterCallbacks(). Profile not support on this device.");
+            return;
+        }
+        if (mCallbacksRegistered.get()) {
+            Log.d(TAG, "unregisterCallbacks()");
+            mSwitchBar.removeOnSwitchChangeListener(this);
+            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
+            mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+            mCallbacksRegistered.set(false);
+        }
+    }
+
+    private void startAudioSharing() {
+        // Compute the device connection state before start audio sharing since the devices will
+        // be set to inactive after the broadcast started.
+        mGroupedConnectedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(mBtManager);
+        List<AudioSharingDeviceItem> deviceItems =
+                AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
+                        mBtManager, mGroupedConnectedDevices, /* filterByInSharing= */ false);
+        // deviceItems is ordered. The active device is the first place if exits.
+        mDeviceItemsForSharing = new ArrayList<>(deviceItems);
+        mTargetActiveSinks = new ArrayList<>();
+        if (!deviceItems.isEmpty() && deviceItems.get(0).isActive()) {
+            for (CachedBluetoothDevice device :
+                    mGroupedConnectedDevices.getOrDefault(
+                            deviceItems.get(0).getGroupId(), ImmutableList.of())) {
+                // If active device exists for audio sharing, share to it
+                // automatically once the broadcast is started.
+                mTargetActiveSinks.add(device.getDevice());
+            }
+            mDeviceItemsForSharing.remove(0);
+        }
+        if (mBroadcast != null) {
+            mBroadcast.startPrivateBroadcast();
+        }
+    }
+
+    private void stopAudioSharing() {
+        mSwitchBar.setEnabled(false);
+        if (!AudioSharingUtils.isBroadcasting(mBtManager)) {
+            Log.d(TAG, "Skip stopAudioSharing, already not broadcasting or broadcast not support.");
+            mSwitchBar.setEnabled(true);
+            return;
+        }
+        if (mBroadcast != null) {
+            mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId());
+        }
+    }
+
+    private void updateSwitch() {
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            boolean isBroadcasting = AudioSharingUtils.isBroadcasting(mBtManager);
+                            boolean isStateReady =
+                                    isBluetoothOn()
+                                            && AudioSharingUtils.isAudioSharingProfileReady(
+                                                    mProfileManager);
+                            AudioSharingUtils.postOnMainThread(
+                                    mContext,
+                                    () -> {
+                                        if (mSwitchBar.isChecked() != isBroadcasting) {
+                                            mSwitchBar.setChecked(isBroadcasting);
+                                        }
+                                        if (mSwitchBar.isEnabled() != isStateReady) {
+                                            mSwitchBar.setEnabled(isStateReady);
+                                        }
+                                        Log.d(
+                                                TAG,
+                                                "updateSwitch, checked = "
+                                                        + isBroadcasting
+                                                        + ", enabled = "
+                                                        + isStateReady);
+                                    });
+                        });
+    }
+
+    private boolean isBluetoothOn() {
+        return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
+    }
+
+    private void handleOnBroadcastReady() {
+        AudioSharingUtils.addSourceToTargetSinks(mTargetActiveSinks, mBtManager);
+        mTargetActiveSinks.clear();
+        if (mFragment == null) {
+            Log.w(TAG, "Dialog fail to show due to null fragment.");
+            mGroupedConnectedDevices.clear();
+            mDeviceItemsForSharing.clear();
+            return;
+        }
+        AudioSharingUtils.postOnMainThread(
+                mContext,
+                () -> {
+                    // Check nullability to pass NullAway check
+                    if (mFragment != null) {
+                        AudioSharingDialogFragment.show(
+                                mFragment,
+                                mDeviceItemsForSharing,
+                                item -> {
+                                    AudioSharingUtils.addSourceToTargetSinks(
+                                            mGroupedConnectedDevices
+                                                    .getOrDefault(
+                                                            item.getGroupId(), ImmutableList.of())
+                                                    .stream()
+                                                    .map(CachedBluetoothDevice::getDevice)
+                                                    .collect(Collectors.toList()),
+                                            mBtManager);
+                                    mGroupedConnectedDevices.clear();
+                                    mDeviceItemsForSharing.clear();
+                                });
+                    }
+                });
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
new file mode 100644
index 0000000..f63717e
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
+import android.provider.Settings;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LeAudioProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+import com.android.settingslib.flags.Flags;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class AudioSharingUtils {
+    public static final String SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID =
+            "bluetooth_le_broadcast_fallback_active_group_id";
+    private static final String TAG = "AudioSharingUtils";
+    private static final boolean DEBUG = BluetoothUtils.D;
+
+    /**
+     * Fetch {@link CachedBluetoothDevice}s connected to the broadcast assistant. The devices are
+     * grouped by CSIP group id.
+     *
+     * @param localBtManager The BT manager to provide BT functions.
+     * @return A map of connected devices grouped by CSIP group id.
+     */
+    public static Map<Integer, List<CachedBluetoothDevice>> fetchConnectedDevicesByGroupId(
+            @Nullable LocalBluetoothManager localBtManager) {
+        Map<Integer, List<CachedBluetoothDevice>> groupedDevices = new HashMap<>();
+        if (localBtManager == null) {
+            Log.d(TAG, "Skip fetchConnectedDevicesByGroupId due to bt manager is null");
+            return groupedDevices;
+        }
+        LocalBluetoothLeBroadcastAssistant assistant =
+                localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
+        if (assistant == null) {
+            Log.d(TAG, "Skip fetchConnectedDevicesByGroupId due to assistant profile is null");
+            return groupedDevices;
+        }
+        List<BluetoothDevice> connectedDevices =
+                assistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED});
+        CachedBluetoothDeviceManager cacheManager = localBtManager.getCachedDeviceManager();
+        for (BluetoothDevice device : connectedDevices) {
+            CachedBluetoothDevice cachedDevice = cacheManager.findDevice(device);
+            if (cachedDevice == null) {
+                Log.d(TAG, "Skip device due to not being cached: " + device.getAnonymizedAddress());
+                continue;
+            }
+            int groupId = getGroupId(cachedDevice);
+            if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+                Log.d(
+                        TAG,
+                        "Skip device due to no valid group id: " + device.getAnonymizedAddress());
+                continue;
+            }
+            if (!groupedDevices.containsKey(groupId)) {
+                groupedDevices.put(groupId, new ArrayList<>());
+            }
+            groupedDevices.get(groupId).add(cachedDevice);
+        }
+        if (DEBUG) {
+            Log.d(TAG, "fetchConnectedDevicesByGroupId: " + groupedDevices);
+        }
+        return groupedDevices;
+    }
+
+    /**
+     * Fetch a list of ordered connected lead {@link CachedBluetoothDevice}s eligible for audio
+     * sharing. The active device is placed in the first place if it exists. The devices can be
+     * filtered by whether it is already in the audio sharing session.
+     *
+     * @param localBtManager The BT manager to provide BT functions. *
+     * @param groupedConnectedDevices devices connected to broadcast assistant grouped by CSIP group
+     *     id.
+     * @param filterByInSharing Whether to filter the device by if is already in the sharing
+     *     session.
+     * @return A list of ordered connected devices eligible for the audio sharing. The active device
+     *     is placed in the first place if it exists.
+     */
+    public static List<CachedBluetoothDevice> buildOrderedConnectedLeadDevices(
+            @Nullable LocalBluetoothManager localBtManager,
+            Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices,
+            boolean filterByInSharing) {
+        List<CachedBluetoothDevice> orderedDevices = new ArrayList<>();
+        for (List<CachedBluetoothDevice> devices : groupedConnectedDevices.values()) {
+            @Nullable CachedBluetoothDevice leadDevice = getLeadDevice(devices);
+            if (leadDevice == null) {
+                Log.d(TAG, "Skip due to no lead device");
+                continue;
+            }
+            if (filterByInSharing
+                    && !BluetoothUtils.hasConnectedBroadcastSource(leadDevice, localBtManager)) {
+                Log.d(
+                        TAG,
+                        "Filtered the device due to not in sharing session: "
+                                + leadDevice.getDevice().getAnonymizedAddress());
+                continue;
+            }
+            orderedDevices.add(leadDevice);
+        }
+        orderedDevices.sort(
+                (CachedBluetoothDevice d1, CachedBluetoothDevice d2) -> {
+                    // Active above not inactive
+                    int comparison =
+                            (isActiveLeAudioDevice(d2) ? 1 : 0)
+                                    - (isActiveLeAudioDevice(d1) ? 1 : 0);
+                    if (comparison != 0) return comparison;
+                    // Bonded above not bonded
+                    comparison =
+                            (d2.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0)
+                                    - (d1.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
+                    if (comparison != 0) return comparison;
+                    // Bond timestamp available above unavailable
+                    comparison =
+                            (d2.getBondTimestamp() != null ? 1 : 0)
+                                    - (d1.getBondTimestamp() != null ? 1 : 0);
+                    if (comparison != 0) return comparison;
+                    // Order by bond timestamp if it is available
+                    // Otherwise order by device name
+                    return d1.getBondTimestamp() != null
+                            ? d1.getBondTimestamp().compareTo(d2.getBondTimestamp())
+                            : d1.getName().compareTo(d2.getName());
+                });
+        return orderedDevices;
+    }
+
+    /**
+     * Get the lead device from a list of devices with same group id.
+     *
+     * @param devices A list of devices with same group id.
+     * @return The lead device
+     */
+    @Nullable
+    public static CachedBluetoothDevice getLeadDevice(
+            @NonNull List<CachedBluetoothDevice> devices) {
+        if (devices.isEmpty()) return null;
+        for (CachedBluetoothDevice device : devices) {
+            if (!device.getMemberDevice().isEmpty()) {
+                return device;
+            }
+        }
+        CachedBluetoothDevice leadDevice = devices.get(0);
+        Log.d(
+                TAG,
+                "No lead device in the group, pick arbitrary device as the lead: "
+                        + leadDevice.getDevice().getAnonymizedAddress());
+        return leadDevice;
+    }
+
+    /**
+     * Fetch a list of ordered connected lead {@link AudioSharingDeviceItem}s eligible for audio
+     * sharing. The active device is placed in the first place if it exists. The devices can be
+     * filtered by whether it is already in the audio sharing session.
+     *
+     * @param localBtManager The BT manager to provide BT functions. *
+     * @param groupedConnectedDevices devices connected to broadcast assistant grouped by CSIP group
+     *     id.
+     * @param filterByInSharing Whether to filter the device by if is already in the sharing
+     *     session.
+     * @return A list of ordered connected devices eligible for the audio sharing. The active device
+     *     is placed in the first place if it exists.
+     */
+    @NonNull
+    public static List<AudioSharingDeviceItem> buildOrderedConnectedLeadAudioSharingDeviceItem(
+            @Nullable LocalBluetoothManager localBtManager,
+            Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices,
+            boolean filterByInSharing) {
+        return buildOrderedConnectedLeadDevices(
+                        localBtManager, groupedConnectedDevices, filterByInSharing)
+                .stream()
+                .map(device -> buildAudioSharingDeviceItem(device))
+                .collect(Collectors.toList());
+    }
+
+    /** Build {@link AudioSharingDeviceItem} from {@link CachedBluetoothDevice}. */
+    public static AudioSharingDeviceItem buildAudioSharingDeviceItem(
+            CachedBluetoothDevice cachedDevice) {
+        return new AudioSharingDeviceItem(
+                cachedDevice.getName(),
+                getGroupId(cachedDevice),
+                isActiveLeAudioDevice(cachedDevice));
+    }
+
+    /**
+     * Check if {@link CachedBluetoothDevice} is an active le audio device.
+     *
+     * @param cachedDevice The cached bluetooth device to check.
+     * @return Whether the device is an active le audio device.
+     */
+    public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) {
+        return BluetoothUtils.isActiveLeAudioDevice(cachedDevice);
+    }
+
+    /** Toast message on main thread. */
+    public static void toastMessage(Context context, String message) {
+        context.getMainExecutor()
+                .execute(() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show());
+    }
+
+    /** Returns if the le audio sharing is enabled. */
+    public static boolean isFeatureEnabled() {
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        return Flags.enableLeAudioSharing()
+                && adapter.isLeAudioBroadcastSourceSupported()
+                        == BluetoothStatusCodes.FEATURE_SUPPORTED
+                && adapter.isLeAudioBroadcastAssistantSupported()
+                        == BluetoothStatusCodes.FEATURE_SUPPORTED;
+    }
+
+    /** Add source to target sinks. */
+    public static void addSourceToTargetSinks(
+            List<BluetoothDevice> sinks, @Nullable LocalBluetoothManager localBtManager) {
+        if (localBtManager == null) {
+            Log.d(TAG, "skip addSourceToTargetDevices: LocalBluetoothManager is null!");
+            return;
+        }
+        if (sinks.isEmpty()) {
+            Log.d(TAG, "Skip addSourceToTargetDevices. No sinks.");
+            return;
+        }
+        LocalBluetoothLeBroadcast broadcast =
+                localBtManager.getProfileManager().getLeAudioBroadcastProfile();
+        if (broadcast == null) {
+            Log.d(TAG, "skip addSourceToTargetDevices. Broadcast profile is null.");
+            return;
+        }
+        LocalBluetoothLeBroadcastAssistant assistant =
+                localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
+        if (assistant == null) {
+            Log.d(TAG, "skip addSourceToTargetDevices. Assistant profile is null.");
+            return;
+        }
+        BluetoothLeBroadcastMetadata broadcastMetadata =
+                broadcast.getLatestBluetoothLeBroadcastMetadata();
+        if (broadcastMetadata == null) {
+            Log.d(TAG, "skip addSourceToTargetDevices: There is no broadcastMetadata.");
+            return;
+        }
+        List<BluetoothDevice> connectedDevices =
+                assistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED});
+        for (BluetoothDevice sink : sinks) {
+            if (connectedDevices.contains(sink)) {
+                Log.d(
+                        TAG,
+                        "Add broadcast with broadcastId: "
+                                + broadcastMetadata.getBroadcastId()
+                                + " to the device: "
+                                + sink.getAnonymizedAddress());
+                assistant.addSource(sink, broadcastMetadata, /* isGroupOp= */ false);
+            } else {
+                Log.d(
+                        TAG,
+                        "Skip add broadcast with broadcastId: "
+                                + broadcastMetadata.getBroadcastId()
+                                + " to the not connected device: "
+                                + sink.getAnonymizedAddress());
+            }
+        }
+    }
+
+    /** Returns if the broadcast is on-going. */
+    public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
+        if (manager == null) return false;
+        LocalBluetoothLeBroadcast broadcast =
+                manager.getProfileManager().getLeAudioBroadcastProfile();
+        return broadcast != null && broadcast.isEnabled(null);
+    }
+
+    /** Stops the latest broadcast. */
+    public static void stopBroadcasting(@Nullable LocalBluetoothManager manager) {
+        if (manager == null) {
+            Log.d(TAG, "Skip stop broadcasting due to bt manager is null");
+            return;
+        }
+        LocalBluetoothLeBroadcast broadcast =
+                manager.getProfileManager().getLeAudioBroadcastProfile();
+        if (broadcast == null) {
+            Log.d(TAG, "Skip stop broadcasting due to broadcast profile is null");
+        }
+        broadcast.stopBroadcast(broadcast.getLatestBroadcastId());
+    }
+
+    /**
+     * Get CSIP group id for {@link CachedBluetoothDevice}.
+     *
+     * <p>If CachedBluetoothDevice#getGroupId is invalid, fetch group id from
+     * LeAudioProfile#getGroupId.
+     */
+    public static int getGroupId(CachedBluetoothDevice cachedDevice) {
+        int groupId = cachedDevice.getGroupId();
+        String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress();
+        if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+            Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress);
+            return groupId;
+        }
+        for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
+            if (profile instanceof LeAudioProfile) {
+                Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress);
+                return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice());
+            }
+        }
+        Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress);
+        return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+    }
+
+    /** Get the fallback active group id from SettingsProvider. */
+    public static int getFallbackActiveGroupId(@NonNull Context context) {
+        return Settings.Secure.getInt(
+                context.getContentResolver(),
+                SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID,
+                BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+    }
+
+    /** Post the runnable to main thread. */
+    public static void postOnMainThread(@NonNull Context context, @NonNull Runnable runnable) {
+        context.getMainExecutor().execute(runnable);
+    }
+
+    /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */
+    public static boolean isLeAudioSupported(CachedBluetoothDevice cachedDevice) {
+        return cachedDevice.getProfiles().stream()
+                .anyMatch(
+                        profile ->
+                                profile instanceof LeAudioProfile
+                                        && profile.isEnabled(cachedDevice.getDevice()));
+    }
+
+    /** Check if the LE Audio related profiles ready */
+    public static boolean isAudioSharingProfileReady(
+            @Nullable LocalBluetoothProfileManager profileManager) {
+        if (profileManager == null) return false;
+        LocalBluetoothLeBroadcast broadcast = profileManager.getLeAudioBroadcastProfile();
+        if (broadcast == null || !broadcast.isProfileReady()) {
+            return false;
+        }
+        LocalBluetoothLeBroadcastAssistant assistant =
+                profileManager.getLeAudioBroadcastAssistantProfile();
+        if (assistant == null || !assistant.isProfileReady()) {
+            return false;
+        }
+        VolumeControlProfile vc = profileManager.getVolumeControlProfile();
+        if (vc == null || !vc.isProfileReady()) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java
new file mode 100644
index 0000000..df94694
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+import java.util.List;
+
+/** Provides a dialog to choose the active device for calls and alarms. */
+public class CallsAndAlarmsDialogFragment extends InstrumentedDialogFragment {
+    private static final String TAG = "CallsAndAlarmsDialog";
+    private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items";
+
+    // The host creates an instance of this dialog fragment must implement this interface to receive
+    // event callbacks.
+    public interface DialogEventListener {
+        /**
+         * Called when users click the device item to set active for calls and alarms in the dialog.
+         *
+         * @param item The device item clicked.
+         */
+        void onItemClick(AudioSharingDeviceItem item);
+    }
+
+    @Nullable private static DialogEventListener sListener;
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_ACTIVE;
+    }
+
+    /**
+     * Display the {@link CallsAndAlarmsDialogFragment} dialog.
+     *
+     * @param host The Fragment this dialog will be hosted.
+     * @param deviceItems The connected device items in audio sharing session.
+     * @param listener The callback to handle the user action on this dialog.
+     */
+    public static void show(
+            @NonNull Fragment host,
+            @NonNull List<AudioSharingDeviceItem> deviceItems,
+            @NonNull DialogEventListener listener) {
+        if (!AudioSharingUtils.isFeatureEnabled()) return;
+        final FragmentManager manager = host.getChildFragmentManager();
+        sListener = listener;
+        if (manager.findFragmentByTag(TAG) == null) {
+            final Bundle bundle = new Bundle();
+            bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
+            final CallsAndAlarmsDialogFragment dialog = new CallsAndAlarmsDialogFragment();
+            dialog.setArguments(bundle);
+            dialog.show(manager, TAG);
+        }
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        Bundle arguments = requireArguments();
+        List<AudioSharingDeviceItem> deviceItems =
+                arguments.getParcelable(BUNDLE_KEY_DEVICE_ITEMS, List.class);
+        int checkedItem = -1;
+        for (AudioSharingDeviceItem item : deviceItems) {
+            int fallbackActiveGroupId = AudioSharingUtils.getFallbackActiveGroupId(getContext());
+            if (item.getGroupId() == fallbackActiveGroupId) {
+                checkedItem = deviceItems.indexOf(item);
+            }
+        }
+        String[] choices =
+                deviceItems.stream().map(AudioSharingDeviceItem::getName).toArray(String[]::new);
+        AlertDialog.Builder builder =
+                new AlertDialog.Builder(getActivity())
+                        .setTitle(R.string.audio_sharing_call_audio_title)
+                        .setSingleChoiceItems(
+                                choices,
+                                checkedItem,
+                                (dialog, which) -> {
+                                    if (sListener != null) {
+                                        sListener.onItemClick(deviceItems.get(which));
+                                    }
+                                });
+        return builder.create();
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceController.java
new file mode 100644
index 0000000..8aaebc6
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceController.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** PreferenceController to control the dialog to choose the active device for calls and alarms */
+public class CallsAndAlarmsPreferenceController extends AudioSharingBasePreferenceController
+        implements BluetoothCallback {
+    private static final String TAG = "CallsAndAlarmsPreferenceController";
+    private static final String PREF_KEY = "calls_and_alarms";
+
+    @Nullable private final LocalBluetoothManager mBtManager;
+    @Nullable private final LocalBluetoothProfileManager mProfileManager;
+    @Nullable private final BluetoothEventManager mEventManager;
+    @Nullable private final ContentResolver mContentResolver;
+    @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
+    private final Executor mExecutor;
+    private final ContentObserver mSettingsObserver;
+    @Nullable private DashboardFragment mFragment;
+    Map<Integer, List<CachedBluetoothDevice>> mGroupedConnectedDevices = new HashMap<>();
+    private List<AudioSharingDeviceItem> mDeviceItemsInSharingSession = new ArrayList<>();
+    private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
+    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+            new BluetoothLeBroadcastAssistant.Callback() {
+                @Override
+                public void onSearchStarted(int reason) {}
+
+                @Override
+                public void onSearchStartFailed(int reason) {}
+
+                @Override
+                public void onSearchStopped(int reason) {}
+
+                @Override
+                public void onSearchStopFailed(int reason) {}
+
+                @Override
+                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
+
+                @Override
+                public void onSourceAdded(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceAddFailed(
+                        @NonNull BluetoothDevice sink,
+                        @NonNull BluetoothLeBroadcastMetadata source,
+                        int reason) {}
+
+                @Override
+                public void onSourceModified(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceModifyFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceRemoved(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onSourceRemoveFailed(
+                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+                @Override
+                public void onReceiveStateChanged(
+                        @NonNull BluetoothDevice sink,
+                        int sourceId,
+                        @NonNull BluetoothLeBroadcastReceiveState state) {
+                    if (BluetoothUtils.isConnected(state)) {
+                        Log.d(TAG, "onReceiveStateChanged: synced, updateSummary");
+                        updateSummary();
+                    }
+                }
+            };
+
+    public CallsAndAlarmsPreferenceController(Context context) {
+        super(context, PREF_KEY);
+        mBtManager = Utils.getLocalBtManager(mContext);
+        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
+        mEventManager = mBtManager == null ? null : mBtManager.getEventManager();
+        mAssistant =
+                mProfileManager == null
+                        ? null
+                        : mProfileManager.getLeAudioBroadcastAssistantProfile();
+        mExecutor = Executors.newSingleThreadExecutor();
+        mContentResolver = context.getContentResolver();
+        mSettingsObserver = new FallbackDeviceGroupIdSettingsObserver();
+    }
+
+    private class FallbackDeviceGroupIdSettingsObserver extends ContentObserver {
+        FallbackDeviceGroupIdSettingsObserver() {
+            super(new Handler(Looper.getMainLooper()));
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            Log.d(TAG, "onChange, fallback device group id has been changed");
+            var unused = ThreadUtils.postOnBackgroundThread(() -> updateSummary());
+        }
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return PREF_KEY;
+    }
+
+    @Override
+    public void displayPreference(@NonNull PreferenceScreen screen) {
+        super.displayPreference(screen);
+        if (mPreference != null) {
+            mPreference.setVisible(false);
+            updateSummary();
+            mPreference.setOnPreferenceClickListener(
+                    preference -> {
+                        if (mFragment == null) {
+                            Log.w(TAG, "Dialog fail to show due to null host.");
+                            return true;
+                        }
+                        updateDeviceItemsInSharingSession();
+                        if (mDeviceItemsInSharingSession.size() >= 1) {
+                            CallsAndAlarmsDialogFragment.show(
+                                    mFragment,
+                                    mDeviceItemsInSharingSession,
+                                    (AudioSharingDeviceItem item) -> {
+                                        if (!mGroupedConnectedDevices.containsKey(
+                                                item.getGroupId())) {
+                                            return;
+                                        }
+                                        List<CachedBluetoothDevice> devices =
+                                                mGroupedConnectedDevices.get(item.getGroupId());
+                                        @Nullable
+                                        CachedBluetoothDevice lead =
+                                                AudioSharingUtils.getLeadDevice(devices);
+                                        if (lead != null) {
+                                            Log.d(
+                                                    TAG,
+                                                    "Set fallback active device: "
+                                                            + lead.getDevice()
+                                                                    .getAnonymizedAddress());
+                                            lead.setActive();
+                                        } else {
+                                            Log.w(
+                                                    TAG,
+                                                    "Fail to set fallback active device: no lead"
+                                                            + " device");
+                                        }
+                                    });
+                        }
+                        return true;
+                    });
+        }
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        super.onStart(owner);
+        registerCallbacks();
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        super.onStop(owner);
+        unregisterCallbacks();
+    }
+
+    @Override
+    public void onProfileConnectionStateChanged(
+            @NonNull CachedBluetoothDevice cachedDevice,
+            @ConnectionState int state,
+            int bluetoothProfile) {
+        if (state == BluetoothAdapter.STATE_DISCONNECTED
+                && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
+            Log.d(TAG, "updatePreference, LE_AUDIO_BROADCAST_ASSISTANT is disconnected.");
+            // The fallback active device could be updated if the previous fallback device is
+            // disconnected.
+            updateSummary();
+        }
+    }
+
+    /**
+     * Initialize the controller.
+     *
+     * @param fragment The fragment to host the {@link CallsAndAlarmsDialogFragment} dialog.
+     */
+    public void init(DashboardFragment fragment) {
+        this.mFragment = fragment;
+    }
+
+    @VisibleForTesting
+    ContentObserver getSettingsObserver() {
+        return mSettingsObserver;
+    }
+
+    /** Test only: set callback registration status in tests. */
+    @VisibleForTesting
+    public void setCallbacksRegistered(boolean registered) {
+        mCallbacksRegistered.set(registered);
+    }
+
+    private void registerCallbacks() {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip registerCallbacks(). Feature is not available.");
+            return;
+        }
+        if (mEventManager == null || mContentResolver == null || mAssistant == null) {
+            Log.d(
+                    TAG,
+                    "Skip registerCallbacks(). Init is not ready: eventManager = "
+                            + (mEventManager == null)
+                            + ", contentResolver"
+                            + (mContentResolver == null));
+            return;
+        }
+        if (!mCallbacksRegistered.get()) {
+            Log.d(TAG, "registerCallbacks()");
+            mEventManager.registerCallback(this);
+            mContentResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID),
+                    false,
+                    mSettingsObserver);
+            mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+            mCallbacksRegistered.set(true);
+        }
+    }
+
+    private void unregisterCallbacks() {
+        if (!isAvailable()) {
+            Log.d(TAG, "Skip unregisterCallbacks(). Feature is not available.");
+            return;
+        }
+        if (mEventManager == null || mContentResolver == null || mAssistant == null) {
+            Log.d(TAG, "Skip unregisterCallbacks(). Init is not ready.");
+            return;
+        }
+        if (mCallbacksRegistered.get()) {
+            Log.d(TAG, "unregisterCallbacks()");
+            mEventManager.unregisterCallback(this);
+            mContentResolver.unregisterContentObserver(mSettingsObserver);
+            mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+            mCallbacksRegistered.set(false);
+        }
+    }
+
+    /**
+     * Update the preference summary: current headset for call audio.
+     *
+     * <p>The summary should be updated when:
+     *
+     * <p>1. displayPreference.
+     *
+     * <p>2. ContentObserver#onChange: the fallback device value in SettingsProvider is changed.
+     *
+     * <p>3. onProfileConnectionStateChanged: the assistant profile of fallback device disconnected.
+     * When the last headset in audio sharing disconnected, both Settings and bluetooth framework
+     * won't set the SettingsProvider, so no ContentObserver#onChange.
+     *
+     * <p>4. onReceiveStateChanged: new headset join the audio sharing. If the headset has already
+     * been set as fallback device in SettingsProvider by bluetooth framework when the broadcast is
+     * started, Settings won't set the SettingsProvider again when the headset join the audio
+     * sharing, so there won't be ContentObserver#onChange. We need listen to onReceiveStateChanged
+     * to handle this scenario.
+     */
+    private void updateSummary() {
+        updateDeviceItemsInSharingSession();
+        int fallbackActiveGroupId = AudioSharingUtils.getFallbackActiveGroupId(mContext);
+        if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+            for (AudioSharingDeviceItem item : mDeviceItemsInSharingSession) {
+                if (item.getGroupId() == fallbackActiveGroupId) {
+                    Log.d(
+                            TAG,
+                            "updatePreference: set summary tp fallback group "
+                                    + fallbackActiveGroupId);
+                    AudioSharingUtils.postOnMainThread(
+                            mContext,
+                            () -> {
+                                if (mPreference != null) {
+                                    mPreference.setSummary(
+                                            mContext.getString(
+                                                    R.string.audio_sharing_call_audio_description,
+                                                    item.getName()));
+                                }
+                            });
+                    return;
+                }
+            }
+        }
+        Log.d(TAG, "updatePreference: set empty summary");
+        AudioSharingUtils.postOnMainThread(
+                mContext,
+                () -> {
+                    if (mPreference != null) {
+                        mPreference.setSummary("");
+                    }
+                });
+    }
+
+    private void updateDeviceItemsInSharingSession() {
+        mGroupedConnectedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(mBtManager);
+        mDeviceItemsInSharingSession =
+                AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
+                        mBtManager, mGroupedConnectedDevices, /* filterByInSharing= */ true);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/StreamSettingsCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/StreamSettingsCategoryController.java
new file mode 100644
index 0000000..e9953a5
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/StreamSettingsCategoryController.java
@@ -0,0 +1,144 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+
+public class StreamSettingsCategoryController extends BasePreferenceController
+        implements DefaultLifecycleObserver, LocalBluetoothProfileManager.ServiceListener {
+    private static final String TAG = "StreamSettingsCategoryController";
+    private final BluetoothAdapter mBluetoothAdapter;
+    @Nullable private final LocalBluetoothManager mBtManager;
+    @Nullable private final LocalBluetoothProfileManager mProfileManager;
+    @Nullable private Preference mPreference;
+    @VisibleForTesting final IntentFilter mIntentFilter;
+
+    @VisibleForTesting
+    BroadcastReceiver mReceiver =
+            new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) return;
+                    updateVisibility();
+                }
+            };
+
+    public StreamSettingsCategoryController(Context context, String key) {
+        super(context, key);
+        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        mBtManager = Utils.getLocalBtManager(context);
+        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
+        mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) return;
+        mContext.registerReceiver(mReceiver, mIntentFilter, Context.RECEIVER_EXPORTED_UNAUDITED);
+        if (!isProfileReady() && mProfileManager != null) {
+            mProfileManager.addServiceListener(this);
+        }
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) return;
+        mContext.unregisterReceiver(mReceiver);
+        if (mProfileManager != null) {
+            mProfileManager.removeServiceListener(this);
+        }
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreference = screen.findPreference(getPreferenceKey());
+        updateVisibility();
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+    }
+
+    @Override
+    public void onServiceConnected() {
+        if (isAvailable() && isProfileReady()) {
+            updateVisibility();
+            if (mProfileManager != null) {
+                mProfileManager.removeServiceListener(this);
+            }
+        }
+    }
+
+    @Override
+    public void onServiceDisconnected() {
+        // Do nothing
+    }
+
+    private void updateVisibility() {
+        if (mPreference == null) {
+            Log.w(TAG, "Skip updateVisibility, null preference");
+            return;
+        }
+        if (!isAvailable()) {
+            Log.w(TAG, "Skip updateVisibility, unavailable preference");
+            AudioSharingUtils.postOnMainThread(
+                    mContext,
+                    () -> { // Check nullability to pass NullAway check
+                        if (mPreference != null) {
+                            mPreference.setVisible(false);
+                        }
+                    });
+            return;
+        }
+        boolean visible = isBluetoothOn() && isProfileReady();
+        AudioSharingUtils.postOnMainThread(
+                mContext,
+                () -> { // Check nullability to pass NullAway check
+                    if (mPreference != null) {
+                        mPreference.setVisible(visible);
+                    }
+                });
+    }
+
+    private boolean isBluetoothOn() {
+        return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
+    }
+
+    private boolean isProfileReady() {
+        return AudioSharingUtils.isAudioSharingProfileReady(mProfileManager);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeState.java
new file mode 100644
index 0000000..1993377
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeState.java
@@ -0,0 +1,49 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settings.R;
+
+class AddSourceBadCodeState extends SyncedState {
+    @VisibleForTesting
+    static final int AUDIO_STREAM_ADD_SOURCE_BAD_CODE_STATE_SUMMARY =
+            R.string.audio_streams_add_source_bad_code_state_summary;
+
+    @Nullable private static AddSourceBadCodeState sInstance = null;
+
+    AddSourceBadCodeState() {}
+
+    static AddSourceBadCodeState getInstance() {
+        if (sInstance == null) {
+            sInstance = new AddSourceBadCodeState();
+        }
+        return sInstance;
+    }
+
+    @Override
+    int getSummary() {
+        return AUDIO_STREAM_ADD_SOURCE_BAD_CODE_STATE_SUMMARY;
+    }
+
+    @Override
+    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
+        return AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_BAD_CODE;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedState.java
new file mode 100644
index 0000000..5d151ee
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedState.java
@@ -0,0 +1,49 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settings.R;
+
+class AddSourceFailedState extends SyncedState {
+    @VisibleForTesting
+    static final int AUDIO_STREAM_ADD_SOURCE_FAILED_STATE_SUMMARY =
+            R.string.audio_streams_add_source_failed_state_summary;
+
+    @Nullable private static AddSourceFailedState sInstance = null;
+
+    AddSourceFailedState() {}
+
+    static AddSourceFailedState getInstance() {
+        if (sInstance == null) {
+            sInstance = new AddSourceFailedState();
+        }
+        return sInstance;
+    }
+
+    @Override
+    int getSummary() {
+        return AUDIO_STREAM_ADD_SOURCE_FAILED_STATE_SUMMARY;
+    }
+
+    @Override
+    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
+        return AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseState.java
new file mode 100644
index 0000000..4d6a7f9
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseState.java
@@ -0,0 +1,106 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.app.AlertDialog;
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settings.R;
+import com.android.settingslib.utils.ThreadUtils;
+
+class AddSourceWaitForResponseState extends AudioStreamStateHandler {
+    @VisibleForTesting
+    static final int AUDIO_STREAM_ADD_SOURCE_WAIT_FOR_RESPONSE_STATE_SUMMARY =
+            R.string.audio_streams_add_source_wait_for_response_summary;
+
+    @VisibleForTesting static final int ADD_SOURCE_WAIT_FOR_RESPONSE_TIMEOUT_MILLIS = 20000;
+
+    @Nullable private static AddSourceWaitForResponseState sInstance = null;
+
+    private AddSourceWaitForResponseState() {}
+
+    static AddSourceWaitForResponseState getInstance() {
+        if (sInstance == null) {
+            sInstance = new AddSourceWaitForResponseState();
+        }
+        return sInstance;
+    }
+
+    @Override
+    void performAction(
+            AudioStreamPreference preference,
+            AudioStreamsProgressCategoryController controller,
+            AudioStreamsHelper helper) {
+        mHandler.removeCallbacksAndMessages(preference);
+        var metadata = preference.getAudioStreamMetadata();
+        if (metadata != null) {
+            helper.addSource(metadata);
+            // Cache the metadata that used for add source, if source is added successfully, we
+            // will save it persistently.
+            mAudioStreamsRepository.cacheMetadata(metadata);
+
+            // It's possible that onSourceLost() is not notified even if the source is no longer
+            // valid. When calling addSource() for a source that's already lost, no callback
+            // will be sent back. So we remove the preference and pop up a dialog if it's state
+            // has not been changed after waiting for a certain time.
+            mHandler.postDelayed(
+                    () -> {
+                        if (preference.isShown()
+                                && preference.getAudioStreamState() == getStateEnum()) {
+                            controller.handleSourceFailedToConnect(
+                                    preference.getAudioStreamBroadcastId());
+                            ThreadUtils.postOnMainThread(
+                                    () -> {
+                                        if (controller.getFragment() != null) {
+                                            AudioStreamsDialogFragment.show(
+                                                    controller.getFragment(),
+                                                    getBroadcastUnavailableNoRetryDialog(
+                                                            preference.getContext(),
+                                                            AudioStreamsHelper.getBroadcastName(
+                                                                    metadata)));
+                                        }
+                                    });
+                        }
+                    },
+                    preference,
+                    ADD_SOURCE_WAIT_FOR_RESPONSE_TIMEOUT_MILLIS);
+        }
+    }
+
+    @Override
+    int getSummary() {
+        return AUDIO_STREAM_ADD_SOURCE_WAIT_FOR_RESPONSE_STATE_SUMMARY;
+    }
+
+    @Override
+    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
+        return AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE;
+    }
+
+    private AudioStreamsDialogFragment.DialogBuilder getBroadcastUnavailableNoRetryDialog(
+            Context context, String broadcastName) {
+        return new AudioStreamsDialogFragment.DialogBuilder(context)
+                .setTitle(context.getString(R.string.audio_streams_dialog_stream_is_not_available))
+                .setSubTitle1(broadcastName)
+                .setSubTitle2(context.getString(R.string.audio_streams_is_not_playing))
+                .setRightButtonText(context.getString(R.string.audio_streams_dialog_close))
+                .setRightButtonOnClickListener(AlertDialog::dismiss);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
new file mode 100644
index 0000000..ea5abdf
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonController.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.utils.ThreadUtils;
+import com.android.settingslib.widget.ActionButtonsPreference;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class AudioStreamButtonController extends BasePreferenceController
+        implements DefaultLifecycleObserver {
+    private static final String TAG = "AudioStreamButtonController";
+    private static final String KEY = "audio_stream_button";
+    private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+            new AudioStreamsBroadcastAssistantCallback() {
+                @Override
+                public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+                    super.onSourceRemoved(sink, sourceId, reason);
+                    updateButton();
+                }
+
+                @Override
+                public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
+                    super.onSourceRemoveFailed(sink, sourceId, reason);
+                    updateButton();
+                }
+
+                @Override
+                public void onReceiveStateChanged(
+                        BluetoothDevice sink,
+                        int sourceId,
+                        BluetoothLeBroadcastReceiveState state) {
+                    super.onReceiveStateChanged(sink, sourceId, state);
+                    if (AudioStreamsHelper.isConnected(state)) {
+                        updateButton();
+                    }
+                }
+
+                @Override
+                public void onSourceAddFailed(
+                        BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
+                    super.onSourceAddFailed(sink, source, reason);
+                    updateButton();
+                }
+
+                @Override
+                public void onSourceLost(int broadcastId) {
+                    super.onSourceLost(broadcastId);
+                    updateButton();
+                }
+            };
+
+    private final AudioStreamsRepository mAudioStreamsRepository =
+            AudioStreamsRepository.getInstance();
+    private final Executor mExecutor;
+    private final AudioStreamsHelper mAudioStreamsHelper;
+    private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+    private @Nullable ActionButtonsPreference mPreference;
+    private int mBroadcastId = -1;
+
+    public AudioStreamButtonController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mExecutor = Executors.newSingleThreadExecutor();
+        mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
+        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
+            return;
+        }
+        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
+            return;
+        }
+        mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+    }
+
+    @Override
+    public final void displayPreference(PreferenceScreen screen) {
+        mPreference = screen.findPreference(getPreferenceKey());
+        updateButton();
+        super.displayPreference(screen);
+    }
+
+    private void updateButton() {
+        if (mPreference != null) {
+            if (mAudioStreamsHelper.getAllConnectedSources().stream()
+                    .map(BluetoothLeBroadcastReceiveState::getBroadcastId)
+                    .anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId)) {
+                ThreadUtils.postOnMainThread(
+                        () -> {
+                            if (mPreference != null) {
+                                mPreference.setButton1Enabled(true);
+                                mPreference
+                                        .setButton1Text(R.string.audio_streams_disconnect)
+                                        .setButton1Icon(
+                                                com.android.settings.R.drawable.ic_settings_close)
+                                        .setButton1OnClickListener(
+                                                unused -> {
+                                                    if (mPreference != null) {
+                                                        mPreference.setButton1Enabled(false);
+                                                    }
+                                                    mAudioStreamsHelper.removeSource(mBroadcastId);
+                                                });
+                            }
+                        });
+            } else {
+                View.OnClickListener clickToRejoin =
+                        unused ->
+                                ThreadUtils.postOnBackgroundThread(
+                                        () -> {
+                                            var metadata =
+                                                    mAudioStreamsRepository.getSavedMetadata(
+                                                            mContext, mBroadcastId);
+                                            if (metadata != null) {
+                                                mAudioStreamsHelper.addSource(metadata);
+                                                ThreadUtils.postOnMainThread(
+                                                        () -> {
+                                                            if (mPreference != null) {
+                                                                mPreference.setButton1Enabled(
+                                                                        false);
+                                                            }
+                                                        });
+                                            }
+                                        });
+                ThreadUtils.postOnMainThread(
+                        () -> {
+                            if (mPreference != null) {
+                                mPreference.setButton1Enabled(true);
+                                mPreference
+                                        .setButton1Text(R.string.audio_streams_connect)
+                                        .setButton1Icon(com.android.settings.R.drawable.ic_add_24dp)
+                                        .setButton1OnClickListener(clickToRejoin);
+                            }
+                        });
+            }
+        } else {
+            Log.w(TAG, "updateButton(): preference is null!");
+        }
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY;
+    }
+
+    /** Initialize with broadcast id */
+    void init(int broadcastId) {
+        mBroadcastId = broadcastId;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java
new file mode 100644
index 0000000..8e27958
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialog.java
@@ -0,0 +1,195 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeFragment;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+
+import com.google.common.base.Strings;
+
+public class AudioStreamConfirmDialog extends InstrumentedDialogFragment {
+    public static final String KEY_BROADCAST_METADATA = "key_broadcast_metadata";
+    private static final String TAG = "AudioStreamConfirmDialog";
+    private static final int DEFAULT_DEVICE_NAME = R.string.audio_streams_dialog_default_device;
+    @Nullable private LocalBluetoothManager mLocalBluetoothManager;
+    @Nullable private LocalBluetoothProfileManager mProfileManager;
+    @Nullable private Activity mActivity;
+    @Nullable private String mBroadcastMetadataStr;
+    @Nullable private BluetoothLeBroadcastMetadata mBroadcastMetadata;
+    private boolean mIsRequestValid = false;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (!AudioSharingUtils.isFeatureEnabled()) {
+            return;
+        }
+        setShowsDialog(true);
+        mActivity = getActivity();
+        if (mActivity == null) {
+            Log.w(TAG, "onCreate() mActivity is null!");
+            return;
+        }
+        mLocalBluetoothManager = Utils.getLocalBluetoothManager(mActivity);
+        mProfileManager =
+                mLocalBluetoothManager == null ? null : mLocalBluetoothManager.getProfileManager();
+        mBroadcastMetadataStr =
+                mActivity.getIntent().getStringExtra(QrCodeScanModeFragment.KEY_BROADCAST_METADATA);
+        if (Strings.isNullOrEmpty(mBroadcastMetadataStr)) {
+            Log.w(TAG, "onCreate() mBroadcastMetadataStr is null or empty!");
+            return;
+        }
+        mBroadcastMetadata =
+                BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
+                        mBroadcastMetadataStr);
+        if (mBroadcastMetadata == null) {
+            Log.w(TAG, "onCreate() mBroadcastMetadata is null!");
+        } else {
+            mIsRequestValid = true;
+        }
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
+            CachedBluetoothDevice connectedLeDevice =
+                    AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
+                                    mLocalBluetoothManager)
+                            .orElse(null);
+            if (connectedLeDevice == null) {
+                return getNoLeDeviceDialog();
+            }
+            String deviceName = connectedLeDevice.getName();
+            return mIsRequestValid ? getConfirmDialog(deviceName) : getErrorDialog(deviceName);
+        }
+        Log.d(TAG, "onCreateDialog() : profile not ready!");
+        String defaultDeviceName =
+                mActivity != null ? mActivity.getString(DEFAULT_DEVICE_NAME) : "";
+        return mIsRequestValid
+                ? getConfirmDialog(defaultDeviceName)
+                : getErrorDialog(defaultDeviceName);
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        // TODO(chelseahao): update metrics id
+        return 0;
+    }
+
+    private Dialog getConfirmDialog(String name) {
+        return new AudioStreamsDialogFragment.DialogBuilder(getActivity())
+                .setTitle(getString(R.string.audio_streams_dialog_listen_to_audio_stream))
+                .setSubTitle1(
+                        mBroadcastMetadata != null
+                                ? AudioStreamsHelper.getBroadcastName(mBroadcastMetadata)
+                                : "")
+                .setSubTitle2(getString(R.string.audio_streams_dialog_control_volume, name))
+                .setLeftButtonText(getString(com.android.settings.R.string.cancel))
+                .setLeftButtonOnClickListener(
+                        unused -> {
+                            dismiss();
+                            if (mActivity != null) {
+                                mActivity.finish();
+                            }
+                        })
+                .setRightButtonText(getString(R.string.audio_streams_dialog_listen))
+                .setRightButtonOnClickListener(
+                        unused -> {
+                            launchAudioStreamsActivity();
+                            dismiss();
+                            if (mActivity != null) {
+                                mActivity.finish();
+                            }
+                        })
+                .build();
+    }
+
+    private Dialog getErrorDialog(String name) {
+        return new AudioStreamsDialogFragment.DialogBuilder(getActivity())
+                .setTitle(getString(R.string.audio_streams_dialog_cannot_listen))
+                .setSubTitle2(getString(R.string.audio_streams_dialog_cannot_play, name))
+                .setRightButtonText(getString(R.string.audio_streams_dialog_close))
+                .setRightButtonOnClickListener(
+                        unused -> {
+                            dismiss();
+                            if (mActivity != null) {
+                                mActivity.finish();
+                            }
+                        })
+                .build();
+    }
+
+    private Dialog getNoLeDeviceDialog() {
+        return new AudioStreamsDialogFragment.DialogBuilder(getActivity())
+                .setTitle(getString(R.string.audio_streams_dialog_no_le_device_title))
+                .setSubTitle2(getString(R.string.audio_streams_dialog_no_le_device_subtitle))
+                .setLeftButtonText(getString(R.string.audio_streams_dialog_close))
+                .setLeftButtonOnClickListener(
+                        unused -> {
+                            dismiss();
+                            if (mActivity != null) {
+                                mActivity.finish();
+                            }
+                        })
+                .setRightButtonText(getString(R.string.audio_streams_dialog_no_le_device_button))
+                .setRightButtonOnClickListener(
+                        dialog -> {
+                            if (mActivity != null) {
+                                mActivity.startActivity(
+                                        new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
+                                                .setPackage(mActivity.getPackageName()));
+                            }
+                            dismiss();
+                            if (mActivity != null) {
+                                mActivity.finish();
+                            }
+                        })
+                .build();
+    }
+
+    private void launchAudioStreamsActivity() {
+        Bundle bundle = new Bundle();
+        bundle.putString(KEY_BROADCAST_METADATA, mBroadcastMetadataStr);
+        if (mActivity != null) {
+            new SubSettingLauncher(getActivity())
+                    .setTitleText(getString(R.string.audio_streams_activity_title))
+                    .setDestination(AudioStreamsDashboardFragment.class.getName())
+                    .setArguments(bundle)
+                    .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
+                    .launch();
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java
new file mode 100644
index 0000000..695ad93
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamConfirmDialogActivity.java
@@ -0,0 +1,38 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.os.Bundle;
+
+import com.android.settings.SettingsActivity;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+
+public class AudioStreamConfirmDialogActivity extends SettingsActivity {
+
+    @Override
+    protected void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        if (!AudioSharingUtils.isFeatureEnabled()) {
+            finish();
+        }
+    }
+
+    @Override
+    protected boolean isValidFragment(String fragmentName) {
+        return AudioStreamConfirmDialog.class.getName().equals(fragmentName);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
new file mode 100644
index 0000000..94e6644
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamDetailsFragment.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import com.android.settings.R;
+import com.android.settings.dashboard.DashboardFragment;
+
+public class AudioStreamDetailsFragment extends DashboardFragment {
+    static final String BROADCAST_NAME_ARG = "broadcast_name";
+    static final String BROADCAST_ID_ARG = "broadcast_id";
+    private static final String TAG = "AudioStreamDetailsFragment";
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        Bundle arguments = getArguments();
+        if (arguments != null) {
+            use(AudioStreamHeaderController.class)
+                    .init(
+                            this,
+                            arguments.getString(BROADCAST_NAME_ARG),
+                            arguments.getInt(BROADCAST_ID_ARG));
+            use(AudioStreamButtonController.class).init(arguments.getInt(BROADCAST_ID_ARG));
+        }
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        // TODO(chelseahao): update metrics id
+        return 0;
+    }
+
+    @Override
+    protected int getPreferenceScreenResId() {
+        return R.xml.bluetooth_le_audio_stream_details_fragment;
+    }
+
+    @Override
+    protected String getLogTag() {
+        return TAG;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
new file mode 100644
index 0000000..860e62e
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderController.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.utils.ThreadUtils;
+import com.android.settingslib.widget.LayoutPreference;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import javax.annotation.Nullable;
+
+public class AudioStreamHeaderController extends BasePreferenceController
+        implements DefaultLifecycleObserver {
+    @VisibleForTesting
+    static final int AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY =
+            R.string.audio_streams_listening_now;
+
+    @VisibleForTesting static final String AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY = "";
+    private static final String TAG = "AudioStreamHeaderController";
+    private static final String KEY = "audio_stream_header";
+    private final Executor mExecutor;
+    private final AudioStreamsHelper mAudioStreamsHelper;
+    @Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+    private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
+            new AudioStreamsBroadcastAssistantCallback() {
+                @Override
+                public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+                    super.onSourceRemoved(sink, sourceId, reason);
+                    updateSummary();
+                }
+
+                @Override
+                public void onSourceLost(int broadcastId) {
+                    super.onSourceLost(broadcastId);
+                    updateSummary();
+                }
+
+                @Override
+                public void onReceiveStateChanged(
+                        BluetoothDevice sink,
+                        int sourceId,
+                        BluetoothLeBroadcastReceiveState state) {
+                    super.onReceiveStateChanged(sink, sourceId, state);
+                    if (AudioStreamsHelper.isConnected(state)) {
+                        updateSummary();
+                        mAudioStreamsHelper.startMediaService(
+                                mContext, mBroadcastId, mBroadcastName);
+                    }
+                }
+            };
+
+    private @Nullable EntityHeaderController mHeaderController;
+    private @Nullable DashboardFragment mFragment;
+    private String mBroadcastName = "";
+    private int mBroadcastId = -1;
+
+    public AudioStreamHeaderController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mExecutor = Executors.newSingleThreadExecutor();
+        mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
+        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
+            return;
+        }
+        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
+            return;
+        }
+        mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+    }
+
+    @Override
+    public final void displayPreference(PreferenceScreen screen) {
+        LayoutPreference headerPreference = screen.findPreference(KEY);
+        if (headerPreference != null && mFragment != null) {
+            mHeaderController =
+                    EntityHeaderController.newInstance(
+                            mFragment.getActivity(),
+                            mFragment,
+                            headerPreference.findViewById(com.android.settings.R.id.entity_header));
+            if (mBroadcastName != null) {
+                mHeaderController.setLabel(mBroadcastName);
+            }
+            mHeaderController.setIcon(
+                    screen.getContext()
+                            .getDrawable(
+                                    com.android.settingslib.R.drawable.ic_bt_le_audio_sharing));
+            screen.addPreference(headerPreference);
+            updateSummary();
+        }
+        super.displayPreference(screen);
+    }
+
+    private void updateSummary() {
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            var latestSummary =
+                                    mAudioStreamsHelper.getAllConnectedSources().stream()
+                                                    .map(
+                                                            BluetoothLeBroadcastReceiveState
+                                                                    ::getBroadcastId)
+                                                    .anyMatch(
+                                                            connectedBroadcastId ->
+                                                                    connectedBroadcastId
+                                                                            == mBroadcastId)
+                                            ? mContext.getString(
+                                                    AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
+                                            : AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
+                            ThreadUtils.postOnMainThread(
+                                    () -> {
+                                        if (mHeaderController != null) {
+                                            mHeaderController.setSummary(latestSummary);
+                                            mHeaderController.done(true);
+                                        }
+                                    });
+                        });
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY;
+    }
+
+    /** Initialize with {@link AudioStreamDetailsFragment} and broadcast name and id */
+    void init(
+            AudioStreamDetailsFragment audioStreamDetailsFragment,
+            String broadcastName,
+            int broadcastId) {
+        mFragment = audioStreamDetailsFragment;
+        mBroadcastName = broadcastName;
+        mBroadcastId = broadcastId;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java
new file mode 100644
index 0000000..2e4930c
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java
@@ -0,0 +1,378 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothVolumeControl;
+import android.content.Intent;
+import android.media.MediaMetadata;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class AudioStreamMediaService extends Service {
+    static final String BROADCAST_ID = "audio_stream_media_service_broadcast_id";
+    static final String BROADCAST_TITLE = "audio_stream_media_service_broadcast_title";
+    static final String DEVICES = "audio_stream_media_service_devices";
+    private static final String TAG = "AudioStreamMediaService";
+    private static final int NOTIFICATION_ID = 1;
+    private static final int BROADCAST_CONTENT_TEXT = R.string.audio_streams_listening_now;
+    private static final String LEAVE_BROADCAST_ACTION = "leave_broadcast_action";
+    private static final String LEAVE_BROADCAST_TEXT = "Leave Broadcast";
+    private static final String CHANNEL_ID = "bluetooth_notification_channel";
+    private static final int STATIC_PLAYBACK_DURATION = 100;
+    private static final int STATIC_PLAYBACK_POSITION = 30;
+    private static final int ZERO_PLAYBACK_SPEED = 0;
+    private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback =
+            new AudioStreamsBroadcastAssistantCallback() {
+                @Override
+                public void onSourceLost(int broadcastId) {
+                    super.onSourceLost(broadcastId);
+                    if (broadcastId == mBroadcastId) {
+                        stopSelf();
+                    }
+                }
+
+                @Override
+                public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+                    super.onSourceRemoved(sink, sourceId, reason);
+                    if (mAudioStreamsHelper != null
+                            && mAudioStreamsHelper.getAllConnectedSources().stream()
+                                    .map(BluetoothLeBroadcastReceiveState::getBroadcastId)
+                                    .noneMatch(id -> id == mBroadcastId)) {
+                        stopSelf();
+                    }
+                }
+            };
+
+    private final BluetoothCallback mBluetoothCallback =
+            new BluetoothCallback() {
+                @Override
+                public void onBluetoothStateChanged(int bluetoothState) {
+                    if (BluetoothAdapter.STATE_OFF == bluetoothState) {
+                        stopSelf();
+                    }
+                }
+
+                @Override
+                public void onProfileConnectionStateChanged(
+                        @NonNull CachedBluetoothDevice cachedDevice,
+                        @ConnectionState int state,
+                        int bluetoothProfile) {
+                    if (state == BluetoothAdapter.STATE_DISCONNECTED
+                            && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
+                            && mDevices != null) {
+                        mDevices.remove(cachedDevice.getDevice());
+                        cachedDevice
+                                .getMemberDevice()
+                                .forEach(
+                                        m -> {
+                                            // Check nullability to pass NullAway check
+                                            if (mDevices != null) {
+                                                mDevices.remove(m.getDevice());
+                                            }
+                                        });
+                    }
+                    if (mDevices == null || mDevices.isEmpty()) {
+                        stopSelf();
+                    }
+                }
+            };
+
+    private final BluetoothVolumeControl.Callback mVolumeControlCallback =
+            new BluetoothVolumeControl.Callback() {
+                @Override
+                public void onDeviceVolumeChanged(
+                        @NonNull BluetoothDevice device,
+                        @IntRange(from = -255, to = 255) int volume) {
+                    if (mDevices == null || mDevices.isEmpty()) {
+                        Log.w(TAG, "active device or device has source is null!");
+                        return;
+                    }
+                    if (mDevices.contains(device)) {
+                        Log.d(
+                                TAG,
+                                "onDeviceVolumeChanged() bluetoothDevice : "
+                                        + device
+                                        + " volume: "
+                                        + volume);
+                        if (volume == 0) {
+                            mIsMuted = true;
+                        } else {
+                            mIsMuted = false;
+                            mLatestPositiveVolume = volume;
+                        }
+                        if (mLocalSession != null) {
+                            mLocalSession.setPlaybackState(getPlaybackState());
+                            if (mNotificationManager != null) {
+                                mNotificationManager.notify(NOTIFICATION_ID, buildNotification());
+                            }
+                        }
+                    }
+                }
+            };
+
+    private final PlaybackState.Builder mPlayStatePlayingBuilder =
+            new PlaybackState.Builder()
+                    .setActions(PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SEEK_TO)
+                    .setState(
+                            PlaybackState.STATE_PLAYING,
+                            STATIC_PLAYBACK_POSITION,
+                            ZERO_PLAYBACK_SPEED)
+                    .addCustomAction(
+                            LEAVE_BROADCAST_ACTION,
+                            LEAVE_BROADCAST_TEXT,
+                            com.android.settings.R.drawable.ic_clear);
+    private final PlaybackState.Builder mPlayStatePausingBuilder =
+            new PlaybackState.Builder()
+                    .setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_SEEK_TO)
+                    .setState(
+                            PlaybackState.STATE_PAUSED,
+                            STATIC_PLAYBACK_POSITION,
+                            ZERO_PLAYBACK_SPEED)
+                    .addCustomAction(
+                            LEAVE_BROADCAST_ACTION,
+                            LEAVE_BROADCAST_TEXT,
+                            com.android.settings.R.drawable.ic_clear);
+
+    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+    private int mBroadcastId;
+    @Nullable private ArrayList<BluetoothDevice> mDevices;
+    @Nullable private LocalBluetoothManager mLocalBtManager;
+    @Nullable private AudioStreamsHelper mAudioStreamsHelper;
+    @Nullable private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+    @Nullable private VolumeControlProfile mVolumeControl;
+    @Nullable private NotificationManager mNotificationManager;
+
+    // Set 25 as default as the volume range from `VolumeControlProfile` is from 0 to 255.
+    // If the initial volume from `onDeviceVolumeChanged` is larger than zero (not muted), we will
+    // override this value. Otherwise, we raise the volume to 25 when the play button is clicked.
+    private int mLatestPositiveVolume = 25;
+    private boolean mIsMuted = false;
+    @Nullable private MediaSession mLocalSession;
+
+    @Override
+    public void onCreate() {
+        if (!AudioSharingUtils.isFeatureEnabled()) {
+            return;
+        }
+
+        super.onCreate();
+        mLocalBtManager = Utils.getLocalBtManager(this);
+        if (mLocalBtManager == null) {
+            Log.w(TAG, "onCreate() : mLocalBtManager is null!");
+            return;
+        }
+
+        mAudioStreamsHelper = new AudioStreamsHelper(mLocalBtManager);
+        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "onCreate() : mLeBroadcastAssistant is null!");
+            return;
+        }
+
+        mNotificationManager = getSystemService(NotificationManager.class);
+        if (mNotificationManager == null) {
+            Log.w(TAG, "onCreate() : notificationManager is null!");
+            return;
+        }
+
+        if (mNotificationManager.getNotificationChannel(CHANNEL_ID) == null) {
+            NotificationChannel notificationChannel =
+                    new NotificationChannel(
+                            CHANNEL_ID,
+                            this.getString(com.android.settings.R.string.bluetooth),
+                            NotificationManager.IMPORTANCE_HIGH);
+            mNotificationManager.createNotificationChannel(notificationChannel);
+        }
+
+        mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback);
+
+        mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile();
+        if (mVolumeControl != null) {
+            mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback);
+        }
+
+        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        if (!AudioSharingUtils.isFeatureEnabled()) {
+            return;
+        }
+        if (mLocalBtManager != null) {
+            mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback);
+        }
+        if (mLeBroadcastAssistant != null) {
+            mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+        }
+        if (mVolumeControl != null) {
+            mVolumeControl.unregisterCallback(mVolumeControlCallback);
+        }
+        if (mLocalSession != null) {
+            mLocalSession.release();
+            mLocalSession = null;
+        }
+    }
+
+    @Override
+    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
+        Log.d(TAG, "onStartCommand()");
+
+        mBroadcastId = intent != null ? intent.getIntExtra(BROADCAST_ID, -1) : -1;
+        if (mBroadcastId == -1) {
+            Log.w(TAG, "Invalid broadcast ID. Service will not start.");
+            stopSelf();
+            return START_NOT_STICKY;
+        }
+
+        if (intent != null) {
+            mDevices = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class);
+        }
+        if (mDevices == null || mDevices.isEmpty()) {
+            Log.w(TAG, "No device. Service will not start.");
+            stopSelf();
+            return START_NOT_STICKY;
+        }
+        if (intent != null) {
+            createLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE));
+            startForeground(NOTIFICATION_ID, buildNotification());
+        }
+
+        return START_NOT_STICKY;
+    }
+
+    private void createLocalMediaSession(String title) {
+        mLocalSession = new MediaSession(this, TAG);
+        mLocalSession.setMetadata(
+                new MediaMetadata.Builder()
+                        .putString(MediaMetadata.METADATA_KEY_TITLE, title)
+                        .putLong(MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION)
+                        .build());
+        mLocalSession.setActive(true);
+        mLocalSession.setPlaybackState(getPlaybackState());
+        mLocalSession.setCallback(
+                new MediaSession.Callback() {
+                    public void onSeekTo(long pos) {
+                        Log.d(TAG, "onSeekTo: " + pos);
+                        if (mLocalSession != null) {
+                            mLocalSession.setPlaybackState(getPlaybackState());
+                            if (mNotificationManager != null) {
+                                mNotificationManager.notify(NOTIFICATION_ID, buildNotification());
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void onPause() {
+                        if (mDevices == null || mDevices.isEmpty()) {
+                            Log.w(TAG, "active device or device has source is null!");
+                            return;
+                        }
+                        Log.d(
+                                TAG,
+                                "onPause() setting volume for device : "
+                                        + mDevices.get(0)
+                                        + " volume: "
+                                        + 0);
+                        if (mVolumeControl != null) {
+                            mVolumeControl.setDeviceVolume(mDevices.get(0), 0, true);
+                        }
+                    }
+
+                    @Override
+                    public void onPlay() {
+                        if (mDevices == null || mDevices.isEmpty()) {
+                            Log.w(TAG, "active device or device has source is null!");
+                            return;
+                        }
+                        Log.d(
+                                TAG,
+                                "onPlay() setting volume for device : "
+                                        + mDevices.get(0)
+                                        + " volume: "
+                                        + mLatestPositiveVolume);
+                        if (mVolumeControl != null) {
+                            mVolumeControl.setDeviceVolume(
+                                    mDevices.get(0), mLatestPositiveVolume, true);
+                        }
+                    }
+
+                    @Override
+                    public void onCustomAction(@NonNull String action, Bundle extras) {
+                        Log.d(TAG, "onCustomAction: " + action);
+                        if (action.equals(LEAVE_BROADCAST_ACTION) && mAudioStreamsHelper != null) {
+                            mAudioStreamsHelper.removeSource(mBroadcastId);
+                        }
+                    }
+                });
+    }
+
+    private PlaybackState getPlaybackState() {
+        return mIsMuted ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build();
+    }
+
+    private Notification buildNotification() {
+        Notification.Builder notificationBuilder =
+                new Notification.Builder(this, CHANNEL_ID)
+                        .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
+                        .setStyle(
+                                new Notification.MediaStyle()
+                                        .setMediaSession(
+                                                mLocalSession != null
+                                                        ? mLocalSession.getSessionToken()
+                                                        : null))
+                        .setContentText(this.getString(BROADCAST_CONTENT_TEXT))
+                        .setSilent(true);
+        return notificationBuilder.build();
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
new file mode 100644
index 0000000..0334e05
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+import com.android.settingslib.widget.TwoTargetPreference;
+
+/**
+ * Custom preference class for managing audio stream preferences with an optional lock icon. Extends
+ * {@link TwoTargetPreference}.
+ */
+class AudioStreamPreference extends TwoTargetPreference {
+    private boolean mIsConnected = false;
+    private boolean mIsEncrypted = true;
+    @Nullable private AudioStream mAudioStream;
+
+    /**
+     * Update preference UI based on connection status
+     *
+     * @param isConnected Is this stream connected
+     * @param summary Summary text
+     * @param onPreferenceClickListener Click listener for the preference
+     */
+    void setIsConnected(
+            boolean isConnected,
+            String summary,
+            @Nullable OnPreferenceClickListener onPreferenceClickListener) {
+        if (mIsConnected == isConnected
+                && getSummary() == summary
+                && getOnPreferenceClickListener() == onPreferenceClickListener) {
+            // Nothing to update.
+            return;
+        }
+        mIsConnected = isConnected;
+        setSummary(summary);
+        setOnPreferenceClickListener(onPreferenceClickListener);
+        notifyChanged();
+    }
+
+    private AudioStreamPreference(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        setIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing);
+    }
+
+    void setAudioStreamState(AudioStreamsProgressCategoryController.AudioStreamState state) {
+        if (mAudioStream != null) {
+            mAudioStream.setState(state);
+        }
+    }
+
+    void setAudioStreamMetadata(BluetoothLeBroadcastMetadata metadata) {
+        if (mAudioStream != null) {
+            mAudioStream.setMetadata(metadata);
+        }
+    }
+
+    int getAudioStreamBroadcastId() {
+        return mAudioStream != null ? mAudioStream.getBroadcastId() : -1;
+    }
+
+    @Nullable
+    String getAudioStreamBroadcastName() {
+        return mAudioStream != null ? mAudioStream.getBroadcastName() : null;
+    }
+
+    int getAudioStreamRssi() {
+        return mAudioStream != null ? mAudioStream.getRssi() : -1;
+    }
+
+    @Nullable
+    BluetoothLeBroadcastMetadata getAudioStreamMetadata() {
+        return mAudioStream != null ? mAudioStream.getMetadata() : null;
+    }
+
+    AudioStreamsProgressCategoryController.AudioStreamState getAudioStreamState() {
+        return mAudioStream != null
+                ? mAudioStream.getState()
+                : AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN;
+    }
+
+    @Override
+    protected boolean shouldHideSecondTarget() {
+        return mIsConnected || !mIsEncrypted;
+    }
+
+    @Override
+    protected int getSecondTargetResId() {
+        return R.layout.preference_widget_lock;
+    }
+
+    @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+        View divider =
+                holder.findViewById(
+                        com.android.settingslib.widget.preference.twotarget.R.id
+                                .two_target_divider);
+        if (divider != null) {
+            divider.setVisibility(View.GONE);
+        }
+    }
+
+    static AudioStreamPreference fromMetadata(
+            Context context, BluetoothLeBroadcastMetadata source) {
+        AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
+        preference.setIsEncrypted(source.isEncrypted());
+        preference.setTitle(AudioStreamsHelper.getBroadcastName(source));
+        preference.setAudioStream(new AudioStream(source));
+        return preference;
+    }
+
+    static AudioStreamPreference fromReceiveState(
+            Context context, BluetoothLeBroadcastReceiveState receiveState) {
+        AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
+        preference.setTitle(AudioStreamsHelper.getBroadcastName(receiveState));
+        preference.setAudioStream(new AudioStream(receiveState));
+        return preference;
+    }
+
+    private void setAudioStream(AudioStream audioStream) {
+        mAudioStream = audioStream;
+    }
+
+    private void setIsEncrypted(boolean isEncrypted) {
+        mIsEncrypted = isEncrypted;
+    }
+
+    private static final class AudioStream {
+        private static final int UNAVAILABLE = -1;
+        @Nullable private BluetoothLeBroadcastMetadata mMetadata;
+        @Nullable private BluetoothLeBroadcastReceiveState mReceiveState;
+        private AudioStreamsProgressCategoryController.AudioStreamState mState =
+                AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN;
+
+        private AudioStream(BluetoothLeBroadcastMetadata metadata) {
+            mMetadata = metadata;
+        }
+
+        private AudioStream(BluetoothLeBroadcastReceiveState receiveState) {
+            mReceiveState = receiveState;
+        }
+
+        private int getBroadcastId() {
+            return mMetadata != null
+                    ? mMetadata.getBroadcastId()
+                    : mReceiveState != null ? mReceiveState.getBroadcastId() : UNAVAILABLE;
+        }
+
+        private @Nullable String getBroadcastName() {
+            return mMetadata != null
+                    ? AudioStreamsHelper.getBroadcastName(mMetadata)
+                    : mReceiveState != null
+                            ? AudioStreamsHelper.getBroadcastName(mReceiveState)
+                            : null;
+        }
+
+        private int getRssi() {
+            return mMetadata != null ? mMetadata.getRssi() : Integer.MAX_VALUE;
+        }
+
+        private AudioStreamsProgressCategoryController.AudioStreamState getState() {
+            return mState;
+        }
+
+        @Nullable
+        private BluetoothLeBroadcastMetadata getMetadata() {
+            return mMetadata;
+        }
+
+        private void setState(AudioStreamsProgressCategoryController.AudioStreamState state) {
+            mState = state;
+        }
+
+        private void setMetadata(BluetoothLeBroadcastMetadata metadata) {
+            mMetadata = metadata;
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java
new file mode 100644
index 0000000..df176be
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java
@@ -0,0 +1,111 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.utils.ThreadUtils;
+
+class AudioStreamStateHandler {
+    private static final String TAG = "AudioStreamStateHandler";
+    private static final boolean DEBUG = BluetoothUtils.D;
+    @VisibleForTesting static final int EMPTY_STRING_RES = 0;
+
+    final AudioStreamsRepository mAudioStreamsRepository = AudioStreamsRepository.getInstance();
+    final Handler mHandler = new Handler(Looper.getMainLooper());
+
+    AudioStreamStateHandler() {}
+
+    void handleStateChange(
+            AudioStreamPreference preference,
+            AudioStreamsProgressCategoryController controller,
+            AudioStreamsHelper helper) {
+        var newState = getStateEnum();
+        if (preference.getAudioStreamState() == newState) {
+            return;
+        }
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "moveToState() : moving preference : ["
+                            + preference.getAudioStreamBroadcastId()
+                            + ", "
+                            + preference.getAudioStreamBroadcastName()
+                            + "] from state : "
+                            + preference.getAudioStreamState()
+                            + " to state : "
+                            + newState);
+        }
+        preference.setAudioStreamState(newState);
+
+        performAction(preference, controller, helper);
+
+        // Update UI
+        ThreadUtils.postOnMainThread(
+                () ->
+                        preference.setIsConnected(
+                                newState
+                                        == AudioStreamsProgressCategoryController.AudioStreamState
+                                                .SOURCE_ADDED,
+                                getSummary() != EMPTY_STRING_RES
+                                        ? preference.getContext().getString(getSummary())
+                                        : "",
+                                getOnClickListener(controller)));
+    }
+
+    /**
+     * Perform action related to the audio stream state (e.g, addSource) This method is intended to
+     * be optionally overridden by subclasses to provide custom behavior based on the audio stream
+     * state change.
+     */
+    void performAction(
+            AudioStreamPreference preference,
+            AudioStreamsProgressCategoryController controller,
+            AudioStreamsHelper helper) {}
+
+    /**
+     * The preference summary for the audio stream state (e.g, Scanning...) This method is intended
+     * to be optionally overridden.
+     */
+    @StringRes
+    int getSummary() {
+        return EMPTY_STRING_RES;
+    }
+
+    /**
+     * The preference on click event for the audio stream state (e.g, open up a dialog) This method
+     * is intended to be optionally overridden.
+     */
+    @Nullable
+    Preference.OnPreferenceClickListener getOnClickListener(
+            AudioStreamsProgressCategoryController controller) {
+        return null;
+    }
+
+    /** Subclasses should always override. */
+    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
+        return AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceController.java
new file mode 100644
index 0000000..6603a08
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceController.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.core.BasePreferenceController;
+
+public class AudioStreamsActiveDeviceController extends BasePreferenceController
+        implements AudioStreamsActiveDeviceSummaryUpdater.OnSummaryChangeListener,
+                DefaultLifecycleObserver {
+
+    public static final String KEY = "audio_streams_active_device";
+    private final AudioStreamsActiveDeviceSummaryUpdater mSummaryHelper;
+    @Nullable private Preference mPreference;
+
+    public AudioStreamsActiveDeviceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mSummaryHelper = new AudioStreamsActiveDeviceSummaryUpdater(mContext, this);
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreference = screen.findPreference(KEY);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public void onSummaryChanged(String summary) {
+        if (mPreference != null) {
+            mPreference.setSummary(summary);
+        }
+    }
+
+    @Override
+    public void onResume(@NonNull LifecycleOwner owner) {
+        mSummaryHelper.register(true);
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        mSummaryHelper.register(false);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdater.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdater.java
new file mode 100644
index 0000000..ab22b07
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdater.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+public class AudioStreamsActiveDeviceSummaryUpdater implements BluetoothCallback {
+    private static final String TAG = "AudioStreamsActiveDeviceSummaryUpdater";
+    private static final boolean DEBUG = BluetoothUtils.D;
+    private final LocalBluetoothManager mBluetoothManager;
+    private Context mContext;
+    @Nullable private String mSummary;
+    private OnSummaryChangeListener mListener;
+
+    public AudioStreamsActiveDeviceSummaryUpdater(
+            Context context, OnSummaryChangeListener listener) {
+        mContext = context;
+        mBluetoothManager = Utils.getLocalBluetoothManager(context);
+        mListener = listener;
+    }
+
+    @Override
+    public void onActiveDeviceChanged(
+            @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "onActiveDeviceChanged() with activeDevice : "
+                            + (activeDevice == null ? "null" : activeDevice.getAddress())
+                            + " on profile : "
+                            + bluetoothProfile);
+        }
+        if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
+            notifyChangeIfNeeded();
+        }
+    }
+
+    void register(boolean register) {
+        if (register) {
+            notifyChangeIfNeeded();
+            mBluetoothManager.getEventManager().registerCallback(this);
+        } else {
+            mBluetoothManager.getEventManager().unregisterCallback(this);
+        }
+    }
+
+    private void notifyChangeIfNeeded() {
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            String summary = getSummary();
+                            if (!TextUtils.equals(mSummary, summary)) {
+                                mSummary = summary;
+                                ThreadUtils.postOnMainThread(
+                                        () -> mListener.onSummaryChanged(summary));
+                            }
+                        });
+    }
+
+    private String getSummary() {
+        var connectedSink =
+                AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
+                        mBluetoothManager);
+        if (connectedSink.isEmpty()) {
+            return mContext.getString(R.string.audio_streams_dialog_no_le_device_title);
+        }
+        return connectedSink.get().getName();
+    }
+
+    /** Interface definition for a callback to be invoked when the summary has been changed. */
+    interface OnSummaryChangeListener {
+        /**
+         * Called when summary has changed.
+         *
+         * @param summary The new summary.
+         */
+        void onSummaryChanged(String summary);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
new file mode 100644
index 0000000..9fb5b21
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.util.Log;
+
+import com.android.settingslib.bluetooth.BluetoothUtils;
+
+public class AudioStreamsBroadcastAssistantCallback
+        implements BluetoothLeBroadcastAssistant.Callback {
+
+    private static final String TAG = "AudioStreamsBroadcastAssistantCallback";
+    private static final boolean DEBUG = BluetoothUtils.D;
+
+    @Override
+    public void onReceiveStateChanged(
+            BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "onReceiveStateChanged() sink : "
+                            + sink.getAddress()
+                            + " sourceId: "
+                            + sourceId
+                            + " state: "
+                            + state);
+        }
+    }
+
+    @Override
+    public void onSearchStartFailed(int reason) {
+        Log.w(TAG, "onSearchStartFailed() reason : " + reason);
+    }
+
+    @Override
+    public void onSearchStarted(int reason) {
+        if (DEBUG) {
+            Log.d(TAG, "onSearchStarted() reason : " + reason);
+        }
+    }
+
+    @Override
+    public void onSearchStopFailed(int reason) {
+        Log.w(TAG, "onSearchStopFailed() reason : " + reason);
+    }
+
+    @Override
+    public void onSearchStopped(int reason) {
+        if (DEBUG) {
+            Log.d(TAG, "onSearchStopped() reason : " + reason);
+        }
+    }
+
+    @Override
+    public void onSourceAddFailed(
+            BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "onSourceAddFailed() sink : "
+                            + sink.getAddress()
+                            + " source: "
+                            + source
+                            + " reason: "
+                            + reason);
+        }
+    }
+
+    @Override
+    public void onSourceAdded(BluetoothDevice sink, int sourceId, int reason) {
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "onSourceAdded() sink : "
+                            + sink.getAddress()
+                            + " sourceId: "
+                            + sourceId
+                            + " reason: "
+                            + reason);
+        }
+    }
+
+    @Override
+    public void onSourceFound(BluetoothLeBroadcastMetadata source) {
+        if (DEBUG) {
+            Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId());
+        }
+    }
+
+    @Override
+    public void onSourceLost(int broadcastId) {
+        if (DEBUG) {
+            Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId);
+        }
+    }
+
+    @Override
+    public void onSourceModified(BluetoothDevice sink, int sourceId, int reason) {}
+
+    @Override
+    public void onSourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) {}
+
+    @Override
+    public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
+        Log.w(TAG, "onSourceRemoveFailed() sourceId : " + sourceId + " reason : " + reason);
+    }
+
+    @Override
+    public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+        if (DEBUG) {
+            Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason);
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsCategoryController.java
new file mode 100644
index 0000000..0f164bb
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsCategoryController.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingBasePreferenceController;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.flags.Flags;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class AudioStreamsCategoryController extends AudioSharingBasePreferenceController {
+    private static final String TAG = "AudioStreamsCategoryController";
+    private static final boolean DEBUG = BluetoothUtils.D;
+    private final LocalBluetoothManager mLocalBtManager;
+    private final Executor mExecutor;
+    private final BluetoothCallback mBluetoothCallback =
+            new BluetoothCallback() {
+                @Override
+                public void onActiveDeviceChanged(
+                        @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
+                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
+                        updateVisibility();
+                    }
+                }
+            };
+
+    public AudioStreamsCategoryController(Context context, String key) {
+        super(context, key);
+        mLocalBtManager = Utils.getLocalBtManager(mContext);
+        mExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        super.onStart(owner);
+        if (mLocalBtManager != null) {
+            mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback);
+        }
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        super.onStop(owner);
+        if (mLocalBtManager != null) {
+            mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback);
+        }
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return Flags.enableLeAudioQrCodePrivateBroadcastSharing()
+                ? AVAILABLE
+                : UNSUPPORTED_ON_DEVICE;
+    }
+
+    @Override
+    public void updateVisibility() {
+        if (mPreference == null) return;
+        mExecutor.execute(
+                () -> {
+                    if (!isAvailable()) {
+                        Log.d(TAG, "skip updateVisibility, unavailable preference");
+                        AudioSharingUtils.postOnMainThread(
+                                mContext,
+                                () -> { // Check nullability to pass NullAway check
+                                    if (mPreference != null) {
+                                        mPreference.setVisible(false);
+                                    }
+                                });
+                        return;
+                    }
+                    boolean hasConnectedLe =
+                            AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
+                                            mLocalBtManager)
+                                    .isPresent();
+                    boolean isProfileReady =
+                            AudioSharingUtils.isAudioSharingProfileReady(
+                                    mLocalBtManager.getProfileManager());
+                    boolean isBroadcasting = isBroadcasting();
+                    boolean isBluetoothOn = isBluetoothStateOn();
+                    if (DEBUG) {
+                        Log.d(
+                                TAG,
+                                "updateVisibility() isBroadcasting : "
+                                        + isBroadcasting
+                                        + " hasConnectedLe : "
+                                        + hasConnectedLe
+                                        + " is BT on : "
+                                        + isBluetoothOn
+                                        + " is profile ready : "
+                                        + isProfileReady);
+                    }
+                    AudioSharingUtils.postOnMainThread(
+                            mContext,
+                            () -> { // Check nullability to pass NullAway check
+                                if (mPreference != null) {
+                                    mPreference.setVisible(
+                                            isProfileReady
+                                                    && isBluetoothOn
+                                                    && hasConnectedLe
+                                                    && !isBroadcasting);
+                                }
+                            });
+                });
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
new file mode 100644
index 0000000..330c325
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController.REQUEST_SCAN_BT_BROADCAST_QR_CODE;
+
+import android.app.Activity;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeFragment;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+
+import com.google.common.base.Strings;
+
+public class AudioStreamsDashboardFragment extends DashboardFragment {
+    private static final String TAG = "AudioStreamsDashboardFrag";
+    private static final boolean DEBUG = BluetoothUtils.D;
+    private AudioStreamsProgressCategoryController mAudioStreamsProgressCategoryController;
+
+    public AudioStreamsDashboardFragment() {
+        super();
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        // TODO: update category id.
+        return 0;
+    }
+
+    @Override
+    protected String getLogTag() {
+        return TAG;
+    }
+
+    @Override
+    public int getHelpResource() {
+        return R.string.help_url_audio_sharing;
+    }
+
+    @Override
+    protected int getPreferenceScreenResId() {
+        return R.xml.bluetooth_le_audio_streams;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    public void onAttach(Context context) {
+        super.onAttach(context);
+        use(AudioStreamsScanQrCodeController.class).setFragment(this);
+        mAudioStreamsProgressCategoryController = use(AudioStreamsProgressCategoryController.class);
+        mAudioStreamsProgressCategoryController.setFragment(this);
+
+        if (getArguments() != null) {
+            String broadcastMetadataStr =
+                    getArguments().getString(AudioStreamConfirmDialog.KEY_BROADCAST_METADATA);
+            if (!Strings.isNullOrEmpty(broadcastMetadataStr)) {
+                BluetoothLeBroadcastMetadata broadcastMetadata =
+                        BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
+                                broadcastMetadataStr);
+                if (broadcastMetadata == null) {
+                    Log.w(TAG, "onAttach() broadcastMetadata is null!");
+                } else {
+                    mAudioStreamsProgressCategoryController.setSourceFromQrCode(broadcastMetadata);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "onActivityResult() requestCode : "
+                            + requestCode
+                            + " resultCode : "
+                            + resultCode);
+        }
+        if (requestCode == REQUEST_SCAN_BT_BROADCAST_QR_CODE) {
+            if (resultCode == Activity.RESULT_OK) {
+                String broadcastMetadata =
+                        data != null
+                                ? data.getStringExtra(QrCodeScanModeFragment.KEY_BROADCAST_METADATA)
+                                : "";
+                BluetoothLeBroadcastMetadata source =
+                        BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
+                                broadcastMetadata);
+                if (source == null) {
+                    Log.w(TAG, "onActivityResult() source is null!");
+                    return;
+                }
+                if (DEBUG) {
+                    Log.d(TAG, "onActivityResult() broadcastId : " + source.getBroadcastId());
+                }
+                if (mAudioStreamsProgressCategoryController == null) {
+                    Log.w(
+                            TAG,
+                            "onActivityResult() AudioStreamsProgressCategoryController is null!");
+                    return;
+                }
+                mAudioStreamsProgressCategoryController.setSourceFromQrCode(source);
+            }
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragment.java
new file mode 100644
index 0000000..eb99b96
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragment.java
@@ -0,0 +1,249 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+import com.google.common.base.Strings;
+
+import java.util.function.Consumer;
+
+/** A dialog fragment for constructing and showing audio stream dialogs. */
+public class AudioStreamsDialogFragment extends InstrumentedDialogFragment {
+    private static final String TAG = "AudioStreamsDialogFragment";
+    private final DialogBuilder mDialogBuilder;
+
+    AudioStreamsDialogFragment(DialogBuilder dialogBuilder) {
+        mDialogBuilder = dialogBuilder;
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        // TODO(chelseahao): update metrics id
+        return 0;
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        return mDialogBuilder.build();
+    }
+
+    /**
+     * Displays the audio stream dialog on the specified host fragment.
+     *
+     * @param host The fragment to host the dialog.
+     * @param dialogBuilder The builder for constructing the dialog.
+     */
+    public static void show(Fragment host, DialogBuilder dialogBuilder) {
+        if (!host.isAdded()) {
+            Log.w(TAG, "The host fragment is not added to the activity!");
+            return;
+        }
+        FragmentManager manager = host.getChildFragmentManager();
+        (new AudioStreamsDialogFragment(dialogBuilder)).show(manager, TAG);
+    }
+
+    static void dismissAll(Fragment host) {
+        if (!host.isAdded()) {
+            Log.w(TAG, "The host fragment is not added to the activity!");
+            return;
+        }
+        FragmentManager manager = host.getChildFragmentManager();
+        Fragment dialog = manager.findFragmentByTag(TAG);
+        if (dialog != null
+                && ((DialogFragment) dialog).getDialog() != null
+                && ((DialogFragment) dialog).getDialog().isShowing()) {
+            ((DialogFragment) dialog).dismiss();
+        }
+    }
+
+    @Override
+    public void show(@NonNull FragmentManager manager, @Nullable String tag) {
+        Fragment dialog = manager.findFragmentByTag(TAG);
+        if (dialog != null
+                && ((DialogFragment) dialog).getDialog() != null
+                && ((DialogFragment) dialog).getDialog().isShowing()) {
+            Log.w(TAG, "Dialog already showing, ignore");
+            return;
+        }
+        super.show(manager, tag);
+    }
+
+    /** A builder class for constructing the audio stream dialog. */
+    public static class DialogBuilder {
+        private final Context mContext;
+        private final AlertDialog.Builder mBuilder;
+        @Nullable private String mTitle;
+        @Nullable private String mSubTitle1;
+        @Nullable private String mSubTitle2;
+        @Nullable private String mLeftButtonText;
+        @Nullable private String mRightButtonText;
+        @Nullable private Consumer<AlertDialog> mLeftButtonOnClickListener;
+        @Nullable private Consumer<AlertDialog> mRightButtonOnClickListener;
+
+        /**
+         * Constructs a new instance of DialogBuilder.
+         *
+         * @param context The context used for building the dialog.
+         */
+        public DialogBuilder(Context context) {
+            mContext = context;
+            mBuilder = new AlertDialog.Builder(context);
+        }
+
+        /**
+         * Sets the title of the dialog.
+         *
+         * @param title The title text.
+         * @return This DialogBuilder instance.
+         */
+        public DialogBuilder setTitle(String title) {
+            mTitle = title;
+            return this;
+        }
+
+        /**
+         * Sets the first subtitle of the dialog.
+         *
+         * @param subTitle1 The text of the first subtitle.
+         * @return This DialogBuilder instance.
+         */
+        public DialogBuilder setSubTitle1(String subTitle1) {
+            mSubTitle1 = subTitle1;
+            return this;
+        }
+
+        /**
+         * Sets the second subtitle of the dialog.
+         *
+         * @param subTitle2 The text of the second subtitle.
+         * @return This DialogBuilder instance.
+         */
+        public DialogBuilder setSubTitle2(String subTitle2) {
+            mSubTitle2 = subTitle2;
+            return this;
+        }
+
+        /**
+         * Sets the text of the left button.
+         *
+         * @param text The text of the left button.
+         * @return This DialogBuilder instance.
+         */
+        public DialogBuilder setLeftButtonText(String text) {
+            mLeftButtonText = text;
+            return this;
+        }
+
+        /**
+         * Sets the click listener of the left button.
+         *
+         * @param listener The click listener for the left button.
+         * @return This DialogBuilder instance.
+         */
+        public DialogBuilder setLeftButtonOnClickListener(Consumer<AlertDialog> listener) {
+            mLeftButtonOnClickListener = listener;
+            return this;
+        }
+
+        /**
+         * Sets the text of the right button.
+         *
+         * @param text The text of the right button.
+         * @return This DialogBuilder instance.
+         */
+        public DialogBuilder setRightButtonText(String text) {
+            mRightButtonText = text;
+            return this;
+        }
+
+        /**
+         * Sets the click listener of the right button.
+         *
+         * @param listener The click listener for the right button.
+         * @return This DialogBuilder instance.
+         */
+        public DialogBuilder setRightButtonOnClickListener(Consumer<AlertDialog> listener) {
+            mRightButtonOnClickListener = listener;
+            return this;
+        }
+
+        AlertDialog build() {
+            View rootView =
+                    LayoutInflater.from(mContext)
+                            .inflate(R.xml.bluetooth_audio_streams_dialog, /* parent= */ null);
+
+            AlertDialog dialog = mBuilder.setView(rootView).setCancelable(false).create();
+            dialog.setCanceledOnTouchOutside(false);
+
+            TextView title = rootView.requireViewById(R.id.dialog_title);
+            title.setText(mTitle);
+
+            if (!Strings.isNullOrEmpty(mSubTitle1)) {
+                TextView subTitle1 = rootView.requireViewById(R.id.dialog_subtitle);
+                subTitle1.setText(mSubTitle1);
+                subTitle1.setVisibility(View.VISIBLE);
+            }
+            if (!Strings.isNullOrEmpty(mSubTitle2)) {
+                TextView subTitle2 = rootView.requireViewById(R.id.dialog_subtitle_2);
+                subTitle2.setText(mSubTitle2);
+                subTitle2.setVisibility(View.VISIBLE);
+            }
+            if (!Strings.isNullOrEmpty(mLeftButtonText)) {
+                Button leftButton = rootView.requireViewById(R.id.left_button);
+                leftButton.setText(mLeftButtonText);
+                leftButton.setVisibility(View.VISIBLE);
+                leftButton.setOnClickListener(
+                        unused -> {
+                            if (mLeftButtonOnClickListener != null) {
+                                mLeftButtonOnClickListener.accept(dialog);
+                            }
+                        });
+            }
+            if (!Strings.isNullOrEmpty(mRightButtonText)) {
+                Button rightButton = rootView.requireViewById(R.id.right_button);
+                rightButton.setText(mRightButtonText);
+                rightButton.setVisibility(View.VISIBLE);
+                rightButton.setOnClickListener(
+                        unused -> {
+                            if (mRightButtonOnClickListener != null) {
+                                mRightButtonOnClickListener.accept(dialog);
+                            }
+                        });
+            }
+
+            return dialog;
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
new file mode 100644
index 0000000..d2b0a8a
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES;
+
+import static java.util.Collections.emptyList;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudioContentMetadata;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import com.google.common.base.Strings;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import javax.annotation.Nullable;
+
+/**
+ * A helper class that adds, removes and retrieves LE broadcast sources for all active sink devices.
+ */
+public class AudioStreamsHelper {
+
+    private static final String TAG = "AudioStreamsHelper";
+    private static final boolean DEBUG = BluetoothUtils.D;
+
+    private final @Nullable LocalBluetoothManager mBluetoothManager;
+    private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+
+    AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
+        mBluetoothManager = bluetoothManager;
+        mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager);
+    }
+
+    /**
+     * Adds the specified LE broadcast source to all active sinks.
+     *
+     * @param source The LE broadcast metadata representing the audio source.
+     */
+    void addSource(BluetoothLeBroadcastMetadata source) {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "addSource(): LeBroadcastAssistant is null!");
+            return;
+        }
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            for (var sink :
+                                    getConnectedBluetoothDevices(
+                                            mBluetoothManager, /* inSharingOnly= */ false)) {
+                                if (DEBUG) {
+                                    Log.d(
+                                            TAG,
+                                            "addSource(): join broadcast broadcastId"
+                                                    + " : "
+                                                    + source.getBroadcastId()
+                                                    + " sink : "
+                                                    + sink.getAddress());
+                                }
+                                mLeBroadcastAssistant.addSource(sink, source, false);
+                            }
+                        });
+    }
+
+    /** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */
+    void removeSource(int broadcastId) {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
+            return;
+        }
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            for (var sink :
+                                    getConnectedBluetoothDevices(
+                                            mBluetoothManager, /* inSharingOnly= */ true)) {
+                                if (DEBUG) {
+                                    Log.d(
+                                            TAG,
+                                            "removeSource(): remove all sources with broadcast id :"
+                                                    + broadcastId
+                                                    + " from sink : "
+                                                    + sink.getAddress());
+                                }
+                                mLeBroadcastAssistant.getAllSources(sink).stream()
+                                        .filter(state -> state.getBroadcastId() == broadcastId)
+                                        .forEach(
+                                                state ->
+                                                        mLeBroadcastAssistant.removeSource(
+                                                                sink, state.getSourceId()));
+                            }
+                        });
+    }
+
+    /** Retrieves a list of all LE broadcast receive states from active sinks. */
+    @VisibleForTesting
+    public List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
+            return emptyList();
+        }
+        return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream()
+                .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
+                .filter(AudioStreamsHelper::isConnected)
+                .toList();
+    }
+
+    @Nullable
+    LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
+        return mLeBroadcastAssistant;
+    }
+
+    /** Checks the connectivity status based on the provided broadcast receive state. */
+    public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
+        return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0);
+    }
+
+    static boolean isBadCode(BluetoothLeBroadcastReceiveState state) {
+        return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
+                && state.getBigEncryptionState()
+                        == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE;
+    }
+
+    /**
+     * Returns a {@code CachedBluetoothDevice} that is either connected to a broadcast source or is
+     * a connected LE device.
+     */
+    public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected(
+            @androidx.annotation.Nullable LocalBluetoothManager manager) {
+        if (manager == null) {
+            Log.w(
+                    TAG,
+                    "getCachedBluetoothDeviceInSharingOrLeConnected(): LocalBluetoothManager is"
+                            + " null!");
+            return Optional.empty();
+        }
+        var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager);
+        var leadDevices =
+                AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
+        if (leadDevices.isEmpty()) {
+            Log.w(TAG, "getCachedBluetoothDeviceInSharingOrLeConnected(): No lead device!");
+            return Optional.empty();
+        }
+        var deviceHasSource =
+                leadDevices.stream()
+                        .filter(device -> hasConnectedBroadcastSource(device, manager))
+                        .findFirst();
+        if (deviceHasSource.isPresent()) {
+            Log.d(
+                    TAG,
+                    "getCachedBluetoothDeviceInSharingOrLeConnected(): Device has connected source"
+                            + " found: "
+                            + deviceHasSource.get().getAddress());
+            return deviceHasSource;
+        }
+        Log.d(
+                TAG,
+                "getCachedBluetoothDeviceInSharingOrLeConnected(): Device connected found: "
+                        + leadDevices.get(0).getAddress());
+        return Optional.of(leadDevices.get(0));
+    }
+
+    /** Returns a {@code CachedBluetoothDevice} that has a connected broadcast source. */
+    static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharing(
+            @androidx.annotation.Nullable LocalBluetoothManager manager) {
+        if (manager == null) {
+            Log.w(TAG, "getCachedBluetoothDeviceInSharing(): LocalBluetoothManager is null!");
+            return Optional.empty();
+        }
+        var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager);
+        var leadDevices =
+                AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
+        if (leadDevices.isEmpty()) {
+            Log.w(TAG, "getCachedBluetoothDeviceInSharing(): No lead device!");
+            return Optional.empty();
+        }
+        return leadDevices.stream()
+                .filter(device -> hasConnectedBroadcastSource(device, manager))
+                .findFirst();
+    }
+
+    /**
+     * Check if {@link CachedBluetoothDevice} has connected to a broadcast source.
+     *
+     * @param cachedDevice The cached bluetooth device to check.
+     * @param localBtManager The BT manager to provide BT functions.
+     * @return Whether the device has connected to a broadcast source.
+     */
+    private static boolean hasConnectedBroadcastSource(
+            CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
+        if (localBtManager == null) {
+            Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null");
+            return false;
+        }
+        LocalBluetoothLeBroadcastAssistant assistant =
+                localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
+        if (assistant == null) {
+            Log.d(TAG, "Skip check hasConnectedBroadcastSource due to assistant profile is null");
+            return false;
+        }
+        List<BluetoothLeBroadcastReceiveState> sourceList =
+                assistant.getAllSources(cachedDevice.getDevice());
+        if (!sourceList.isEmpty()
+                && sourceList.stream().anyMatch(AudioStreamsHelper::isConnected)) {
+            Log.d(
+                    TAG,
+                    "Lead device has connected broadcast source, device = "
+                            + cachedDevice.getDevice().getAnonymizedAddress());
+            return true;
+        }
+        // Return true if member device is in broadcast.
+        for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
+            List<BluetoothLeBroadcastReceiveState> list =
+                    assistant.getAllSources(device.getDevice());
+            if (!list.isEmpty() && list.stream().anyMatch(AudioStreamsHelper::isConnected)) {
+                Log.d(
+                        TAG,
+                        "Member device has connected broadcast source, device = "
+                                + device.getDevice().getAnonymizedAddress());
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Retrieves a list of connected Bluetooth devices that belongs to one {@link
+     * CachedBluetoothDevice} that's either connected to a broadcast source or is a connected LE
+     * audio device.
+     */
+    static List<BluetoothDevice> getConnectedBluetoothDevices(
+            @Nullable LocalBluetoothManager manager, boolean inSharingOnly) {
+        if (manager == null) {
+            Log.w(TAG, "getConnectedBluetoothDevices(): LocalBluetoothManager is null!");
+            return emptyList();
+        }
+        var leBroadcastAssistant = getLeBroadcastAssistant(manager);
+        if (leBroadcastAssistant == null) {
+            Log.w(TAG, "getConnectedBluetoothDevices(): LeBroadcastAssistant is null!");
+            return emptyList();
+        }
+        List<BluetoothDevice> connectedDevices =
+                leBroadcastAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED});
+        Optional<CachedBluetoothDevice> cachedBluetoothDevice =
+                inSharingOnly
+                        ? getCachedBluetoothDeviceInSharing(manager)
+                        : getCachedBluetoothDeviceInSharingOrLeConnected(manager);
+        List<BluetoothDevice> bluetoothDevices =
+                cachedBluetoothDevice
+                        .map(
+                                c ->
+                                        Stream.concat(
+                                                        Stream.of(c.getDevice()),
+                                                        c.getMemberDevice().stream()
+                                                                .map(
+                                                                        CachedBluetoothDevice
+                                                                                ::getDevice))
+                                                .filter(connectedDevices::contains)
+                                                .toList())
+                        .orElse(emptyList());
+        Log.d(TAG, "getConnectedBluetoothDevices() devices: " + bluetoothDevices);
+        return bluetoothDevices;
+    }
+
+    private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant(
+            @Nullable LocalBluetoothManager manager) {
+        if (manager == null) {
+            Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!");
+            return null;
+        }
+
+        LocalBluetoothProfileManager profileManager = manager.getProfileManager();
+        if (profileManager == null) {
+            Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!");
+            return null;
+        }
+
+        return profileManager.getLeAudioBroadcastAssistantProfile();
+    }
+
+    static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
+        // TODO(b/331547596): prioritize broadcastName
+        Optional<String> optionalProgramInfo =
+                source.getSubgroups().stream()
+                        .map(subgroup -> subgroup.getContentMetadata().getProgramInfo())
+                        .filter(programInfo -> !Strings.isNullOrEmpty(programInfo))
+                        .findFirst();
+
+        return optionalProgramInfo.orElseGet(
+                () -> {
+                    String broadcastName = source.getBroadcastName();
+                    if (broadcastName != null && !broadcastName.isEmpty()) {
+                        return broadcastName;
+                    } else {
+                        return "Broadcast Id: " + source.getBroadcastId();
+                    }
+                });
+    }
+
+    static String getBroadcastName(BluetoothLeBroadcastReceiveState state) {
+        return state.getSubgroupMetadata().stream()
+                .map(BluetoothLeAudioContentMetadata::getProgramInfo)
+                .filter(i -> !Strings.isNullOrEmpty(i))
+                .findFirst()
+                .orElse("Broadcast Id: " + state.getBroadcastId());
+    }
+
+    void startMediaService(Context context, int audioStreamBroadcastId, String title) {
+        List<BluetoothDevice> devices =
+                getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true);
+        if (devices.isEmpty()) {
+            return;
+        }
+        var intent = new Intent(context, AudioStreamMediaService.class);
+        intent.putExtra(BROADCAST_ID, audioStreamBroadcastId);
+        intent.putExtra(BROADCAST_TITLE, title);
+        intent.putParcelableArrayListExtra(DEVICES, new ArrayList<>(devices));
+        context.startService(intent);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
new file mode 100644
index 0000000..bc39a42
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
@@ -0,0 +1,122 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.util.Log;
+
+import java.util.Locale;
+
+public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback {
+    private static final String TAG = "AudioStreamsProgressCategoryCallback";
+
+    private final AudioStreamsProgressCategoryController mCategoryController;
+
+    public AudioStreamsProgressCategoryCallback(
+            AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
+        mCategoryController = audioStreamsProgressCategoryController;
+    }
+
+    @Override
+    public void onReceiveStateChanged(
+            BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
+        super.onReceiveStateChanged(sink, sourceId, state);
+
+        if (AudioStreamsHelper.isConnected(state)) {
+            mCategoryController.handleSourceConnected(state);
+        } else if (AudioStreamsHelper.isBadCode(state)) {
+            mCategoryController.handleSourceConnectBadCode(state);
+        }
+    }
+
+    @Override
+    public void onSearchStartFailed(int reason) {
+        super.onSearchStartFailed(reason);
+        mCategoryController.showToast(
+                String.format(Locale.US, "Failed to start scanning, reason %d", reason));
+        mCategoryController.setScanning(false);
+    }
+
+    @Override
+    public void onSearchStarted(int reason) {
+        super.onSearchStarted(reason);
+        if (mCategoryController == null) {
+            Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
+            return;
+        }
+        mCategoryController.setScanning(true);
+    }
+
+    @Override
+    public void onSearchStopFailed(int reason) {
+        super.onSearchStopFailed(reason);
+        mCategoryController.showToast(
+                String.format(Locale.US, "Failed to stop scanning, reason %d", reason));
+    }
+
+    @Override
+    public void onSearchStopped(int reason) {
+        super.onSearchStopped(reason);
+        if (mCategoryController == null) {
+            Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
+            return;
+        }
+        mCategoryController.setScanning(false);
+    }
+
+    @Override
+    public void onSourceAddFailed(
+            BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
+        super.onSourceAddFailed(sink, source, reason);
+        mCategoryController.handleSourceFailedToConnect(source.getBroadcastId());
+    }
+
+    @Override
+    public void onSourceFound(BluetoothLeBroadcastMetadata source) {
+        super.onSourceFound(source);
+        if (mCategoryController == null) {
+            Log.w(TAG, "onSourceFound() : mCategoryController is null!");
+            return;
+        }
+        mCategoryController.handleSourceFound(source);
+    }
+
+    @Override
+    public void onSourceLost(int broadcastId) {
+        super.onSourceLost(broadcastId);
+        mCategoryController.handleSourceLost(broadcastId);
+    }
+
+    @Override
+    public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
+        super.onSourceRemoveFailed(sink, sourceId, reason);
+        mCategoryController.showToast(
+                String.format(
+                        Locale.US,
+                        "Failed to remove source %d for sink %s",
+                        sourceId,
+                        sink.getAddress()));
+    }
+
+    @Override
+    public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+        super.onSourceRemoved(sink, sourceId, reason);
+        mCategoryController.handleSourceRemoved();
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
new file mode 100644
index 0000000..749220f
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -0,0 +1,575 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static java.util.Collections.emptyList;
+
+import android.app.AlertDialog;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.Comparator;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import javax.annotation.Nullable;
+
+public class AudioStreamsProgressCategoryController extends BasePreferenceController
+        implements DefaultLifecycleObserver {
+    private static final String TAG = "AudioStreamsProgressCategoryController";
+    private static final boolean DEBUG = BluetoothUtils.D;
+    private static final int UNSET_BROADCAST_ID = -1;
+    private final BluetoothCallback mBluetoothCallback =
+            new BluetoothCallback() {
+                @Override
+                public void onActiveDeviceChanged(
+                        @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
+                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
+                        mExecutor.execute(() -> init());
+                    }
+                }
+            };
+
+    private final Comparator<AudioStreamPreference> mComparator =
+            Comparator.<AudioStreamPreference, Boolean>comparing(
+                            p ->
+                                    p.getAudioStreamState()
+                                            == AudioStreamsProgressCategoryController
+                                                    .AudioStreamState.SOURCE_ADDED)
+                    .thenComparingInt(AudioStreamPreference::getAudioStreamRssi)
+                    .reversed();
+
+    public enum AudioStreamState {
+        UNKNOWN,
+        // When mSourceFromQrCode is present and this source has not been synced.
+        WAIT_FOR_SYNC,
+        // When source has been synced but not added to any sink.
+        SYNCED,
+        // When addSource is called for this source and waiting for response.
+        ADD_SOURCE_WAIT_FOR_RESPONSE,
+        // When addSource result in a bad code response.
+        ADD_SOURCE_BAD_CODE,
+        // When addSource result in other bad state.
+        ADD_SOURCE_FAILED,
+        // Source is added to active sink.
+        SOURCE_ADDED,
+    }
+
+    private final Executor mExecutor;
+    private final AudioStreamsProgressCategoryCallback mBroadcastAssistantCallback;
+    private final AudioStreamsHelper mAudioStreamsHelper;
+    private final MediaControlHelper mMediaControlHelper;
+    private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+    private final @Nullable LocalBluetoothManager mBluetoothManager;
+    private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
+            new ConcurrentHashMap<>();
+    private @Nullable BluetoothLeBroadcastMetadata mSourceFromQrCode;
+    @Nullable private AudioStreamsProgressCategoryPreference mCategoryPreference;
+    @Nullable private AudioStreamsDashboardFragment mFragment;
+
+    public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mExecutor = Executors.newSingleThreadExecutor();
+        mBluetoothManager = Utils.getLocalBtManager(mContext);
+        mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
+        mMediaControlHelper = new MediaControlHelper(mContext, mBluetoothManager);
+        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
+        mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mCategoryPreference = screen.findPreference(getPreferenceKey());
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (mBluetoothManager != null) {
+            mBluetoothManager.getEventManager().registerCallback(mBluetoothCallback);
+        }
+        mExecutor.execute(this::init);
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (mBluetoothManager != null) {
+            mBluetoothManager.getEventManager().unregisterCallback(mBluetoothCallback);
+        }
+        mExecutor.execute(this::stopScanning);
+    }
+
+    void setFragment(AudioStreamsDashboardFragment fragment) {
+        mFragment = fragment;
+    }
+
+    @Nullable
+    AudioStreamsDashboardFragment getFragment() {
+        return mFragment;
+    }
+
+    void setSourceFromQrCode(BluetoothLeBroadcastMetadata source) {
+        if (DEBUG) {
+            Log.d(TAG, "setSourceFromQrCode(): broadcastId " + source.getBroadcastId());
+        }
+        mSourceFromQrCode = source;
+    }
+
+    void setScanning(boolean isScanning) {
+        ThreadUtils.postOnMainThread(
+                () -> {
+                    if (mCategoryPreference != null) mCategoryPreference.setProgress(isScanning);
+                });
+    }
+
+    // Find preference by scanned source and decide next state.
+    // Expect one of the following:
+    // 1) No preference existed, create new preference with state SYNCED
+    // 2) WAIT_FOR_SYNC, move to ADD_SOURCE_WAIT_FOR_RESPONSE
+    // 3) SOURCE_ADDED, leave as-is
+    void handleSourceFound(BluetoothLeBroadcastMetadata source) {
+        if (DEBUG) {
+            Log.d(TAG, "handleSourceFound()");
+        }
+        var broadcastIdFound = source.getBroadcastId();
+
+        if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) {
+            // mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the
+            // scanned metadata.
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "handleSourceFound() : processing mSourceFromQrCode with broadcastId"
+                                + " unset");
+            }
+            boolean updated =
+                    maybeUpdateId(
+                            AudioStreamsHelper.getBroadcastName(source), source.getBroadcastId());
+            if (updated && mBroadcastIdToPreferenceMap.containsKey(UNSET_BROADCAST_ID)) {
+                var preference = mBroadcastIdToPreferenceMap.remove(UNSET_BROADCAST_ID);
+                mBroadcastIdToPreferenceMap.put(source.getBroadcastId(), preference);
+            }
+        }
+
+        mBroadcastIdToPreferenceMap.compute(
+                broadcastIdFound,
+                (k, existingPreference) -> {
+                    if (existingPreference == null) {
+                        return addNewPreference(source, AudioStreamState.SYNCED);
+                    }
+                    var fromState = existingPreference.getAudioStreamState();
+                    if (fromState == AudioStreamState.WAIT_FOR_SYNC && mSourceFromQrCode != null) {
+                        // A preference with source founded is existed from a QR code scan. As the
+                        // source is now synced, we update the preference with source from scanning
+                        // as it includes complete broadcast info.
+                        existingPreference.setAudioStreamMetadata(
+                                new BluetoothLeBroadcastMetadata.Builder(source)
+                                        .setBroadcastCode(mSourceFromQrCode.getBroadcastCode())
+                                        .build());
+                        moveToState(
+                                existingPreference, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
+                    } else {
+                        // A preference with source founded existed either because it's already
+                        // connected (SOURCE_ADDED). Any other reason is unexpected. We update the
+                        // preference with this source and won't change it's state.
+                        existingPreference.setAudioStreamMetadata(source);
+                        if (fromState != AudioStreamState.SOURCE_ADDED) {
+                            Log.w(
+                                    TAG,
+                                    "handleSourceFound(): unexpected state : "
+                                            + fromState
+                                            + " for broadcastId : "
+                                            + broadcastIdFound);
+                        }
+                    }
+                    return existingPreference;
+                });
+    }
+
+    private boolean maybeUpdateId(String targetBroadcastName, int broadcastIdToSet) {
+        if (mSourceFromQrCode == null) {
+            return false;
+        }
+        if (targetBroadcastName.equals(AudioStreamsHelper.getBroadcastName(mSourceFromQrCode))) {
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "maybeUpdateId() : updating unset broadcastId for metadataFromQrCode with"
+                                + " broadcastName: "
+                                + AudioStreamsHelper.getBroadcastName(mSourceFromQrCode)
+                                + " to broadcast Id: "
+                                + broadcastIdToSet);
+            }
+            mSourceFromQrCode =
+                    new BluetoothLeBroadcastMetadata.Builder(mSourceFromQrCode)
+                            .setBroadcastId(broadcastIdToSet)
+                            .build();
+            return true;
+        }
+        return false;
+    }
+
+    // Find preference by mSourceFromQrCode and decide next state.
+    // Expect no preference existed, create new preference with state WAIT_FOR_SYNC
+    private void handleSourceFromQrCodeIfExists() {
+        if (DEBUG) {
+            Log.d(TAG, "handleSourceFromQrCodeIfExists()");
+        }
+        if (mSourceFromQrCode == null) {
+            return;
+        }
+        mBroadcastIdToPreferenceMap.compute(
+                mSourceFromQrCode.getBroadcastId(),
+                (k, existingPreference) -> {
+                    if (existingPreference == null) {
+                        // No existing preference for this source from the QR code scan, add one and
+                        // set initial state to WAIT_FOR_SYNC.
+                        // Check nullability to bypass NullAway check.
+                        if (mSourceFromQrCode != null) {
+                            return addNewPreference(
+                                    mSourceFromQrCode, AudioStreamState.WAIT_FOR_SYNC);
+                        }
+                    }
+                    Log.w(
+                            TAG,
+                            "handleSourceFromQrCodeIfExists(): unexpected state : "
+                                    + existingPreference.getAudioStreamState()
+                                    + " for broadcastId : "
+                                    + (mSourceFromQrCode == null
+                                            ? "null"
+                                            : mSourceFromQrCode.getBroadcastId()));
+                    return existingPreference;
+                });
+    }
+
+    void handleSourceLost(int broadcastId) {
+        if (DEBUG) {
+            Log.d(TAG, "handleSourceLost()");
+        }
+        if (mAudioStreamsHelper.getAllConnectedSources().stream()
+                .anyMatch(connected -> connected.getBroadcastId() == broadcastId)) {
+            Log.d(
+                    TAG,
+                    "handleSourceLost() : keep this preference as the source is still connected.");
+            return;
+        }
+        var toRemove = mBroadcastIdToPreferenceMap.remove(broadcastId);
+        if (toRemove != null) {
+            ThreadUtils.postOnMainThread(
+                    () -> {
+                        if (mCategoryPreference != null) {
+                            mCategoryPreference.removePreference(toRemove);
+                        }
+                    });
+        }
+    }
+
+    void handleSourceRemoved() {
+        if (DEBUG) {
+            Log.d(TAG, "handleSourceRemoved()");
+        }
+        for (var entry : mBroadcastIdToPreferenceMap.entrySet()) {
+            var preference = entry.getValue();
+
+            // Look for preference has SOURCE_ADDED state, re-check if they are still connected. If
+            // not, means the source is removed from the sink, we move back the preference to SYNCED
+            // state.
+            if (preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
+                    && mAudioStreamsHelper.getAllConnectedSources().stream()
+                            .noneMatch(
+                                    connected ->
+                                            connected.getBroadcastId()
+                                                    == preference.getAudioStreamBroadcastId())) {
+
+                ThreadUtils.postOnMainThread(
+                        () -> {
+                            var metadata = preference.getAudioStreamMetadata();
+
+                            if (metadata != null) {
+                                moveToState(preference, AudioStreamState.SYNCED);
+                            } else {
+                                handleSourceLost(preference.getAudioStreamBroadcastId());
+                            }
+                        });
+
+                return;
+            }
+        }
+    }
+
+    // Find preference by receiveState and decide next state.
+    // Expect one of the following:
+    // 1) No preference existed, create new preference with state SOURCE_ADDED
+    // 2) Any other state, move to SOURCE_ADDED
+    void handleSourceConnected(BluetoothLeBroadcastReceiveState receiveState) {
+        if (DEBUG) {
+            Log.d(TAG, "handleSourceConnected()");
+        }
+        if (!AudioStreamsHelper.isConnected(receiveState)) {
+            return;
+        }
+        var broadcastIdConnected = receiveState.getBroadcastId();
+        if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) {
+            // mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the
+            // connected source receiveState.
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "handleSourceConnected() : processing mSourceFromQrCode with broadcastId"
+                                + " unset");
+            }
+            boolean updated =
+                    maybeUpdateId(
+                            AudioStreamsHelper.getBroadcastName(receiveState),
+                            receiveState.getBroadcastId());
+            if (updated && mBroadcastIdToPreferenceMap.containsKey(UNSET_BROADCAST_ID)) {
+                var preference = mBroadcastIdToPreferenceMap.remove(UNSET_BROADCAST_ID);
+                mBroadcastIdToPreferenceMap.put(receiveState.getBroadcastId(), preference);
+            }
+        }
+
+        mBroadcastIdToPreferenceMap.compute(
+                broadcastIdConnected,
+                (k, existingPreference) -> {
+                    if (existingPreference == null) {
+                        // No existing preference for this source even if it's already connected,
+                        // add one and set initial state to SOURCE_ADDED. This could happen because
+                        // we retrieves the connected source during onStart() from
+                        // AudioStreamsHelper#getAllConnectedSources() even before the source is
+                        // founded by scanning.
+                        return addNewPreference(receiveState, AudioStreamState.SOURCE_ADDED);
+                    }
+                    if (existingPreference.getAudioStreamState() == AudioStreamState.WAIT_FOR_SYNC
+                            && existingPreference.getAudioStreamBroadcastId() == UNSET_BROADCAST_ID
+                            && mSourceFromQrCode != null) {
+                        existingPreference.setAudioStreamMetadata(mSourceFromQrCode);
+                    }
+                    moveToState(existingPreference, AudioStreamState.SOURCE_ADDED);
+                    return existingPreference;
+                });
+    }
+
+    // Find preference by receiveState and decide next state.
+    // Expect one preference existed, move to ADD_SOURCE_BAD_CODE
+    void handleSourceConnectBadCode(BluetoothLeBroadcastReceiveState receiveState) {
+        if (DEBUG) {
+            Log.d(TAG, "handleSourceConnectBadCode()");
+        }
+        if (!AudioStreamsHelper.isBadCode(receiveState)) {
+            return;
+        }
+        mBroadcastIdToPreferenceMap.computeIfPresent(
+                receiveState.getBroadcastId(),
+                (k, existingPreference) -> {
+                    moveToState(existingPreference, AudioStreamState.ADD_SOURCE_BAD_CODE);
+                    return existingPreference;
+                });
+    }
+
+    // Find preference by broadcastId and decide next state.
+    // Expect one preference existed, move to ADD_SOURCE_FAILED
+    void handleSourceFailedToConnect(int broadcastId) {
+        if (DEBUG) {
+            Log.d(TAG, "handleSourceFailedToConnect()");
+        }
+        mBroadcastIdToPreferenceMap.computeIfPresent(
+                broadcastId,
+                (k, existingPreference) -> {
+                    moveToState(existingPreference, AudioStreamState.ADD_SOURCE_FAILED);
+                    return existingPreference;
+                });
+    }
+
+    // Find preference by metadata and decide next state.
+    // Expect one preference existed, move to ADD_SOURCE_WAIT_FOR_RESPONSE
+    void handleSourceAddRequest(
+            AudioStreamPreference preference, BluetoothLeBroadcastMetadata metadata) {
+        if (DEBUG) {
+            Log.d(TAG, "handleSourceAddRequest()");
+        }
+        mBroadcastIdToPreferenceMap.computeIfPresent(
+                metadata.getBroadcastId(),
+                (k, existingPreference) -> {
+                    if (!existingPreference.equals(preference)) {
+                        Log.w(TAG, "handleSourceAddedRequest(): existing preference not match");
+                    }
+                    existingPreference.setAudioStreamMetadata(metadata);
+                    moveToState(existingPreference, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
+                    return existingPreference;
+                });
+    }
+
+    void showToast(String msg) {
+        AudioSharingUtils.toastMessage(mContext, msg);
+    }
+
+    private void init() {
+        mBroadcastIdToPreferenceMap.clear();
+        boolean hasConnected =
+                AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(mBluetoothManager)
+                        .isPresent();
+        AudioSharingUtils.postOnMainThread(
+                mContext,
+                () -> {
+                    if (mCategoryPreference != null) {
+                        mCategoryPreference.removeAudioStreamPreferences();
+                        mCategoryPreference.setVisible(hasConnected);
+                    }
+                });
+        if (hasConnected) {
+            startScanning();
+            AudioSharingUtils.postOnMainThread(
+                    mContext,
+                    () -> {
+                        if (mFragment != null) {
+                            AudioStreamsDialogFragment.dismissAll(mFragment);
+                        }
+                    });
+        } else {
+            stopScanning();
+            AudioSharingUtils.postOnMainThread(
+                    mContext,
+                    () -> {
+                        if (mFragment != null) {
+                            AudioStreamsDialogFragment.show(mFragment, getNoLeDeviceDialog());
+                        }
+                    });
+        }
+    }
+
+    private void startScanning() {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "startScanning(): LeBroadcastAssistant is null!");
+            return;
+        }
+        if (mLeBroadcastAssistant.isSearchInProgress()) {
+            Log.w(TAG, "startScanning(): scanning still in progress, stop scanning first.");
+            stopScanning();
+        }
+        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+        mExecutor.execute(
+                () -> {
+                    // Handle QR code scan, display currently connected streams then start scanning
+                    // sequentially
+                    handleSourceFromQrCodeIfExists();
+                    mAudioStreamsHelper
+                            .getAllConnectedSources()
+                            .forEach(this::handleSourceConnected);
+                    mLeBroadcastAssistant.startSearchingForSources(emptyList());
+                    mMediaControlHelper.start();
+                });
+    }
+
+    private void stopScanning() {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "stopScanning(): LeBroadcastAssistant is null!");
+            return;
+        }
+        if (mLeBroadcastAssistant.isSearchInProgress()) {
+            if (DEBUG) {
+                Log.d(TAG, "stopScanning()");
+            }
+            mLeBroadcastAssistant.stopSearchingForSources();
+            mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
+        }
+        mMediaControlHelper.stop();
+        mSourceFromQrCode = null;
+    }
+
+    private AudioStreamPreference addNewPreference(
+            BluetoothLeBroadcastReceiveState receiveState, AudioStreamState state) {
+        var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState);
+        moveToState(preference, state);
+        return preference;
+    }
+
+    private AudioStreamPreference addNewPreference(
+            BluetoothLeBroadcastMetadata metadata, AudioStreamState state) {
+        var preference = AudioStreamPreference.fromMetadata(mContext, metadata);
+        moveToState(preference, state);
+        return preference;
+    }
+
+    private void moveToState(AudioStreamPreference preference, AudioStreamState state) {
+        AudioStreamStateHandler stateHandler = switch (state) {
+            case SYNCED -> SyncedState.getInstance();
+            case WAIT_FOR_SYNC -> WaitForSyncState.getInstance();
+            case ADD_SOURCE_WAIT_FOR_RESPONSE ->
+                    AddSourceWaitForResponseState.getInstance();
+            case ADD_SOURCE_BAD_CODE -> AddSourceBadCodeState.getInstance();
+            case ADD_SOURCE_FAILED -> AddSourceFailedState.getInstance();
+            case SOURCE_ADDED -> SourceAddedState.getInstance();
+            default -> throw new IllegalArgumentException("Unsupported state: " + state);
+        };
+
+        stateHandler.handleStateChange(preference, this, mAudioStreamsHelper);
+
+        // Update UI with the updated preference
+        AudioSharingUtils.postOnMainThread(
+                mContext,
+                () -> {
+                    if (mCategoryPreference != null) {
+                        mCategoryPreference.addAudioStreamPreference(preference, mComparator);
+                    }
+                });
+    }
+
+    private AudioStreamsDialogFragment.DialogBuilder getNoLeDeviceDialog() {
+        return new AudioStreamsDialogFragment.DialogBuilder(mContext)
+                .setTitle(mContext.getString(R.string.audio_streams_dialog_no_le_device_title))
+                .setSubTitle2(
+                        mContext.getString(R.string.audio_streams_dialog_no_le_device_subtitle))
+                .setLeftButtonText(mContext.getString(R.string.audio_streams_dialog_close))
+                .setLeftButtonOnClickListener(AlertDialog::dismiss)
+                .setRightButtonText(
+                        mContext.getString(R.string.audio_streams_dialog_no_le_device_button))
+                .setRightButtonOnClickListener(
+                        dialog -> {
+                            mContext.startActivity(
+                                    new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
+                                            .setPackage(mContext.getPackageName()));
+                            dialog.dismiss();
+                        });
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java
new file mode 100644
index 0000000..33adc31
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryPreference.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.ProgressCategory;
+import com.android.settings.R;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+public class AudioStreamsProgressCategoryPreference extends ProgressCategory {
+
+    public AudioStreamsProgressCategoryPreference(Context context) {
+        super(context);
+        init();
+    }
+
+    public AudioStreamsProgressCategoryPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public AudioStreamsProgressCategoryPreference(
+            Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    public AudioStreamsProgressCategoryPreference(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        init();
+    }
+
+    void addAudioStreamPreference(
+            @NonNull AudioStreamPreference preference,
+            Comparator<AudioStreamPreference> comparator) {
+        super.addPreference(preference);
+
+        List<AudioStreamPreference> preferences = getAllAudioStreamPreferences();
+        preferences.sort(comparator);
+        for (int i = 0; i < preferences.size(); i++) {
+            // setOrder to i + 1, since the order 0 preference should always be the
+            // "audio_streams_scan_qr_code"
+            preferences.get(i).setOrder(i + 1);
+        }
+    }
+
+    void removeAudioStreamPreferences() {
+        List<AudioStreamPreference> streams = getAllAudioStreamPreferences();
+        for (var toRemove : streams) {
+            removePreference(toRemove);
+        }
+    }
+
+    private List<AudioStreamPreference> getAllAudioStreamPreferences() {
+        List<AudioStreamPreference> streams = new ArrayList<>();
+        for (int i = 0; i < getPreferenceCount(); i++) {
+            if (getPreference(i) instanceof AudioStreamPreference) {
+                streams.add((AudioStreamPreference) getPreference(i));
+            }
+        }
+        return streams;
+    }
+
+    private void init() {
+        setEmptyTextRes(R.string.audio_streams_empty);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
new file mode 100644
index 0000000..4b6dfa5
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsQrCodeFragment.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.core.InstrumentedFragment;
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.qrcode.QrCodeGenerator;
+
+import com.google.zxing.WriterException;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
+    private static final String TAG = "AudioStreamsQrCodeFragment";
+
+    @Override
+    public int getMetricsCategory() {
+        // TODO(chelseahao): update metrics id
+        return 0;
+    }
+
+    @Override
+    public final View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        View view = inflater.inflate(R.xml.bluetooth_audio_streams_qr_code, container, false);
+
+        BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata();
+
+        if (broadcastMetadata != null) {
+            Optional<Bitmap> bm = getQrCodeBitmap(broadcastMetadata);
+            if (bm.isEmpty()) {
+                return view;
+            }
+            ((ImageView) view.requireViewById(R.id.qrcode_view)).setImageBitmap(bm.get());
+            if (broadcastMetadata.getBroadcastCode() != null) {
+                String password =
+                        new String(broadcastMetadata.getBroadcastCode(), StandardCharsets.UTF_8);
+                String passwordText =
+                        getContext()
+                                .getString(R.string.audio_streams_qr_code_page_password, password);
+                ((TextView) view.requireViewById(R.id.password)).setText(passwordText);
+            }
+            TextView summaryView = view.requireViewById(android.R.id.summary);
+            String summary =
+                    view.getContext()
+                            .getString(
+                                    R.string.audio_streams_qr_code_page_description,
+                                    broadcastMetadata.getBroadcastName());
+            summaryView.setText(summary);
+        }
+        return view;
+    }
+
+    private Optional<Bitmap> getQrCodeBitmap(@Nullable BluetoothLeBroadcastMetadata metadata) {
+        if (metadata == null) {
+            Log.d(TAG, "onCreateView: broadcastMetadata is empty!");
+            return Optional.empty();
+        }
+        String metadataStr = BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata);
+        if (metadataStr.isEmpty()) {
+            Log.d(TAG, "onCreateView: metadataStr is empty!");
+            return Optional.empty();
+        }
+        Log.i(TAG, "onCreateView: metadataStr : " + metadataStr);
+        try {
+            int qrcodeSize =
+                    getContext()
+                            .getResources()
+                            .getDimensionPixelSize(R.dimen.audio_streams_qrcode_size);
+            Bitmap bitmap = QrCodeGenerator.encodeQrCode(metadataStr, qrcodeSize);
+            return Optional.of(bitmap);
+        } catch (WriterException e) {
+            Log.d(
+                    TAG,
+                    "onCreateView: broadcastMetadata "
+                            + metadata
+                            + " qrCode generation exception "
+                            + e);
+        }
+
+        return Optional.empty();
+    }
+
+    @Nullable
+    private BluetoothLeBroadcastMetadata getBroadcastMetadata() {
+        LocalBluetoothLeBroadcast localBluetoothLeBroadcast =
+                Utils.getLocalBtManager(getActivity())
+                        .getProfileManager()
+                        .getLeAudioBroadcastProfile();
+        if (localBluetoothLeBroadcast == null) {
+            Log.d(TAG, "getBroadcastMetadataQrCode: localBluetoothLeBroadcast is null!");
+            return null;
+        }
+
+        BluetoothLeBroadcastMetadata metadata =
+                localBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata();
+        if (metadata == null) {
+            Log.d(TAG, "getBroadcastMetadataQrCode: metadata is null!");
+            return null;
+        }
+
+        return metadata;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java
new file mode 100644
index 0000000..2c60e85
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsRepository.java
@@ -0,0 +1,159 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.Nullable;
+
+/** Manages the caching and storage of Bluetooth audio stream metadata. */
+public class AudioStreamsRepository {
+
+    private static final String TAG = "AudioStreamsRepository";
+    private static final boolean DEBUG = BluetoothUtils.D;
+
+    private static final String PREF_KEY = "bluetooth_audio_stream_pref";
+    private static final String METADATA_KEY = "bluetooth_audio_stream_metadata";
+
+    @Nullable private static AudioStreamsRepository sInstance = null;
+
+    private AudioStreamsRepository() {}
+
+    /**
+     * Gets the single instance of AudioStreamsRepository.
+     *
+     * @return The AudioStreamsRepository instance.
+     */
+    public static synchronized AudioStreamsRepository getInstance() {
+        if (sInstance == null) {
+            sInstance = new AudioStreamsRepository();
+        }
+        return sInstance;
+    }
+
+    private final ConcurrentHashMap<Integer, BluetoothLeBroadcastMetadata>
+            mBroadcastIdToMetadataCacheMap = new ConcurrentHashMap<>();
+
+    /**
+     * Caches BluetoothLeBroadcastMetadata in a local cache.
+     *
+     * @param metadata The BluetoothLeBroadcastMetadata to be cached.
+     */
+    void cacheMetadata(BluetoothLeBroadcastMetadata metadata) {
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "cacheMetadata(): broadcastId "
+                            + metadata.getBroadcastId()
+                            + " saved in local cache.");
+        }
+        mBroadcastIdToMetadataCacheMap.put(metadata.getBroadcastId(), metadata);
+    }
+
+    /**
+     * Gets cached BluetoothLeBroadcastMetadata by broadcastId.
+     *
+     * @param broadcastId The broadcastId to look up in the cache.
+     * @return The cached BluetoothLeBroadcastMetadata or null if not found.
+     */
+    @Nullable
+    BluetoothLeBroadcastMetadata getCachedMetadata(int broadcastId) {
+        var metadata = mBroadcastIdToMetadataCacheMap.get(broadcastId);
+        if (metadata == null) {
+            Log.w(
+                    TAG,
+                    "getCachedMetadata(): broadcastId not found in"
+                            + " mBroadcastIdToMetadataCacheMap.");
+            return null;
+        }
+        return metadata;
+    }
+
+    /**
+     * Saves metadata to SharedPreferences asynchronously.
+     *
+     * @param context The context.
+     * @param metadata The BluetoothLeBroadcastMetadata to be saved.
+     */
+    void saveMetadata(Context context, BluetoothLeBroadcastMetadata metadata) {
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            SharedPreferences sharedPref =
+                                    context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
+                            if (sharedPref != null) {
+                                SharedPreferences.Editor editor = sharedPref.edit();
+                                editor.putString(
+                                        METADATA_KEY,
+                                        BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(
+                                                metadata));
+                                editor.apply();
+                                if (DEBUG) {
+                                    Log.d(
+                                            TAG,
+                                            "saveMetadata(): broadcastId "
+                                                    + metadata.getBroadcastId()
+                                                    + " metadata saved in storage.");
+                                }
+                            }
+                        });
+    }
+
+    /**
+     * Gets saved metadata from SharedPreferences.
+     *
+     * @param context The context.
+     * @param broadcastId The broadcastId to retrieve metadata for.
+     * @return The saved BluetoothLeBroadcastMetadata or null if not found.
+     */
+    @Nullable
+    BluetoothLeBroadcastMetadata getSavedMetadata(Context context, int broadcastId) {
+        SharedPreferences sharedPref = context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
+        if (sharedPref != null) {
+            String savedMetadataStr = sharedPref.getString(METADATA_KEY, null);
+            if (savedMetadataStr == null) {
+                Log.w(TAG, "getSavedMetadata(): savedMetadataStr is null");
+                return null;
+            }
+            var savedMetadata =
+                    BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
+                            savedMetadataStr);
+            if (savedMetadata == null || savedMetadata.getBroadcastId() != broadcastId) {
+                Log.w(TAG, "getSavedMetadata(): savedMetadata doesn't match broadcast Id.");
+                return null;
+            }
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "getSavedMetadata(): broadcastId "
+                                + savedMetadata.getBroadcastId()
+                                + " metadata found in storage.");
+            }
+            return savedMetadata;
+        }
+        return null;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
new file mode 100644
index 0000000..b63ec7e
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeActivity;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+public class AudioStreamsScanQrCodeController extends BasePreferenceController
+        implements DefaultLifecycleObserver {
+    static final int REQUEST_SCAN_BT_BROADCAST_QR_CODE = 0;
+    private static final String TAG = "AudioStreamsProgressCategoryController";
+    private static final boolean DEBUG = BluetoothUtils.D;
+    private static final String KEY = "audio_streams_scan_qr_code";
+    private final BluetoothCallback mBluetoothCallback =
+            new BluetoothCallback() {
+                @Override
+                public void onActiveDeviceChanged(
+                        @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
+                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
+                        updateVisibility();
+                    }
+                }
+            };
+
+    @Nullable private final LocalBluetoothManager mLocalBtManager;
+    @Nullable private AudioStreamsDashboardFragment mFragment;
+    @Nullable private Preference mPreference;
+
+    public AudioStreamsScanQrCodeController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+        mLocalBtManager = Utils.getLocalBtManager(mContext);
+    }
+
+    public void setFragment(AudioStreamsDashboardFragment fragment) {
+        mFragment = fragment;
+    }
+
+    @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (mLocalBtManager != null) {
+            mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback);
+        }
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (mLocalBtManager != null) {
+            mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback);
+        }
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return AVAILABLE;
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return KEY;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreference = screen.findPreference(getPreferenceKey());
+        if (mPreference == null) {
+            Log.w(TAG, "displayPreference() mPreference is null!");
+            return;
+        }
+        mPreference.setOnPreferenceClickListener(
+                preference -> {
+                    if (mFragment == null) {
+                        Log.w(TAG, "displayPreference() mFragment is null!");
+                        return false;
+                    }
+                    if (preference.getKey().equals(KEY)) {
+                        Intent intent = new Intent(mContext, QrCodeScanModeActivity.class);
+                        intent.setAction(
+                                BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER);
+                        mFragment.startActivityForResult(intent, REQUEST_SCAN_BT_BROADCAST_QR_CODE);
+                        if (DEBUG) {
+                            Log.w(TAG, "displayPreference() sent intent : " + intent);
+                        }
+                        return true;
+                    }
+                    return false;
+                });
+    }
+
+    private void updateVisibility() {
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            boolean hasConnectedLe =
+                                    AudioStreamsHelper
+                                            .getCachedBluetoothDeviceInSharingOrLeConnected(
+                                                    mLocalBtManager)
+                                            .isPresent();
+                            ThreadUtils.postOnMainThread(
+                                    () -> {
+                                        if (mPreference != null) {
+                                            mPreference.setVisible(hasConnectedLe);
+                                        }
+                                    });
+                        });
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/MediaControlHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/MediaControlHelper.java
new file mode 100644
index 0000000..0f6b786
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/MediaControlHelper.java
@@ -0,0 +1,133 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.content.Context;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.media.BluetoothMediaDevice;
+import com.android.settingslib.media.LocalMediaManager;
+import com.android.settingslib.media.MediaDevice;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+class MediaControlHelper {
+    private static final String TAG = "MediaControlHelper";
+    private final Context mContext;
+    private final MediaSessionManager mMediaSessionManager;
+    @Nullable private final LocalBluetoothManager mLocalBluetoothManager;
+    private final List<Pair<LocalMediaManager, LocalMediaManager.DeviceCallback>>
+            mLocalMediaManagers = new ArrayList<>();
+
+    MediaControlHelper(Context context, @Nullable LocalBluetoothManager localBluetoothManager) {
+        mContext = context;
+        mMediaSessionManager = context.getSystemService(MediaSessionManager.class);
+        mLocalBluetoothManager = localBluetoothManager;
+    }
+
+    void start() {
+        if (mLocalBluetoothManager == null) {
+            return;
+        }
+        var currentLeDevice =
+                AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
+                        mLocalBluetoothManager);
+        if (currentLeDevice.isEmpty()) {
+            Log.d(TAG, "start() : current LE device is empty!");
+            return;
+        }
+
+        for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) {
+            String packageName = controller.getPackageName();
+
+            // We won't stop media created from settings.
+            if (Objects.equals(packageName, mContext.getPackageName())) {
+                Log.d(TAG, "start() : skip package: " + packageName);
+                continue;
+            }
+
+            // Start scanning and listen to device list update, stop this media if device matched.
+            var localMediaManager = new LocalMediaManager(mContext, packageName);
+            var deviceCallback =
+                    new LocalMediaManager.DeviceCallback() {
+                        public void onDeviceListUpdate(List<MediaDevice> devices) {
+                            if (shouldStopMedia(
+                                    controller,
+                                    currentLeDevice.get(),
+                                    localMediaManager.getCurrentConnectedDevice())) {
+                                Log.d(
+                                        TAG,
+                                        "start() : Stopping media player for package: "
+                                                + controller.getPackageName());
+                                var controls = controller.getTransportControls();
+                                if (controls != null) {
+                                    controls.stop();
+                                }
+                            }
+                        }
+                    };
+            localMediaManager.registerCallback(deviceCallback);
+            localMediaManager.startScan();
+            mLocalMediaManagers.add(new Pair<>(localMediaManager, deviceCallback));
+        }
+    }
+
+    void stop() {
+        mLocalMediaManagers.forEach(
+                m -> {
+                    m.first.stopScan();
+                    m.first.unregisterCallback(m.second);
+                });
+        mLocalMediaManagers.clear();
+    }
+
+    private static boolean shouldStopMedia(
+            MediaController controller,
+            CachedBluetoothDevice currentLeDevice,
+            MediaDevice currentMediaDevice) {
+        // We won't stop media if it's already stopped.
+        if (controller.getPlaybackState() != null
+                && controller.getPlaybackState().getState() == PlaybackState.STATE_STOPPED) {
+            Log.d(TAG, "shouldStopMedia() : skip already stopped: " + controller.getPackageName());
+            return false;
+        }
+
+        var deviceForMedia =
+                currentMediaDevice instanceof BluetoothMediaDevice
+                        ? (BluetoothMediaDevice) currentMediaDevice
+                        : null;
+        return deviceForMedia != null
+                && hasOverlap(deviceForMedia.getCachedDevice(), currentLeDevice);
+    }
+
+    private static boolean hasOverlap(
+            CachedBluetoothDevice device1, CachedBluetoothDevice device2) {
+        return device1.equals(device2)
+                || device1.getMemberDevice().contains(device2)
+                || device2.getMemberDevice().contains(device1);
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedState.java
new file mode 100644
index 0000000..4fdaf15
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedState.java
@@ -0,0 +1,94 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.app.settings.SettingsEnums;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.core.SubSettingLauncher;
+
+class SourceAddedState extends AudioStreamStateHandler {
+    @VisibleForTesting
+    static final int AUDIO_STREAM_SOURCE_ADDED_STATE_SUMMARY = R.string.audio_streams_listening_now;
+
+    @Nullable private static SourceAddedState sInstance = null;
+
+    private SourceAddedState() {}
+
+    static SourceAddedState getInstance() {
+        if (sInstance == null) {
+            sInstance = new SourceAddedState();
+        }
+        return sInstance;
+    }
+
+    @Override
+    void performAction(
+            AudioStreamPreference preference,
+            AudioStreamsProgressCategoryController controller,
+            AudioStreamsHelper helper) {
+        var context = preference.getContext();
+        // Saved connected metadata for user to re-join this broadcast later.
+        var cached =
+                mAudioStreamsRepository.getCachedMetadata(preference.getAudioStreamBroadcastId());
+        if (cached != null) {
+            mAudioStreamsRepository.saveMetadata(context, cached);
+        }
+        helper.startMediaService(
+                context,
+                preference.getAudioStreamBroadcastId(),
+                String.valueOf(preference.getTitle()));
+    }
+
+    @Override
+    int getSummary() {
+        return AUDIO_STREAM_SOURCE_ADDED_STATE_SUMMARY;
+    }
+
+    @Override
+    Preference.OnPreferenceClickListener getOnClickListener(
+            AudioStreamsProgressCategoryController controller) {
+        return preference -> {
+            var p = (AudioStreamPreference) preference;
+            Bundle broadcast = new Bundle();
+            broadcast.putString(
+                    AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) p.getTitle());
+            broadcast.putInt(
+                    AudioStreamDetailsFragment.BROADCAST_ID_ARG, p.getAudioStreamBroadcastId());
+
+            new SubSettingLauncher(p.getContext())
+                    .setTitleText(
+                            p.getContext().getString(R.string.audio_streams_detail_page_title))
+                    .setDestination(AudioStreamDetailsFragment.class.getName())
+                    // TODO(chelseahao): Add logging enum
+                    .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
+                    .setArguments(broadcast)
+                    .launch();
+            return true;
+        };
+    }
+
+    @Override
+    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
+        return AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED;
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SyncedState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SyncedState.java
new file mode 100644
index 0000000..dffb235
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SyncedState.java
@@ -0,0 +1,114 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import android.app.AlertDialog;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.nio.charset.StandardCharsets;
+
+class SyncedState extends AudioStreamStateHandler {
+    private static final String TAG = "SyncedState";
+    private static final boolean DEBUG = BluetoothUtils.D;
+    @Nullable private static SyncedState sInstance = null;
+
+    SyncedState() {}
+
+    static SyncedState getInstance() {
+        if (sInstance == null) {
+            sInstance = new SyncedState();
+        }
+        return sInstance;
+    }
+
+    @Override
+    Preference.OnPreferenceClickListener getOnClickListener(
+            AudioStreamsProgressCategoryController controller) {
+        return p -> addSourceOrShowDialog(p, controller);
+    }
+
+    @Override
+    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
+        return AudioStreamsProgressCategoryController.AudioStreamState.SYNCED;
+    }
+
+    private boolean addSourceOrShowDialog(
+            Preference preference, AudioStreamsProgressCategoryController controller) {
+        var p = (AudioStreamPreference) preference;
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "preferenceClicked(): attempt to join broadcast id : "
+                            + p.getAudioStreamBroadcastId());
+        }
+        var source = p.getAudioStreamMetadata();
+        if (source != null) {
+            if (source.isEncrypted()) {
+                ThreadUtils.postOnMainThread(() -> launchPasswordDialog(source, p, controller));
+            } else {
+                controller.handleSourceAddRequest(p, source);
+            }
+        }
+        return true;
+    }
+
+    private void launchPasswordDialog(
+            BluetoothLeBroadcastMetadata source,
+            AudioStreamPreference preference,
+            AudioStreamsProgressCategoryController controller) {
+        View layout =
+                LayoutInflater.from(preference.getContext())
+                        .inflate(R.layout.bluetooth_find_broadcast_password_dialog, null);
+        ((TextView) layout.requireViewById(R.id.broadcast_name_text))
+                .setText(preference.getTitle());
+        AlertDialog alertDialog =
+                new AlertDialog.Builder(preference.getContext())
+                        .setTitle(R.string.find_broadcast_password_dialog_title)
+                        .setView(layout)
+                        .setNeutralButton(android.R.string.cancel, null)
+                        .setPositiveButton(
+                                R.string.bluetooth_connect_access_dialog_positive,
+                                (dialog, which) -> {
+                                    var code =
+                                            ((EditText)
+                                                            layout.requireViewById(
+                                                                    R.id.broadcast_edit_text))
+                                                    .getText()
+                                                    .toString();
+                                    var metadata =
+                                            new BluetoothLeBroadcastMetadata.Builder(source)
+                                                    .setBroadcastCode(
+                                                            code.getBytes(StandardCharsets.UTF_8))
+                                                    .build();
+                                    controller.handleSourceAddRequest(preference, metadata);
+                                })
+                        .create();
+        alertDialog.show();
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncState.java
new file mode 100644
index 0000000..ac4d9a1
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncState.java
@@ -0,0 +1,117 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController.REQUEST_SCAN_BT_BROADCAST_QR_CODE;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settings.R;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeActivity;
+import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
+import com.android.settingslib.utils.ThreadUtils;
+
+class WaitForSyncState extends AudioStreamStateHandler {
+    @VisibleForTesting
+    static final int AUDIO_STREAM_WAIT_FOR_SYNC_STATE_SUMMARY =
+            R.string.audio_streams_wait_for_sync_state_summary;
+
+    @VisibleForTesting static final int WAIT_FOR_SYNC_TIMEOUT_MILLIS = 15000;
+
+    @Nullable private static WaitForSyncState sInstance = null;
+
+    private WaitForSyncState() {}
+
+    static WaitForSyncState getInstance() {
+        if (sInstance == null) {
+            sInstance = new WaitForSyncState();
+        }
+        return sInstance;
+    }
+
+    @Override
+    void performAction(
+            AudioStreamPreference preference,
+            AudioStreamsProgressCategoryController controller,
+            AudioStreamsHelper helper) {
+        var metadata = preference.getAudioStreamMetadata();
+        if (metadata != null) {
+            mHandler.postDelayed(
+                    () -> {
+                        if (preference.isShown()
+                                && preference.getAudioStreamState() == getStateEnum()) {
+                            controller.handleSourceLost(preference.getAudioStreamBroadcastId());
+                            ThreadUtils.postOnMainThread(
+                                    () -> {
+                                        if (controller.getFragment() != null) {
+                                            AudioStreamsDialogFragment.show(
+                                                    controller.getFragment(),
+                                                    getBroadcastUnavailableDialog(
+                                                            preference.getContext(),
+                                                            AudioStreamsHelper.getBroadcastName(
+                                                                    metadata),
+                                                            controller));
+                                        }
+                                    });
+                        }
+                    },
+                    WAIT_FOR_SYNC_TIMEOUT_MILLIS);
+        }
+    }
+
+    @Override
+    int getSummary() {
+        return AUDIO_STREAM_WAIT_FOR_SYNC_STATE_SUMMARY;
+    }
+
+    @Override
+    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
+        return AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC;
+    }
+
+    private AudioStreamsDialogFragment.DialogBuilder getBroadcastUnavailableDialog(
+            Context context,
+            String broadcastName,
+            AudioStreamsProgressCategoryController controller) {
+        return new AudioStreamsDialogFragment.DialogBuilder(context)
+                .setTitle(context.getString(R.string.audio_streams_dialog_stream_is_not_available))
+                .setSubTitle1(broadcastName)
+                .setSubTitle2(context.getString(R.string.audio_streams_is_not_playing))
+                .setLeftButtonText(context.getString(R.string.audio_streams_dialog_close))
+                .setLeftButtonOnClickListener(AlertDialog::dismiss)
+                .setRightButtonText(context.getString(R.string.audio_streams_dialog_retry))
+                .setRightButtonOnClickListener(
+                        dialog -> {
+                            if (controller.getFragment() != null) {
+                                Intent intent = new Intent(context, QrCodeScanModeActivity.class);
+                                intent.setAction(
+                                        BluetoothBroadcastUtils
+                                                .ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER);
+                                controller
+                                        .getFragment()
+                                        .startActivityForResult(
+                                                intent, REQUEST_SCAN_BT_BROADCAST_QR_CODE);
+                                dialog.dismiss();
+                            }
+                        });
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeActivity.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeActivity.java
new file mode 100644
index 0000000..aa460d3
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeActivity.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams.qrcode;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.fragment.app.FragmentTransaction;
+
+import com.android.settings.R;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
+import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+
+/**
+ * Finding a broadcast through QR code.
+ *
+ * <p>To use intent action {@link
+ * BluetoothBroadcastUtils#ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER}, specify the bluetooth device
+ * sink of the broadcast to be provisioned in {@link
+ * BluetoothBroadcastUtils#EXTRA_BLUETOOTH_DEVICE_SINK} and check the operation for all coordinated
+ * set members throughout one session or not by {@link
+ * BluetoothBroadcastUtils#EXTRA_BLUETOOTH_SINK_IS_GROUP}.
+ */
+public class QrCodeScanModeActivity extends QrCodeScanModeBaseActivity {
+    private static final boolean DEBUG = BluetoothUtils.D;
+    private static final String TAG = "QrCodeScanModeActivity";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    protected void handleIntent(Intent intent) {
+        if (!AudioSharingUtils.isFeatureEnabled()) {
+            finish();
+        }
+        String action = intent != null ? intent.getAction() : null;
+        if (DEBUG) {
+            Log.d(TAG, "handleIntent(), action = " + action);
+        }
+
+        if (action == null) {
+            finish();
+            return;
+        }
+
+        switch (action) {
+            case BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER:
+                showQrCodeScannerFragment(intent);
+                break;
+            default:
+                if (DEBUG) {
+                    Log.e(TAG, "Launch with an invalid action");
+                }
+                finish();
+        }
+    }
+
+    protected void showQrCodeScannerFragment(Intent intent) {
+        if (intent == null) {
+            if (DEBUG) {
+                Log.d(TAG, "intent is null, can not get bluetooth information from intent.");
+            }
+            return;
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "showQrCodeScannerFragment");
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "get extra from intent");
+        }
+
+        QrCodeScanModeFragment fragment =
+                (QrCodeScanModeFragment)
+                        mFragmentManager.findFragmentByTag(
+                                BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
+
+        if (fragment == null) {
+            fragment = new QrCodeScanModeFragment();
+        } else {
+            if (fragment.isVisible()) {
+                return;
+            }
+
+            // When the fragment in back stack but not on top of the stack, we can simply pop
+            // stack because current fragment transactions are arranged in an order
+            mFragmentManager.popBackStackImmediate();
+            return;
+        }
+        final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
+
+        fragmentTransaction.replace(
+                R.id.fragment_container,
+                fragment,
+                BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
+        fragmentTransaction.commit();
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeBaseActivity.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeBaseActivity.java
new file mode 100644
index 0000000..637014a
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeBaseActivity.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams.qrcode;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.SystemProperties;
+
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settings.R;
+import com.android.settingslib.core.lifecycle.ObservableActivity;
+
+import com.google.android.setupdesign.util.ThemeHelper;
+import com.google.android.setupdesign.util.ThemeResolver;
+
+public abstract class QrCodeScanModeBaseActivity extends ObservableActivity {
+
+    private static final String THEME_KEY = "setupwizard.theme";
+    private static final String THEME_DEFAULT_VALUE = "SudThemeGlifV3_DayNight";
+    protected FragmentManager mFragmentManager;
+
+    protected abstract void handleIntent(Intent intent);
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        int defaultTheme =
+                ThemeHelper.isSetupWizardDayNightEnabled(this)
+                        ? com.google.android.setupdesign.R.style.SudThemeGlifV3_DayNight
+                        : com.google.android.setupdesign.R.style.SudThemeGlifV3_Light;
+        ThemeResolver themeResolver =
+                new ThemeResolver.Builder(ThemeResolver.getDefault())
+                        .setDefaultTheme(defaultTheme)
+                        .setUseDayNight(true)
+                        .build();
+        setTheme(
+                themeResolver.resolve(
+                        SystemProperties.get(THEME_KEY, THEME_DEFAULT_VALUE),
+                        /* suppressDayNight= */ !ThemeHelper.isSetupWizardDayNightEnabled(this)));
+
+        setContentView(R.layout.qrcode_scan_mode_activity);
+        mFragmentManager = getSupportFragmentManager();
+
+        if (savedInstanceState == null) {
+            handleIntent(getIntent());
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java
new file mode 100644
index 0000000..a00c29b
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/qrcode/QrCodeScanModeFragment.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing.audiostreams.qrcode;
+
+import android.app.Activity;
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Matrix;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.util.Log;
+import android.util.Size;
+import android.view.LayoutInflater;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsHelper;
+import com.android.settings.core.InstrumentedFragment;
+import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.qrcode.QrCamera;
+
+import java.time.Duration;
+
+public class QrCodeScanModeFragment extends InstrumentedFragment
+        implements TextureView.SurfaceTextureListener, QrCamera.ScannerCallback {
+    private static final boolean DEBUG = BluetoothUtils.D;
+    private static final String TAG = "QrCodeScanModeFragment";
+
+    /** Message sent to hide error message */
+    private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1;
+
+    /** Message sent to show error message */
+    private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2;
+
+    /** Message sent to broadcast QR code */
+    private static final int MESSAGE_SCAN_BROADCAST_SUCCESS = 3;
+
+    private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000;
+    private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000;
+
+    private static final Duration VIBRATE_DURATION_QR_CODE_RECOGNITION = Duration.ofMillis(3);
+
+    public static final String KEY_BROADCAST_METADATA = "key_broadcast_metadata";
+
+    private LocalBluetoothManager mLocalBluetoothManager;
+    private int mCornerRadius;
+    @Nullable private String mBroadcastMetadata;
+    private Context mContext;
+    @Nullable private QrCamera mCamera;
+    private TextureView mTextureView;
+    private TextView mSummary;
+    private TextView mErrorMessage;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mContext = getContext();
+        mLocalBluetoothManager = Utils.getLocalBluetoothManager(mContext);
+    }
+
+    @Override
+    public final View onCreateView(
+            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        return inflater.inflate(
+                R.layout.qrcode_scanner_fragment, container, /* attachToRoot */ false);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        mTextureView = view.findViewById(R.id.preview_view);
+        mCornerRadius =
+                mContext.getResources()
+                        .getDimensionPixelSize(R.dimen.audio_streams_qrcode_preview_radius);
+        mTextureView.setSurfaceTextureListener(this);
+        mTextureView.setOutlineProvider(
+                new ViewOutlineProvider() {
+                    @Override
+                    public void getOutline(View view, Outline outline) {
+                        outline.setRoundRect(
+                                0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
+                    }
+                });
+        mTextureView.setClipToOutline(true);
+        mErrorMessage = view.findViewById(R.id.error_message);
+
+        var device =
+                AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
+                        mLocalBluetoothManager);
+        mSummary = view.findViewById(android.R.id.summary);
+        if (mSummary != null && device.isPresent()) {
+            mSummary.setText(
+                    getString(
+                            R.string.audio_streams_main_page_qr_code_scanner_summary,
+                            device.get().getName()));
+        }
+    }
+
+    private void initCamera(SurfaceTexture surface) {
+        // Check if the camera has already created.
+        if (mCamera == null) {
+            mCamera = new QrCamera(mContext, this);
+            mCamera.start(surface);
+        }
+    }
+
+    private void destroyCamera() {
+        if (mCamera != null) {
+            mCamera.stop();
+            mCamera = null;
+        }
+    }
+
+    @Override
+    public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
+        initCamera(surface);
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(
+            @NonNull SurfaceTexture surface, int width, int height) {}
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
+        destroyCamera();
+        return true;
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {}
+
+    @Override
+    public void handleSuccessfulResult(String qrCode) {
+        if (DEBUG) {
+            Log.d(TAG, "handleSuccessfulResult(), get the qr code string.");
+        }
+        mBroadcastMetadata = qrCode;
+        handleBtLeAudioScanner();
+    }
+
+    @Override
+    public void handleCameraFailure() {
+        destroyCamera();
+    }
+
+    @Override
+    public Size getViewSize() {
+        return new Size(mTextureView.getWidth(), mTextureView.getHeight());
+    }
+
+    @Override
+    public Rect getFramePosition(Size previewSize, int cameraOrientation) {
+        return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight());
+    }
+
+    @Override
+    public void setTransform(Matrix transform) {
+        mTextureView.setTransform(transform);
+    }
+
+    @Override
+    public boolean isValid(String qrCode) {
+        if (qrCode.startsWith(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)) {
+            return true;
+        } else {
+            showErrorMessage(R.string.audio_streams_qr_code_is_not_valid_format);
+            return false;
+        }
+    }
+
+    protected boolean isDecodeTaskAlive() {
+        return mCamera != null && mCamera.isDecodeTaskAlive();
+    }
+
+    private final Handler mHandler =
+            new Handler() {
+                @Override
+                public void handleMessage(Message msg) {
+                    switch (msg.what) {
+                        case MESSAGE_HIDE_ERROR_MESSAGE:
+                            mErrorMessage.setVisibility(View.INVISIBLE);
+                            break;
+
+                        case MESSAGE_SHOW_ERROR_MESSAGE:
+                            final String errorMessage = (String) msg.obj;
+
+                            mErrorMessage.setVisibility(View.VISIBLE);
+                            mErrorMessage.setText(errorMessage);
+                            mErrorMessage.sendAccessibilityEvent(
+                                    AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+
+                            // Cancel any pending messages to hide error view and requeue the
+                            // message so
+                            // user has time to see error
+                            removeMessages(MESSAGE_HIDE_ERROR_MESSAGE);
+                            sendEmptyMessageDelayed(
+                                    MESSAGE_HIDE_ERROR_MESSAGE, SHOW_ERROR_MESSAGE_INTERVAL);
+                            break;
+
+                        case MESSAGE_SCAN_BROADCAST_SUCCESS:
+                            Log.d(TAG, "scan success");
+                            final Intent resultIntent = new Intent();
+                            resultIntent.putExtra(KEY_BROADCAST_METADATA, mBroadcastMetadata);
+                            getActivity().setResult(Activity.RESULT_OK, resultIntent);
+                            notifyUserForQrCodeRecognition();
+                            break;
+                        default:
+                    }
+                }
+            };
+
+    private void notifyUserForQrCodeRecognition() {
+        if (mCamera != null) {
+            mCamera.stop();
+        }
+
+        mErrorMessage.setVisibility(View.INVISIBLE);
+        mTextureView.setVisibility(View.INVISIBLE);
+
+        triggerVibrationForQrCodeRecognition(getContext());
+
+        getActivity().finish();
+    }
+
+    private static void triggerVibrationForQrCodeRecognition(Context context) {
+        Vibrator vibrator = context.getSystemService(Vibrator.class);
+        if (vibrator == null) {
+            return;
+        }
+        vibrator.vibrate(
+                VibrationEffect.createOneShot(
+                        VIBRATE_DURATION_QR_CODE_RECOGNITION.toMillis(),
+                        VibrationEffect.DEFAULT_AMPLITUDE));
+    }
+
+    private void showErrorMessage(@StringRes int messageResId) {
+        final Message message =
+                mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE, getString(messageResId));
+        message.sendToTarget();
+    }
+
+    private void handleBtLeAudioScanner() {
+        Message message = mHandler.obtainMessage(MESSAGE_SCAN_BROADCAST_SUCCESS);
+        mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL);
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.LE_AUDIO_BROADCAST_SCAN_QR_CODE;
+    }
+}
diff --git a/src/com/android/settings/overlay/FeatureFactory.kt b/src/com/android/settings/overlay/FeatureFactory.kt
index ef63f19..675d789 100644
--- a/src/com/android/settings/overlay/FeatureFactory.kt
+++ b/src/com/android/settings/overlay/FeatureFactory.kt
@@ -24,7 +24,6 @@
 import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider
 import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider
 import com.android.settings.bluetooth.BluetoothFeatureProvider
-import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider
 import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider
 import com.android.settings.connecteddevice.stylus.StylusFeatureProvider
 import com.android.settings.dashboard.DashboardFeatureProvider
@@ -185,11 +184,6 @@
     abstract val displayFeatureProvider: DisplayFeatureProvider
 
     /**
-     * Gets implementation for audio sharing related feature.
-     */
-    abstract val audioSharingFeatureProvider: AudioSharingFeatureProvider
-
-    /**
      * Gets implementation for sync across devices related feature.
      */
     abstract val syncAcrossDevicesFeatureProvider: SyncAcrossDevicesFeatureProvider
diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.kt b/src/com/android/settings/overlay/FeatureFactoryImpl.kt
index c74260c..2142ea5 100644
--- a/src/com/android/settings/overlay/FeatureFactoryImpl.kt
+++ b/src/com/android/settings/overlay/FeatureFactoryImpl.kt
@@ -34,8 +34,6 @@
 import com.android.settings.biometrics2.factory.BiometricsRepositoryProviderImpl
 import com.android.settings.bluetooth.BluetoothFeatureProvider
 import com.android.settings.bluetooth.BluetoothFeatureProviderImpl
-import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider
-import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProviderImpl
 import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl
 import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider
 import com.android.settings.connecteddevice.fastpair.FastPairFeatureProviderImpl
@@ -196,10 +194,6 @@
         DisplayFeatureProviderImpl()
     }
 
-    override val audioSharingFeatureProvider: AudioSharingFeatureProvider by lazy {
-        AudioSharingFeatureProviderImpl()
-    }
-
     override val syncAcrossDevicesFeatureProvider: SyncAcrossDevicesFeatureProvider by lazy {
         SyncAcrossDevicesFeatureProviderImpl()
     }
diff --git a/tests/robotests/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdaterTest.java
index 6aa2831..fc19728 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdaterTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/AvailableMediaBluetoothDeviceUpdaterTest.java
@@ -24,26 +24,32 @@
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
 import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.media.AudioManager;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.Pair;
 
 import com.android.settings.connecteddevice.DevicePreferenceCallback;
-import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider;
 import com.android.settings.dashboard.DashboardFragment;
-import com.android.settings.testutils.FakeFeatureFactory;
 import com.android.settings.testutils.shadow.ShadowAudioManager;
 import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
 import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.flags.Flags;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -55,6 +61,7 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.List;
 
 @RunWith(RobolectricTestRunner.class)
 @Config(
@@ -66,6 +73,8 @@
 public class AvailableMediaBluetoothDeviceUpdaterTest {
     private static final String MAC_ADDRESS = "04:52:C7:0B:D8:3C";
 
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     @Mock private DashboardFragment mDashboardFragment;
     @Mock private DevicePreferenceCallback mDevicePreferenceCallback;
     @Mock private CachedBluetoothDevice mCachedBluetoothDevice;
@@ -73,6 +82,9 @@
     @Mock private Drawable mDrawable;
     @Mock private LocalBluetoothManager mLocalBtManager;
     @Mock private CachedBluetoothDeviceManager mCachedDeviceManager;
+    @Mock private LocalBluetoothProfileManager mProfileManager;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private BluetoothLeBroadcastReceiveState mBroadcastReceiveState;
 
     private Context mContext;
     private AvailableMediaBluetoothDeviceUpdater mBluetoothDeviceUpdater;
@@ -80,20 +92,24 @@
     private AudioManager mAudioManager;
     private BluetoothDevicePreference mPreference;
     private ShadowBluetoothAdapter mShadowBluetoothAdapter;
-    private AudioSharingFeatureProvider mFeatureProvider;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
         mContext = RuntimeEnvironment.application;
-        mFeatureProvider = FakeFeatureFactory.setupForTest().getAudioSharingFeatureProvider();
         mAudioManager = mContext.getSystemService(AudioManager.class);
         ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
         mLocalBtManager = Utils.getLocalBtManager(mContext);
+        when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager);
         when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
         mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
         mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
         mCachedDevices = new ArrayList<>();
         mCachedDevices.add(mCachedBluetoothDevice);
         when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
@@ -252,14 +268,16 @@
 
     @Test
     public void
-            onProfileConnectionStateChanged_leaDeviceConnected_notInCallNoSharing_addsPreference() {
+            onProfileConnectionStateChanged_leaConnected_notInCallSharingFlagOff_addsPreference() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         mAudioManager.setMode(AudioManager.MODE_NORMAL);
         when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
                 .thenReturn(true);
         when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
-        when(mFeatureProvider.isAudioSharingFilterMatched(
-                        any(CachedBluetoothDevice.class), any(LocalBluetoothManager.class)))
-                .thenReturn(false);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mBroadcastReceiveState));
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
 
         mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
                 mCachedBluetoothDevice,
@@ -271,14 +289,50 @@
 
     @Test
     public void
-            onProfileConnectionStateChanged_leaDeviceConnected_inCallNoSharing_addsPreference() {
+            onProfileConnectionStateChanged_leaConnected_notInCallNotInSharing_addsPreference() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mAudioManager.setMode(AudioManager.MODE_NORMAL);
+        when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
+                .thenReturn(true);
+        when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
+
+        mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+
+        verify(mBluetoothDeviceUpdater).addPreference(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaConnected_inCallSharingFlagOff_addsPreference() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         mAudioManager.setMode(AudioManager.MODE_IN_CALL);
         when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
                 .thenReturn(true);
         when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
-        when(mFeatureProvider.isAudioSharingFilterMatched(
-                        any(CachedBluetoothDevice.class), any(LocalBluetoothManager.class)))
-                .thenReturn(false);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mBroadcastReceiveState));
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
+
+        mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+
+        verify(mBluetoothDeviceUpdater).addPreference(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaConnected_inCallNotInSharing_addsPreference() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mAudioManager.setMode(AudioManager.MODE_IN_CALL);
+        when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
+                .thenReturn(true);
+        when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
 
         mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
                 mCachedBluetoothDevice,
@@ -291,14 +345,16 @@
     @Test
     public void
             onProfileConnectionStateChanged_leaDeviceConnected_notInCallInSharing_removesPref() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         mAudioManager.setMode(AudioManager.MODE_NORMAL);
         when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
                 .thenReturn(true);
         when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
         when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true);
-        when(mFeatureProvider.isAudioSharingFilterMatched(
-                        any(CachedBluetoothDevice.class), any(LocalBluetoothManager.class)))
-                .thenReturn(true);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mBroadcastReceiveState));
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
 
         mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
                 mCachedBluetoothDevice,
@@ -310,14 +366,16 @@
 
     @Test
     public void onProfileConnectionStateChanged_leaDeviceConnected_inCallInSharing_removesPref() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         mAudioManager.setMode(AudioManager.MODE_NORMAL);
         when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
                 .thenReturn(true);
         when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
         when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true);
-        when(mFeatureProvider.isAudioSharingFilterMatched(
-                        any(CachedBluetoothDevice.class), any(LocalBluetoothManager.class)))
-                .thenReturn(true);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mBroadcastReceiveState));
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
 
         mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
                 mCachedBluetoothDevice,
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java
index 8f07cca..211817a 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/AvailableMediaDeviceGroupControllerTest.java
@@ -22,18 +22,24 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
 import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
 import android.media.AudioManager;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.util.Pair;
 
 import androidx.appcompat.app.AlertDialog;
@@ -48,16 +54,21 @@
 import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
 import com.android.settings.bluetooth.BluetoothDevicePreference;
 import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.audiosharing.AudioSharingDialogHandler;
 import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
 import com.android.settings.testutils.shadow.ShadowAudioManager;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
 import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
 import com.android.settingslib.bluetooth.BluetoothCallback;
 import com.android.settingslib.bluetooth.BluetoothEventManager;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
 import com.android.settingslib.bluetooth.HearingAidInfo;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.flags.Flags;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -71,17 +82,22 @@
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.concurrent.Executor;
 
 /** Tests for {@link AvailableMediaDeviceGroupController}. */
 @RunWith(RobolectricTestRunner.class)
 @Config(
         shadows = {
             ShadowAudioManager.class,
+            ShadowBluetoothAdapter.class,
             ShadowBluetoothUtils.class,
             ShadowAlertDialogCompat.class,
         })
 public class AvailableMediaDeviceGroupControllerTest {
     @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1";
     private static final String PREFERENCE_KEY_1 = "pref_key_1";
@@ -96,17 +112,20 @@
     @Mock private PackageManager mPackageManager;
     @Mock private BluetoothEventManager mEventManager;
     @Mock private LocalBluetoothManager mLocalBluetoothManager;
+    @Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
     @Mock private CachedBluetoothDeviceManager mCachedDeviceManager;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
     @Mock private CachedBluetoothDevice mCachedBluetoothDevice;
     @Mock private BluetoothDevice mDevice;
-    @Mock
-    private Drawable mDrawable;
+    @Mock private Drawable mDrawable;
+    @Mock private AudioSharingDialogHandler mDialogHandler;
 
     private PreferenceGroup mPreferenceGroup;
     private Context mContext;
     private Preference mPreference;
     private AvailableMediaDeviceGroupController mAvailableMediaDeviceGroupController;
     private AudioManager mAudioManager;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
     private LifecycleOwner mLifecycleOwner;
     private Lifecycle mLifecycle;
 
@@ -123,19 +142,27 @@
         doReturn(mPackageManager).when(mContext).getPackageManager();
         doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
 
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
         ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
         mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
         mAudioManager = mContext.getSystemService(AudioManager.class);
         doReturn(mEventManager).when(mLocalBluetoothManager).getEventManager();
+        when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
         when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
         when(mCachedDeviceManager.findDevice(any(BluetoothDevice.class)))
                 .thenReturn(mCachedBluetoothDevice);
         when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS);
 
         mAvailableMediaDeviceGroupController =
-                spy(new AvailableMediaDeviceGroupController(mContext, null, mLifecycle));
+                spy(new AvailableMediaDeviceGroupController(mContext));
         mAvailableMediaDeviceGroupController.setBluetoothDeviceUpdater(
                 mAvailableMediaBluetoothDeviceUpdater);
+        mAvailableMediaDeviceGroupController.setDialogHandler(mDialogHandler);
         mAvailableMediaDeviceGroupController.setFragmentManager(
                 mActivity.getSupportFragmentManager());
         mAvailableMediaDeviceGroupController.mPreferenceGroup = mPreferenceGroup;
@@ -181,23 +208,58 @@
     }
 
     @Test
-    public void testRegister() {
+    public void testRegister_audioSharingOff() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         // register the callback in onStart()
         mAvailableMediaDeviceGroupController.onStart(mLifecycleOwner);
 
         verify(mAvailableMediaBluetoothDeviceUpdater).registerCallback();
-        verify(mLocalBluetoothManager.getEventManager())
-                .registerCallback(any(BluetoothCallback.class));
+        verify(mEventManager).registerCallback(any(BluetoothCallback.class));
         verify(mAvailableMediaBluetoothDeviceUpdater).refreshPreference();
+        verify(mAssistant, times(0))
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mDialogHandler, times(0)).registerCallbacks(any(Executor.class));
     }
 
     @Test
-    public void testUnregister() {
+    public void testRegister_audioSharingOn() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        setUpBroadcast();
+        // register the callback in onStart()
+        mAvailableMediaDeviceGroupController.onStart(mLifecycleOwner);
+        verify(mAvailableMediaBluetoothDeviceUpdater).registerCallback();
+        verify(mEventManager).registerCallback(any(BluetoothCallback.class));
+        verify(mAvailableMediaBluetoothDeviceUpdater).refreshPreference();
+        verify(mAssistant)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mDialogHandler).registerCallbacks(any(Executor.class));
+    }
+
+    @Test
+    public void testUnregister_audioSharingOff() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         // unregister the callback in onStop()
         mAvailableMediaDeviceGroupController.onStop(mLifecycleOwner);
         verify(mAvailableMediaBluetoothDeviceUpdater).unregisterCallback();
-        verify(mLocalBluetoothManager.getEventManager())
-                .unregisterCallback(any(BluetoothCallback.class));
+        verify(mEventManager).unregisterCallback(any(BluetoothCallback.class));
+        verify(mAssistant, times(0))
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mDialogHandler, times(0)).unregisterCallbacks();
+    }
+
+    @Test
+    public void testUnregister_audioSharingOn() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        setUpBroadcast();
+        // unregister the callback in onStop()
+        mAvailableMediaDeviceGroupController.onStop(mLifecycleOwner);
+        verify(mAvailableMediaBluetoothDeviceUpdater).unregisterCallback();
+        verify(mEventManager).unregisterCallback(any(BluetoothCallback.class));
+        verify(mAssistant)
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mDialogHandler).unregisterCallbacks();
     }
 
     @Test
@@ -267,7 +329,8 @@
     }
 
     @Test
-    public void onDeviceClick_setActive() {
+    public void onDeviceClick_audioSharingOff_setActive() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         when(mCachedBluetoothDevice.getDevice()).thenReturn(mDevice);
         Pair<Drawable, String> pair = new Pair<>(mDrawable, TEST_DEVICE_NAME);
         when(mCachedBluetoothDevice.getDrawableWithDescription()).thenReturn(pair);
@@ -280,4 +343,37 @@
         mAvailableMediaDeviceGroupController.onDeviceClick(preference);
         verify(mCachedBluetoothDevice).setActive();
     }
+
+    @Test
+    public void onDeviceClick_audioSharingOn_dialogHandler() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        setUpBroadcast();
+        when(mCachedBluetoothDevice.getDevice()).thenReturn(mDevice);
+        Pair<Drawable, String> pair = new Pair<>(mDrawable, TEST_DEVICE_NAME);
+        when(mCachedBluetoothDevice.getDrawableWithDescription()).thenReturn(pair);
+        BluetoothDevicePreference preference =
+                new BluetoothDevicePreference(
+                        mContext,
+                        mCachedBluetoothDevice,
+                        true,
+                        BluetoothDevicePreference.SortType.TYPE_NO_SORT);
+        mAvailableMediaDeviceGroupController.onDeviceClick(preference);
+        verify(mDialogHandler)
+                .handleDeviceConnected(mCachedBluetoothDevice, /* userTriggered= */ true);
+    }
+
+    private void setUpBroadcast() {
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        when(mLocalBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        doNothing()
+                .when(mAssistant)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        doNothing()
+                .when(mAssistant)
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragmentTest.java
index 0cd464c..33292af 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragmentTest.java
@@ -72,6 +72,9 @@
     private static final String KEY_DISCOVERABLE_FOOTER = "discoverable_footer";
     private static final String KEY_SAVED_DEVICE_SEE_ALL = "previously_connected_devices_see_all";
     private static final String KEY_FAST_PAIR_DEVICE_SEE_ALL = "fast_pair_devices_see_all";
+    private static final String KEY_AUDIO_SHARING_DEVICES = "audio_sharing_device_list";
+    private static final String KEY_AUDIO_SHARING_SETTINGS =
+            "connected_device_audio_sharing_settings";
     private static final String KEY_ADD_BT_DEVICES = "add_bt_devices";
     private static final String SETTINGS_PACKAGE_NAME = "com.android.settings";
     private static final String SYSTEMUI_PACKAGE_NAME = "com.android.systemui";
@@ -84,7 +87,6 @@
     private Context mContext;
     private ConnectedDeviceDashboardFragment mFragment;
     private FakeFeatureFactory mFeatureFactory;
-    private AvailableMediaDeviceGroupController mMediaDeviceGroupController;
 
     @Before
     public void setUp() {
@@ -93,21 +95,13 @@
         mContext = spy(RuntimeEnvironment.application);
         mFragment = new ConnectedDeviceDashboardFragment();
         mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION);
+        mSetFlagsRule.enableFlags(com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
         mFeatureFactory = FakeFeatureFactory.setupForTest();
         when(mFeatureFactory
                         .getFastPairFeatureProvider()
                         .getFastPairDeviceUpdater(
                                 any(Context.class), any(DevicePreferenceCallback.class)))
                 .thenReturn(mFastPairDeviceUpdater);
-        when(mFeatureFactory
-                        .getAudioSharingFeatureProvider()
-                        .createAudioSharingDevicePreferenceController(mContext, null, null))
-                .thenReturn(null);
-        mMediaDeviceGroupController = new AvailableMediaDeviceGroupController(mContext, null, null);
-        when(mFeatureFactory
-                        .getAudioSharingFeatureProvider()
-                        .createAvailableMediaDeviceGroupController(mContext, null, null))
-                .thenReturn(mMediaDeviceGroupController);
         doReturn(mPackageManager).when(mContext).getPackageManager();
         doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
     }
@@ -135,7 +129,9 @@
                         KEY_NEARBY_DEVICES,
                         KEY_DISCOVERABLE_FOOTER,
                         KEY_SAVED_DEVICE_SEE_ALL,
-                        KEY_FAST_PAIR_DEVICE_SEE_ALL);
+                        KEY_FAST_PAIR_DEVICE_SEE_ALL,
+                        KEY_AUDIO_SHARING_DEVICES,
+                        KEY_AUDIO_SHARING_SETTINGS);
     }
 
     @Test
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingActivityTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingActivityTest.java
new file mode 100644
index 0000000..0dddec9
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingActivityTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+import android.os.Bundle;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothAdapter.class})
+public class AudioSharingActivityTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private AudioSharingActivity mActivity;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+    @Before
+    public void setUp() {
+        mActivity = spy(Robolectric.buildActivity(AudioSharingActivity.class).get());
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+    }
+
+    @Test
+    public void isValidFragment_returnsTrue() {
+        assertThat(mActivity.isValidFragment(AudioSharingDashboardFragment.class.getName()))
+                .isTrue();
+    }
+
+    @Test
+    public void isValidFragment_returnsFalse() {
+        assertThat(mActivity.isValidFragment("")).isFalse();
+    }
+
+    @Test
+    public void onCreate_flagOff_finish() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mActivity.onCreate(new Bundle());
+        verify(mActivity).finish();
+    }
+
+    @Test
+    public void onCreate_flagOn_create() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mActivity.onCreate(new Bundle());
+        verify(mActivity, times(0)).finish();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBluetoothDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBluetoothDeviceUpdaterTest.java
new file mode 100644
index 0000000..f1c3126
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingBluetoothDeviceUpdaterTest.java
@@ -0,0 +1,270 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Looper;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.Pair;
+
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.bluetooth.BluetoothDevicePreference;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settings.testutils.shadow.ShadowThreadUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.flags.Flags;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+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;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+            ShadowThreadUtils.class
+        })
+public class AudioSharingBluetoothDeviceUpdaterTest {
+    private static final String MAC_ADDRESS = "04:52:C7:0B:D8:3C";
+    private static final String TEST_DEVICE_NAME = "test";
+    private static final String PREF_KEY = "audio_sharing_bt";
+    private static final String TAG = "AudioSharingBluetoothDeviceUpdater";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Mock private DevicePreferenceCallback mDevicePreferenceCallback;
+    @Mock private CachedBluetoothDevice mCachedBluetoothDevice;
+    @Mock private BluetoothDevice mBluetoothDevice;
+    @Mock private Drawable mDrawable;
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private CachedBluetoothDeviceManager mCachedDeviceManager;
+    @Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private BluetoothLeBroadcastReceiveState mState;
+
+    private Context mContext;
+    private AudioSharingBluetoothDeviceUpdater mDeviceUpdater;
+    private Collection<CachedBluetoothDevice> mCachedDevices;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+    @Before
+    public void setUp() {
+        mContext = ApplicationProvider.getApplicationContext();
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mLocalBtManager = Utils.getLocalBtManager(mContext);
+        when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
+        when(mLocalBtManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
+        when(mLocalBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(mState.getBisSyncState()).thenReturn(bisSyncState);
+        Pair<Drawable, String> pairs = new Pair<>(mDrawable, TEST_DEVICE_NAME);
+        doReturn(TEST_DEVICE_NAME).when(mCachedBluetoothDevice).getName();
+        doReturn(mBluetoothDevice).when(mCachedBluetoothDevice).getDevice();
+        doReturn(MAC_ADDRESS).when(mCachedBluetoothDevice).getAddress();
+        doReturn(pairs).when(mCachedBluetoothDevice).getDrawableWithDescription();
+        doReturn(ImmutableSet.of()).when(mCachedBluetoothDevice).getMemberDevice();
+        doReturn("").when(mCachedBluetoothDevice).getConnectionSummary();
+        mCachedDevices = new ArrayList<>();
+        mCachedDevices.add(mCachedBluetoothDevice);
+        when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
+        doNothing().when(mDevicePreferenceCallback).onDeviceAdded(any(Preference.class));
+        doNothing().when(mDevicePreferenceCallback).onDeviceRemoved(any(Preference.class));
+        mDeviceUpdater =
+                spy(
+                        new AudioSharingBluetoothDeviceUpdater(
+                                mContext, mDevicePreferenceCallback, /* metricsCategory= */ 0));
+        mDeviceUpdater.setPrefContext(mContext);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaDeviceConnected_flagOff_removesPref() {
+        setupPreferenceMapWithDevice();
+
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
+        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
+        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaDeviceConnected_noSource_removesPref() {
+        setupPreferenceMapWithDevice();
+
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(ImmutableList.of());
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
+        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
+        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_deviceIsNotInList_removesPref() {
+        setupPreferenceMapWithDevice();
+
+        mCachedDevices.clear();
+        when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
+        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
+        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaDeviceDisconnected_removesPref() {
+        setupPreferenceMapWithDevice();
+
+        when(mDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(false);
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_DISCONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
+        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
+        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaDeviceDisconnecting_removesPref() {
+        setupPreferenceMapWithDevice();
+        doReturn(false).when(mCachedBluetoothDevice).isConnectedLeAudioDevice();
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
+        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
+        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaDeviceConnected_hasSource_addsPreference() {
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+        setupPreferenceMapWithDevice();
+
+        verify(mDevicePreferenceCallback).onDeviceAdded(captor.capture());
+        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
+        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void getLogTag_returnsCorrectTag() {
+        assertThat(mDeviceUpdater.getLogTag()).isEqualTo(TAG);
+    }
+
+    @Test
+    public void getPreferenceKey_returnsCorrectKey() {
+        assertThat(mDeviceUpdater.getPreferenceKey()).isEqualTo(PREF_KEY);
+    }
+
+    private void setupPreferenceMapWithDevice() {
+        // Add device to preferenceMap
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(ImmutableList.of(mState));
+        when(mDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true);
+        doReturn(true).when(mCachedBluetoothDevice).isConnectedLeAudioDevice();
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceControllerTest.java
new file mode 100644
index 0000000..a395716
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCompatibilityPreferenceControllerTest.java
@@ -0,0 +1,271 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
+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.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
+import android.os.Looper;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.TwoStatePreference;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settings.testutils.shadow.ShadowThreadUtils;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.concurrent.Executor;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+            ShadowThreadUtils.class,
+        })
+public class AudioSharingCompatibilityPreferenceControllerTest {
+    private static final String PREF_KEY = "audio_sharing_stream_compatibility";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Spy Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private PreferenceScreen mScreen;
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private BluetoothEventManager mBtEventManager;
+    @Mock private LocalBluetoothProfileManager mBtProfileManager;
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private VolumeControlProfile mVolumeControl;
+    @Mock private TwoStatePreference mPreference;
+    private AudioSharingCompatibilityPreferenceController mController;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private LocalBluetoothManager mLocalBluetoothManager;
+    private Lifecycle mLifecycle;
+    private LifecycleOwner mLifecycleOwner;
+
+    @Before
+    public void setUp() {
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+        when(mLocalBluetoothManager.getEventManager()).thenReturn(mBtEventManager);
+        when(mLocalBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
+        when(mBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        when(mBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        when(mBtProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
+        when(mBroadcast.isProfileReady()).thenReturn(true);
+        when(mAssistant.isProfileReady()).thenReturn(true);
+        when(mVolumeControl.isProfileReady()).thenReturn(true);
+        mController = new AudioSharingCompatibilityPreferenceController(mContext, PREF_KEY);
+        when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference);
+    }
+
+    @Test
+    public void onStart_flagOn_registerCallback() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mBroadcast)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
+        verify(mBtProfileManager, times(0)).addServiceListener(mController);
+    }
+
+    @Test
+    public void onStart_flagOnProfileNotReady_registerProfileCallback() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isProfileReady()).thenReturn(false);
+        mController.onStart(mLifecycleOwner);
+        verify(mBroadcast, times(0))
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
+        verify(mBtProfileManager).addServiceListener(mController);
+    }
+
+    @Test
+    public void onStart_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mBroadcast, times(0))
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
+    }
+
+    @Test
+    public void onStop_flagOn_unregisterCallback() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(true);
+        mController.onStop(mLifecycleOwner);
+        verify(mBroadcast).unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
+        verify(mBtProfileManager).removeServiceListener(mController);
+    }
+
+    @Test
+    public void onStop_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(true);
+        mController.onStop(mLifecycleOwner);
+        verify(mBroadcast, times(0))
+                .unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
+        verify(mBtProfileManager, times(0)).removeServiceListener(mController);
+    }
+
+    @Test
+    public void onServiceConnected_updateSwitch() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isEnabled(null)).thenReturn(false);
+        when(mBroadcast.isProfileReady()).thenReturn(false);
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        verify(mPreference).setEnabled(true);
+
+        when(mBroadcast.isEnabled(null)).thenReturn(true);
+        when(mBroadcast.isProfileReady()).thenReturn(true);
+        mController.onServiceConnected();
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mBroadcast)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
+        verify(mBtProfileManager).removeServiceListener(mController);
+        verify(mPreference).setEnabled(false);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOn() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOff() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+    }
+
+    @Test
+    public void getPreferenceKey_returnsCorrectKey() {
+        assertThat(mController.getPreferenceKey()).isEqualTo(PREF_KEY);
+    }
+
+    @Test
+    public void getSliceHighlightMenuRes_returnsZero() {
+        assertThat(mController.getSliceHighlightMenuRes()).isEqualTo(0);
+    }
+
+    @Test
+    public void displayPreference_broadcastOn_Disabled() {
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        verify(mPreference).setEnabled(false);
+        verify(mPreference)
+                .setSummary(
+                        eq(mContext.getString(
+                                R.string
+                                        .audio_sharing_stream_compatibility_disabled_description)));
+    }
+
+    @Test
+    public void displayPreference_broadcastOff_Enabled() {
+        when(mBroadcast.isEnabled(any())).thenReturn(false);
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        verify(mPreference).setEnabled(true);
+        verify(mPreference)
+                .setSummary(
+                        eq(mContext.getString(
+                                R.string.audio_sharing_stream_compatibility_description)));
+    }
+
+    @Test
+    public void isChecked_returnsTrue() {
+        when(mBroadcast.getImproveCompatibility()).thenReturn(true);
+        assertThat(mController.isChecked()).isTrue();
+    }
+
+    @Test
+    public void isChecked_returnsFalse() {
+        when(mBroadcast.getImproveCompatibility()).thenReturn(false);
+        assertThat(mController.isChecked()).isFalse();
+        mBroadcast = null;
+        assertThat(mController.isChecked()).isFalse();
+    }
+
+    @Test
+    public void setCheckedToNewValue_returnsTrue() {
+        when(mBroadcast.getImproveCompatibility()).thenReturn(true);
+        doNothing().when(mBroadcast).setImproveCompatibility(anyBoolean());
+        boolean setChecked = mController.setChecked(false);
+        verify(mBroadcast).setImproveCompatibility(false);
+        assertThat(setChecked).isTrue();
+    }
+
+    @Test
+    public void setCheckedToCurrentValue_returnsFalse() {
+        when(mBroadcast.getImproveCompatibility()).thenReturn(true);
+        boolean setChecked = mController.setChecked(true);
+        verify(mBroadcast, times(0)).setImproveCompatibility(anyBoolean());
+        assertThat(setChecked).isFalse();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java
new file mode 100644
index 0000000..c1afeaa
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.settings.SettingsEnums;
+
+import com.android.settings.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioSharingDashboardFragmentTest {
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private AudioSharingDashboardFragment mFragment;
+
+    @Before
+    public void setUp() {
+        mFragment = new AudioSharingDashboardFragment();
+    }
+
+    @Test
+    public void getPreferenceScreenResId_returnsCorrectXml() {
+        assertThat(mFragment.getPreferenceScreenResId())
+                .isEqualTo(R.xml.bluetooth_le_audio_sharing);
+    }
+
+    @Test
+    public void getLogTag_returnsCorrectTag() {
+        assertThat(mFragment.getLogTag()).isEqualTo("AudioSharingDashboardFrag");
+    }
+
+    @Test
+    public void getMetricsCategory_returnsCorrectCategory() {
+        assertThat(mFragment.getMetricsCategory()).isEqualTo(SettingsEnums.AUDIO_SHARING_SETTINGS);
+    }
+
+    @Test
+    public void getHelpResource_returnsCorrectResource() {
+        assertThat(mFragment.getHelpResource())
+                .isEqualTo(R.string.help_url_audio_sharing);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItemTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItemTest.java
new file mode 100644
index 0000000..1bae3d1
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceItemTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioSharingDeviceItemTest {
+    private static final String TEST_NAME = "test";
+    private static final int TEST_GROUP_ID = 1;
+    private static final boolean TEST_IS_ACTIVE = true;
+
+    @Test
+    public void createItem_new() {
+        AudioSharingDeviceItem item =
+                new AudioSharingDeviceItem(TEST_NAME, TEST_GROUP_ID, TEST_IS_ACTIVE);
+        assertThat(item.getName()).isEqualTo(TEST_NAME);
+        assertThat(item.getGroupId()).isEqualTo(TEST_GROUP_ID);
+        assertThat(item.isActive()).isEqualTo(TEST_IS_ACTIVE);
+    }
+
+    @Test
+    public void createItem_withParcel() {
+        AudioSharingDeviceItem item =
+                new AudioSharingDeviceItem(TEST_NAME, TEST_GROUP_ID, TEST_IS_ACTIVE);
+        Parcel parcel = Parcel.obtain();
+        item.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        AudioSharingDeviceItem newItem = new AudioSharingDeviceItem(parcel);
+        assertThat(newItem.getName()).isEqualTo(TEST_NAME);
+        assertThat(newItem.getGroupId()).isEqualTo(TEST_GROUP_ID);
+        assertThat(newItem.isActive()).isEqualTo(TEST_IS_ACTIVE);
+    }
+
+    @Test
+    public void describeContents_returnsZero() {
+        AudioSharingDeviceItem item =
+                new AudioSharingDeviceItem(TEST_NAME, TEST_GROUP_ID, TEST_IS_ACTIVE);
+        assertThat(item.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void creator_newArray() {
+        assertThat(AudioSharingDeviceItem.CREATOR.newArray(2)).hasLength(2);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceControllerTest.java
new file mode 100644
index 0000000..14bca08
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDevicePreferenceControllerTest.java
@@ -0,0 +1,487 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Looper;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.SettingsActivity;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settings.testutils.shadow.ShadowFragment;
+import com.android.settingslib.bluetooth.A2dpProfile;
+import com.android.settingslib.bluetooth.BluetoothCallback;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.HeadsetProfile;
+import com.android.settingslib.bluetooth.LeAudioProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.flags.Flags;
+
+import com.google.common.collect.ImmutableList;
+
+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 org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.concurrent.Executor;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+            ShadowFragment.class,
+        })
+public class AudioSharingDevicePreferenceControllerTest {
+    private static final String KEY = "audio_sharing_device_list";
+    private static final String KEY_AUDIO_SHARING_SETTINGS =
+            "connected_device_audio_sharing_settings";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Mock private AudioSharingBluetoothDeviceUpdater mBluetoothDeviceUpdater;
+    @Mock private PreferenceManager mPreferenceManager;
+    @Mock private CachedBluetoothDevice mCachedDevice;
+    @Mock private BluetoothDevice mDevice;
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private BluetoothEventManager mEventManager;
+    @Mock private LocalBluetoothProfileManager mProfileManager;
+    @Mock private CachedBluetoothDeviceManager mDeviceManager;
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private VolumeControlProfile mVolumeControl;
+    @Mock private PreferenceScreen mScreen;
+    @Mock private AudioSharingDialogHandler mDialogHandler;
+    @Mock private DashboardFragment mFragment;
+    @Mock private FragmentActivity mActivity;
+    @Mock private LeAudioProfile mLeAudioProfile;
+    @Mock private A2dpProfile mA2dpProfile;
+    @Mock private HeadsetProfile mHeadsetProfile;
+
+    private Context mContext;
+    private AudioSharingDevicePreferenceController mController;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private Lifecycle mLifecycle;
+    private LifecycleOwner mLifecycleOwner;
+    private PreferenceCategory mPreferenceGroup;
+    private Preference mAudioSharingPreference;
+
+    @Before
+    public void setUp() {
+        mContext = ApplicationProvider.getApplicationContext();
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mLocalBtManager = Utils.getLocalBtManager(mContext);
+        when(mLocalBtManager.getEventManager()).thenReturn(mEventManager);
+        when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager);
+        when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
+        when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        when(mProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
+        when(mBroadcast.isProfileReady()).thenReturn(true);
+        when(mAssistant.isProfileReady()).thenReturn(true);
+        when(mVolumeControl.isProfileReady()).thenReturn(true);
+        when(mDevice.getAnonymizedAddress()).thenReturn("");
+        doReturn(mDevice).when(mCachedDevice).getDevice();
+        when(mDeviceManager.findDevice(mDevice)).thenReturn(mCachedDevice);
+        when(mHeadsetProfile.getProfileId()).thenReturn(BluetoothProfile.HEADSET);
+        when(mA2dpProfile.getProfileId()).thenReturn(BluetoothProfile.A2DP);
+        when(mLeAudioProfile.getProfileId()).thenReturn(BluetoothProfile.LE_AUDIO);
+        when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true);
+        when(mScreen.getContext()).thenReturn(mContext);
+        mPreferenceGroup = spy(new PreferenceCategory(mContext));
+        doReturn(mPreferenceManager).when(mPreferenceGroup).getPreferenceManager();
+        mAudioSharingPreference = new Preference(mContext);
+        mPreferenceGroup.addPreference(mAudioSharingPreference);
+        when(mPreferenceGroup.findPreference(KEY_AUDIO_SHARING_SETTINGS))
+                .thenReturn(mAudioSharingPreference);
+        when(mScreen.findPreference(KEY)).thenReturn(mPreferenceGroup);
+        mController = new AudioSharingDevicePreferenceController(mContext);
+        mController.setBluetoothDeviceUpdater(mBluetoothDeviceUpdater);
+        mController.setDialogHandler(mDialogHandler);
+        doReturn(mActivity).when(mFragment).getActivity();
+        mController.setHostFragment(mFragment);
+    }
+
+    @Test
+    public void onStart_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mEventManager, times(0)).registerCallback(any(BluetoothCallback.class));
+        verify(mDialogHandler, times(0)).registerCallbacks(any(Executor.class));
+        verify(mAssistant, times(0))
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mBluetoothDeviceUpdater, times(0)).registerCallback();
+        verify(mBluetoothDeviceUpdater, times(0)).refreshPreference();
+    }
+
+    @Test
+    public void onStart_flagOn_registerCallbacks() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mEventManager).registerCallback(any(BluetoothCallback.class));
+        verify(mDialogHandler).registerCallbacks(any(Executor.class));
+        verify(mAssistant)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mBluetoothDeviceUpdater).registerCallback();
+        verify(mBluetoothDeviceUpdater).refreshPreference();
+    }
+
+    @Test
+    public void onStop_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStop(mLifecycleOwner);
+        verify(mEventManager, times(0)).unregisterCallback(any(BluetoothCallback.class));
+        verify(mDialogHandler, times(0)).unregisterCallbacks();
+        verify(mAssistant, times(0))
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mBluetoothDeviceUpdater, times(0)).unregisterCallback();
+    }
+
+    @Test
+    public void onStop_flagOn_unregisterCallbacks() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStop(mLifecycleOwner);
+        verify(mEventManager).unregisterCallback(any(BluetoothCallback.class));
+        verify(mDialogHandler).unregisterCallbacks();
+        verify(mAssistant)
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mBluetoothDeviceUpdater).unregisterCallback();
+    }
+
+    @Test
+    public void displayPreference_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.displayPreference(mScreen);
+        assertThat(mPreferenceGroup.isVisible()).isFalse();
+        assertThat(mAudioSharingPreference.isVisible()).isFalse();
+        verify(mBluetoothDeviceUpdater, times(0)).forceUpdate();
+    }
+
+    @Test
+    public void displayPreference_flagOn_updateDeviceList() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.displayPreference(mScreen);
+        assertThat(mPreferenceGroup.isVisible()).isFalse();
+        assertThat(mAudioSharingPreference.isVisible()).isFalse();
+        verify(mBluetoothDeviceUpdater).setPrefContext(mContext);
+        verify(mBluetoothDeviceUpdater).forceUpdate();
+    }
+
+    @Test
+    public void getPreferenceKey_returnsCorrectKey() {
+        assertThat(mController.getPreferenceKey()).isEqualTo(KEY);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOff_returnUnSupported() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOn_returnSupported() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE_UNSEARCHABLE);
+    }
+
+    @Test
+    public void onDeviceAdded_firstDevice_updateVisibility() {
+        mController.displayPreference(mScreen);
+        Preference preference = new Preference(mContext);
+        mController.onDeviceAdded(preference);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertThat(mPreferenceGroup.isVisible()).isTrue();
+        assertThat(mAudioSharingPreference.isVisible()).isTrue();
+        assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void onDeviceRemoved_lastDevice_updateVisibility() {
+        Preference preference = new Preference(mContext);
+        mPreferenceGroup.addPreference(preference);
+        mController.displayPreference(mScreen);
+        mController.onDeviceRemoved(preference);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertThat(mPreferenceGroup.isVisible()).isFalse();
+        assertThat(mAudioSharingPreference.isVisible()).isFalse();
+        assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_notMediaDevice_doNothing() {
+        doReturn(ImmutableList.of()).when(mCachedDevice).getConnectableProfiles();
+        mController.onProfileConnectionStateChanged(
+                mCachedDevice, BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.HID_DEVICE);
+        verifyNoInteractions(mDialogHandler);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaDeviceDisconnected_closeOpeningDialogsForIt() {
+        // Test when LEA device LE_AUDIO_BROADCAST_ASSISTANT disconnected.
+        when(mDevice.isConnected()).thenReturn(true);
+        doReturn(ImmutableList.of(mLeAudioProfile)).when(mCachedDevice).getConnectableProfiles();
+        doReturn(ImmutableList.of(mLeAudioProfile)).when(mCachedDevice).getProfiles();
+        mController.onProfileConnectionStateChanged(
+                mCachedDevice,
+                BluetoothAdapter.STATE_DISCONNECTED,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+        verify(mDialogHandler).closeOpeningDialogsForLeaDevice(mCachedDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_assistantProfileConnecting_doNothing() {
+        // Test when LEA device LE_AUDIO_BROADCAST_ASSISTANT connecting
+        doReturn(ImmutableList.of(mLeAudioProfile)).when(mCachedDevice).getConnectableProfiles();
+        doReturn(ImmutableList.of(mLeAudioProfile)).when(mCachedDevice).getProfiles();
+        mController.onProfileConnectionStateChanged(
+                mCachedDevice,
+                BluetoothAdapter.STATE_CONNECTING,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+        verifyNoInteractions(mDialogHandler);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_otherProfileConnected_doNothing() {
+        // Test when LEA device other profile connected
+        when(mDevice.isConnected()).thenReturn(true);
+        doReturn(ImmutableList.of(mLeAudioProfile)).when(mCachedDevice).getConnectableProfiles();
+        doReturn(ImmutableList.of(mLeAudioProfile)).when(mCachedDevice).getProfiles();
+        mController.onProfileConnectionStateChanged(
+                mCachedDevice, BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP);
+        verifyNoInteractions(mDialogHandler);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_otherProfileConnecting_doNothing() {
+        // Test when LEA device other profile connecting
+        when(mDevice.isConnected()).thenReturn(true);
+        doReturn(ImmutableList.of(mLeAudioProfile)).when(mCachedDevice).getConnectableProfiles();
+        doReturn(ImmutableList.of(mLeAudioProfile)).when(mCachedDevice).getProfiles();
+        mController.onProfileConnectionStateChanged(
+                mCachedDevice, BluetoothAdapter.STATE_CONNECTING, BluetoothProfile.A2DP);
+        verifyNoInteractions(mDialogHandler);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_assistantProfileConnected_handle() {
+        // Test when LEA device LE_AUDIO_BROADCAST_ASSISTANT connected
+        when(mDevice.isConnected()).thenReturn(true);
+        doReturn(ImmutableList.of(mLeAudioProfile)).when(mCachedDevice).getConnectableProfiles();
+        doReturn(ImmutableList.of(mLeAudioProfile)).when(mCachedDevice).getProfiles();
+        mController.onProfileConnectionStateChanged(
+                mCachedDevice,
+                BluetoothAdapter.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+        verify(mDialogHandler).handleDeviceConnected(mCachedDevice, false);
+    }
+
+    @Test
+    public void
+            onProfileConnectionStateChanged_nonLeaDeviceDisconnected_closeOpeningDialogsForIt() {
+        // Test when non-LEA device totally disconnected
+        when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(false);
+        doReturn(ImmutableList.of(mA2dpProfile)).when(mCachedDevice).getConnectableProfiles();
+        doReturn(ImmutableList.of(mLeAudioProfile, mA2dpProfile)).when(mCachedDevice).getProfiles();
+        when(mCachedDevice.isConnected()).thenReturn(false);
+        mController.onProfileConnectionStateChanged(
+                mCachedDevice, BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.A2DP);
+        verify(mDialogHandler).closeOpeningDialogsForNonLeaDevice(mCachedDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_nonLeaNotFirstProfileConnected_doNothing() {
+        // Test when non-LEA device LE_AUDIO_BROADCAST_ASSISTANT connecting
+        when(mDevice.isConnected()).thenReturn(true);
+        when(mHeadsetProfile.getConnectionStatus(mDevice))
+                .thenReturn(BluetoothAdapter.STATE_CONNECTED);
+        doReturn(ImmutableList.of(mA2dpProfile, mHeadsetProfile))
+                .when(mCachedDevice)
+                .getConnectableProfiles();
+        doReturn(ImmutableList.of(mA2dpProfile, mHeadsetProfile)).when(mCachedDevice).getProfiles();
+        mController.onProfileConnectionStateChanged(
+                mCachedDevice, BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP);
+        verifyNoInteractions(mDialogHandler);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_nonLeaFirstProfileConnected_handle() {
+        // Test when non-LEA device LE_AUDIO_BROADCAST_ASSISTANT connecting
+        when(mDevice.isConnected()).thenReturn(true);
+        when(mHeadsetProfile.getConnectionStatus(mDevice))
+                .thenReturn(BluetoothAdapter.STATE_DISCONNECTED);
+        doReturn(ImmutableList.of(mA2dpProfile, mHeadsetProfile))
+                .when(mCachedDevice)
+                .getConnectableProfiles();
+        doReturn(ImmutableList.of(mA2dpProfile, mHeadsetProfile)).when(mCachedDevice).getProfiles();
+        mController.onProfileConnectionStateChanged(
+                mCachedDevice, BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP);
+        verify(mDialogHandler).handleDeviceConnected(mCachedDevice, false);
+    }
+
+    @Test
+    public void handleDeviceClickFromIntent_noDevice_doNothing() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        Intent intent = new Intent();
+        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, new Bundle());
+        doReturn(intent).when(mActivity).getIntent();
+        mController.displayPreference(mScreen);
+
+        verify(mDeviceManager, times(0)).findDevice(any(BluetoothDevice.class));
+        verify(mDialogHandler, times(0))
+                .handleDeviceConnected(any(CachedBluetoothDevice.class), anyBoolean());
+    }
+
+    @Test
+    public void handleDeviceClickFromIntent_profileNotReady_doNothing() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isProfileReady()).thenReturn(false);
+        Bundle arg = new Bundle();
+        arg.putParcelable(EXTRA_BLUETOOTH_DEVICE, mDevice);
+        Intent intent = new Intent();
+        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, arg);
+        doReturn(intent).when(mActivity).getIntent();
+        when(mDevice.isConnected()).thenReturn(false);
+        mController.displayPreference(mScreen);
+
+        verify(mDeviceManager, times(0)).findDevice(any(BluetoothDevice.class));
+        verify(mDialogHandler, times(0))
+                .handleDeviceConnected(any(CachedBluetoothDevice.class), anyBoolean());
+    }
+
+    @Test
+    public void handleDeviceClickFromIntent_intentHandled_handle() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        Bundle arg = new Bundle();
+        arg.putParcelable(EXTRA_BLUETOOTH_DEVICE, mDevice);
+        Intent intent = new Intent();
+        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, arg);
+        doReturn(intent).when(mActivity).getIntent();
+        when(mDevice.isConnected()).thenReturn(true);
+        when(mCachedDevice.isConnected()).thenReturn(true);
+        mController.setIntentHandled(true);
+        mController.displayPreference(mScreen);
+
+        verify(mDeviceManager, times(0)).findDevice(any(BluetoothDevice.class));
+        verify(mDialogHandler, times(0))
+                .handleDeviceConnected(any(CachedBluetoothDevice.class), anyBoolean());
+    }
+
+    @Test
+    public void handleDeviceClickFromIntent_disconnectedDevice_connect() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        Bundle arg = new Bundle();
+        arg.putParcelable(EXTRA_BLUETOOTH_DEVICE, mDevice);
+        Intent intent = new Intent();
+        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, arg);
+        doReturn(intent).when(mActivity).getIntent();
+        when(mDevice.isConnected()).thenReturn(false);
+        mController.displayPreference(mScreen);
+
+        verify(mCachedDevice).connect();
+    }
+
+    @Test
+    public void handleDeviceClickFromIntent_connectedDevice_handle() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        Bundle arg = new Bundle();
+        arg.putParcelable(EXTRA_BLUETOOTH_DEVICE, mDevice);
+        Intent intent = new Intent();
+        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, arg);
+        doReturn(intent).when(mActivity).getIntent();
+        when(mDevice.isConnected()).thenReturn(true);
+        when(mCachedDevice.isConnected()).thenReturn(true);
+        mController.displayPreference(mScreen);
+
+        verify(mDialogHandler).handleDeviceConnected(mCachedDevice, true);
+    }
+
+    @Test
+    public void handleDeviceClickFromIntent_onServiceConnected_handle() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        Bundle arg = new Bundle();
+        arg.putParcelable(EXTRA_BLUETOOTH_DEVICE, mDevice);
+        Intent intent = new Intent();
+        intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, arg);
+        doReturn(intent).when(mActivity).getIntent();
+        when(mDevice.isConnected()).thenReturn(true);
+        when(mCachedDevice.isConnected()).thenReturn(true);
+        mController.onServiceConnected();
+
+        verify(mDialogHandler).handleDeviceConnected(mCachedDevice, true);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeControlUpdaterTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeControlUpdaterTest.java
new file mode 100644
index 0000000..a8563d1
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeControlUpdaterTest.java
@@ -0,0 +1,299 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Looper;
+import android.provider.Settings;
+import android.widget.SeekBar;
+
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+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;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothUtils.class})
+public class AudioSharingDeviceVolumeControlUpdaterTest {
+    private static final String TEST_DEVICE_NAME = "test";
+    private static final String TAG = "AudioSharingDeviceVolumeControlUpdater";
+    private static final String PREF_KEY = "audio_sharing_volume_control";
+    private static final String TEST_SETTINGS_KEY =
+            "bluetooth_le_broadcast_fallback_active_group_id";
+    private static final int TEST_DEVICE_GROUP_ID = 1;
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock private DevicePreferenceCallback mDevicePreferenceCallback;
+    @Mock private CachedBluetoothDevice mCachedBluetoothDevice;
+    @Mock private BluetoothDevice mBluetoothDevice;
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private CachedBluetoothDeviceManager mCachedDeviceManager;
+    @Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private VolumeControlProfile mVolumeControl;
+    @Mock private BluetoothLeBroadcastReceiveState mState;
+    @Mock private AudioManager mAudioManager;
+
+    private Context mContext;
+    private AudioSharingDeviceVolumeControlUpdater mDeviceUpdater;
+    private Collection<CachedBluetoothDevice> mCachedDevices;
+
+    @Before
+    public void setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mLocalBtManager = Utils.getLocalBtManager(mContext);
+        when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
+        when(mLocalBtManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
+        when(mLocalBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        when(mLocalBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        when(mLocalBtProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(mState.getBisSyncState()).thenReturn(bisSyncState);
+        doReturn(TEST_DEVICE_NAME).when(mCachedBluetoothDevice).getName();
+        doReturn(mBluetoothDevice).when(mCachedBluetoothDevice).getDevice();
+        doReturn(ImmutableSet.of()).when(mCachedBluetoothDevice).getMemberDevice();
+        doReturn(TEST_DEVICE_GROUP_ID).when(mCachedBluetoothDevice).getGroupId();
+        mCachedDevices = new ArrayList<>();
+        mCachedDevices.add(mCachedBluetoothDevice);
+        when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
+        doNothing().when(mDevicePreferenceCallback).onDeviceAdded(any(Preference.class));
+        doNothing().when(mDevicePreferenceCallback).onDeviceRemoved(any(Preference.class));
+        when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager);
+        mDeviceUpdater =
+                spy(
+                        new AudioSharingDeviceVolumeControlUpdater(
+                                mContext, mDevicePreferenceCallback, /* metricsCategory= */ 0));
+        mDeviceUpdater.setPrefContext(mContext);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaDeviceConnected_noSharing_removesPref() {
+        setupPreferenceMapWithDevice();
+
+        when(mBroadcast.isEnabled(null)).thenReturn(false);
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
+        assertThat(captor.getValue() instanceof AudioSharingDeviceVolumePreference).isTrue();
+        assertThat(((AudioSharingDeviceVolumePreference) captor.getValue()).getCachedDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaDeviceConnected_noSource_removesPref() {
+        setupPreferenceMapWithDevice();
+
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(ImmutableList.of());
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
+        assertThat(captor.getValue() instanceof AudioSharingDeviceVolumePreference).isTrue();
+        assertThat(((AudioSharingDeviceVolumePreference) captor.getValue()).getCachedDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_deviceIsNotInList_removesPref() {
+        setupPreferenceMapWithDevice();
+
+        mCachedDevices.clear();
+        when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
+        assertThat(captor.getValue() instanceof AudioSharingDeviceVolumePreference).isTrue();
+        assertThat(((AudioSharingDeviceVolumePreference) captor.getValue()).getCachedDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaDeviceDisconnected_removesPref() {
+        setupPreferenceMapWithDevice();
+
+        when(mDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(false);
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_DISCONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
+        assertThat(captor.getValue() instanceof AudioSharingDeviceVolumePreference).isTrue();
+        assertThat(((AudioSharingDeviceVolumePreference) captor.getValue()).getCachedDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaDeviceDisconnecting_removesPref() {
+        setupPreferenceMapWithDevice();
+
+        when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(false);
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
+        assertThat(captor.getValue() instanceof AudioSharingDeviceVolumePreference).isTrue();
+        assertThat(((AudioSharingDeviceVolumePreference) captor.getValue()).getCachedDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leaDeviceConnected_hasSource_addsPreference() {
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+        setupPreferenceMapWithDevice();
+
+        verify(mDevicePreferenceCallback).onDeviceAdded(captor.capture());
+        assertThat(captor.getValue() instanceof AudioSharingDeviceVolumePreference).isTrue();
+        assertThat(((AudioSharingDeviceVolumePreference) captor.getValue()).getCachedDevice())
+                .isEqualTo(mCachedBluetoothDevice);
+    }
+
+    @Test
+    public void addPreference_notFallbackDevice_setDeviceVolume() {
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+        setupPreferenceMapWithDevice();
+
+        verify(mDevicePreferenceCallback).onDeviceAdded(captor.capture());
+        assertThat(captor.getValue() instanceof AudioSharingDeviceVolumePreference).isTrue();
+        AudioSharingDeviceVolumePreference preference =
+                (AudioSharingDeviceVolumePreference) captor.getValue();
+
+        SeekBar seekBar = mock(SeekBar.class);
+        when(seekBar.getProgress()).thenReturn(255);
+        preference.onStopTrackingTouch(seekBar);
+
+        verify(mVolumeControl).setDeviceVolume(mBluetoothDevice, 255, true);
+        verifyNoInteractions(mAudioManager);
+    }
+
+    @Test
+    public void addPreference_fallbackDevice_setStreamVolume() {
+        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+        setupPreferenceMapWithDevice();
+
+        verify(mDevicePreferenceCallback).onDeviceAdded(captor.capture());
+        assertThat(captor.getValue() instanceof AudioSharingDeviceVolumePreference).isTrue();
+        AudioSharingDeviceVolumePreference preference =
+                (AudioSharingDeviceVolumePreference) captor.getValue();
+
+        Settings.Secure.putInt(
+                mContext.getContentResolver(), TEST_SETTINGS_KEY, TEST_DEVICE_GROUP_ID);
+        when(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)).thenReturn(10);
+        when(mAudioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)).thenReturn(0);
+        SeekBar seekBar = mock(SeekBar.class);
+        when(seekBar.getProgress()).thenReturn(255);
+        preference.onStopTrackingTouch(seekBar);
+
+        verifyNoInteractions(mVolumeControl);
+        verify(mAudioManager).setStreamVolume(AudioManager.STREAM_MUSIC, 10, 0);
+    }
+
+    @Test
+    public void getLogTag_returnsCorrectTag() {
+        assertThat(mDeviceUpdater.getLogTag()).isEqualTo(TAG);
+    }
+
+    @Test
+    public void getPreferenceKey_returnsCorrectKey() {
+        assertThat(mDeviceUpdater.getPreferenceKey()).isEqualTo(PREF_KEY);
+    }
+
+    private void setupPreferenceMapWithDevice() {
+        // Add device to preferenceMap
+        when(mBroadcast.isEnabled(null)).thenReturn(true);
+        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(ImmutableList.of(mState));
+        when(mDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true);
+        when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
+        mDeviceUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO);
+        shadowOf(Looper.getMainLooper()).idle();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeGroupControllerTest.java
new file mode 100644
index 0000000..7c8709c
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumeGroupControllerTest.java
@@ -0,0 +1,414 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID;
+
+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.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothStatusCodes;
+import android.bluetooth.BluetoothVolumeControl;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.media.AudioManager;
+import android.os.Looper;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settings.testutils.shadow.ShadowThreadUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.flags.Flags;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.concurrent.Executor;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+            ShadowThreadUtils.class,
+        })
+public class AudioSharingDeviceVolumeGroupControllerTest {
+    private static final String TEST_DEVICE_NAME1 = "test1";
+    private static final String TEST_DEVICE_NAME2 = "test2";
+    private static final int TEST_DEVICE_GROUP_ID1 = 1;
+    private static final int TEST_DEVICE_GROUP_ID2 = 2;
+    private static final int TEST_VOLUME_VALUE = 10;
+    private static final int TEST_INVALID_VOLUME_VALUE = -1;
+    private static final int TEST_MAX_VOLUME_VALUE = 100;
+    private static final int TEST_MIN_VOLUME_VALUE = 0;
+    private static final String PREF_KEY = "audio_sharing_device_volume_group";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Mock private DevicePreferenceCallback mDevicePreferenceCallback;
+    @Mock private CachedBluetoothDevice mCachedDevice1;
+    @Mock private CachedBluetoothDevice mCachedDevice2;
+    @Mock private BluetoothDevice mDevice1;
+    @Mock private BluetoothDevice mDevice2;
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private CachedBluetoothDeviceManager mCachedDeviceManager;
+    @Mock private LocalBluetoothProfileManager mProfileManager;
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private AudioSharingDeviceVolumeControlUpdater mDeviceUpdater;
+    @Mock private VolumeControlProfile mVolumeControl;
+    @Mock private PreferenceScreen mScreen;
+    @Mock private AudioSharingDeviceVolumePreference mPreference1;
+    @Mock private AudioSharingDeviceVolumePreference mPreference2;
+    @Mock private AudioManager mAudioManager;
+    @Mock private PreferenceManager mPreferenceManager;
+    @Mock private ContentResolver mContentResolver;
+    @Spy private ContentObserver mContentObserver;
+
+    private Context mContext;
+    private AudioSharingDeviceVolumeGroupController mController;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private Lifecycle mLifecycle;
+    private LifecycleOwner mLifecycleOwner;
+    private PreferenceCategory mPreferenceGroup;
+
+    @Before
+    public void setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mLocalBtManager = Utils.getLocalBtManager(mContext);
+        when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
+        when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager);
+        when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        when(mProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
+        when(mBroadcast.isProfileReady()).thenReturn(true);
+        when(mAssistant.isProfileReady()).thenReturn(true);
+        when(mVolumeControl.isProfileReady()).thenReturn(true);
+        when(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC))
+                .thenReturn(TEST_MAX_VOLUME_VALUE);
+        when(mAudioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC))
+                .thenReturn(TEST_MIN_VOLUME_VALUE);
+        when(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC))
+                .thenReturn(TEST_VOLUME_VALUE);
+        when(mContext.getSystemService(AudioManager.class)).thenReturn(mAudioManager);
+        when(mContext.getContentResolver()).thenReturn(mContentResolver);
+        doReturn(TEST_DEVICE_NAME1).when(mCachedDevice1).getName();
+        doReturn(TEST_DEVICE_GROUP_ID1).when(mCachedDevice1).getGroupId();
+        doReturn(mDevice1).when(mCachedDevice1).getDevice();
+        doReturn(ImmutableSet.of()).when(mCachedDevice1).getMemberDevice();
+        when(mPreference1.getCachedDevice()).thenReturn(mCachedDevice1);
+        doReturn(TEST_DEVICE_NAME2).when(mCachedDevice2).getName();
+        doReturn(TEST_DEVICE_GROUP_ID2).when(mCachedDevice2).getGroupId();
+        doReturn(mDevice2).when(mCachedDevice2).getDevice();
+        doReturn(ImmutableSet.of()).when(mCachedDevice2).getMemberDevice();
+        when(mPreference2.getCachedDevice()).thenReturn(mCachedDevice2);
+        doNothing().when(mDevicePreferenceCallback).onDeviceAdded(any(Preference.class));
+        doNothing().when(mDevicePreferenceCallback).onDeviceRemoved(any(Preference.class));
+        when(mScreen.getContext()).thenReturn(mContext);
+        mPreferenceGroup = spy(new PreferenceCategory(mContext));
+        doReturn(mPreferenceManager).when(mPreferenceGroup).getPreferenceManager();
+        when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreferenceGroup);
+        mController = new AudioSharingDeviceVolumeGroupController(mContext);
+        mController.setDeviceUpdater(mDeviceUpdater);
+        mContentObserver = mController.getSettingsObserver();
+    }
+
+    @Test
+    public void onStart_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mAssistant, times(0))
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mDeviceUpdater, times(0)).registerCallback();
+        verify(mVolumeControl, times(0))
+                .registerCallback(any(Executor.class), any(BluetoothVolumeControl.Callback.class));
+        verify(mContentResolver, times(0))
+                .registerContentObserver(
+                        Settings.Secure.getUriFor(SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID),
+                        false,
+                        mContentObserver);
+    }
+
+    @Test
+    public void onStart_flagOn_registerCallbacks() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mAssistant)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mDeviceUpdater).registerCallback();
+        verify(mVolumeControl)
+                .registerCallback(any(Executor.class), any(BluetoothVolumeControl.Callback.class));
+        verify(mContentResolver)
+                .registerContentObserver(
+                        Settings.Secure.getUriFor(SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID),
+                        false,
+                        mContentObserver);
+    }
+
+    @Test
+    public void onStop_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStop(mLifecycleOwner);
+        verify(mAssistant, times(0))
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mDeviceUpdater, times(0)).unregisterCallback();
+        verify(mVolumeControl, times(0))
+                .unregisterCallback(any(BluetoothVolumeControl.Callback.class));
+        verify(mContentResolver, times(0)).unregisterContentObserver(mContentObserver);
+    }
+
+    @Test
+    public void onStop_flagOn_callbacksNotRegistered_doNothing() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(false);
+        mController.onStop(mLifecycleOwner);
+        verify(mAssistant, times(0))
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mDeviceUpdater, times(0)).unregisterCallback();
+        verify(mVolumeControl, times(0))
+                .unregisterCallback(any(BluetoothVolumeControl.Callback.class));
+        verify(mContentResolver, times(0)).unregisterContentObserver(mContentObserver);
+    }
+
+    @Test
+    public void onStop_flagOn_callbacksRegistered_unregisterCallbacks() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(true);
+        mController.onStop(mLifecycleOwner);
+        verify(mAssistant)
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mDeviceUpdater).unregisterCallback();
+        verify(mVolumeControl).unregisterCallback(any(BluetoothVolumeControl.Callback.class));
+        verify(mContentResolver).unregisterContentObserver(mContentObserver);
+    }
+
+    @Test
+    public void displayPreference_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.displayPreference(mScreen);
+        assertThat(mPreferenceGroup.isVisible()).isFalse();
+        verify(mDeviceUpdater, times(0)).forceUpdate();
+    }
+
+    @Test
+    public void displayPreference_flagOn_updateDeviceList() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.displayPreference(mScreen);
+        assertThat(mPreferenceGroup.isVisible()).isFalse();
+        verify(mDeviceUpdater).forceUpdate();
+    }
+
+    @Test
+    public void getPreferenceKey_returnsCorrectKey() {
+        assertThat(mController.getPreferenceKey()).isEqualTo(PREF_KEY);
+    }
+
+    @Test
+    public void onDeviceAdded_firstDevice_updateVisibility() {
+        when(mPreference1.getProgress()).thenReturn(TEST_VOLUME_VALUE);
+        mController.setPreferenceGroup(mPreferenceGroup);
+        mController.onDeviceAdded(mPreference1);
+        verify(mPreferenceGroup).setVisible(true);
+        assertThat(mPreferenceGroup.isVisible()).isTrue();
+    }
+
+    @Test
+    public void onDeviceAdded_rankFallbackDeviceOnTop() {
+        Settings.Secure.putInt(
+                mContentResolver, SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID, TEST_DEVICE_GROUP_ID2);
+        when(mPreference1.getProgress()).thenReturn(TEST_VOLUME_VALUE);
+        when(mPreference2.getProgress()).thenReturn(TEST_VOLUME_VALUE);
+        mController.setPreferenceGroup(mPreferenceGroup);
+        mController.onDeviceAdded(mPreference1);
+        mController.onDeviceAdded(mPreference2);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mPreference1).setOrder(1);
+        verify(mPreference2).setOrder(0);
+    }
+
+    @Test
+    public void onDeviceAdded_setVolumeFromVolumeControlService() {
+        when(mPreference1.getProgress()).thenReturn(TEST_INVALID_VOLUME_VALUE);
+        mController.setVolumeMap(ImmutableMap.of(TEST_DEVICE_GROUP_ID1, TEST_VOLUME_VALUE));
+        mController.setPreferenceGroup(mPreferenceGroup);
+        mController.onDeviceAdded(mPreference1);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mPreference1).setProgress(eq(TEST_VOLUME_VALUE));
+    }
+
+    @Test
+    public void onDeviceAdded_setVolumeFromAudioManager() {
+        when(mPreference1.getProgress()).thenReturn(TEST_INVALID_VOLUME_VALUE);
+        mController.setPreferenceGroup(mPreferenceGroup);
+        mController.onDeviceAdded(mPreference1);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mPreference1).setProgress(eq(26));
+    }
+
+    @Test
+    public void onDeviceRemoved_notLastDevice_isVisible() {
+        mPreferenceGroup.addPreference(mPreference2);
+        mPreferenceGroup.addPreference(mPreference1);
+        mController.setPreferenceGroup(mPreferenceGroup);
+        mController.onDeviceRemoved(mPreference1);
+        verify(mPreferenceGroup, times(0)).setVisible(false);
+        assertThat(mPreferenceGroup.isVisible()).isTrue();
+    }
+
+    @Test
+    public void onDeviceRemoved_lastDevice_updateVisibility() {
+        mPreferenceGroup.addPreference(mPreference1);
+        mController.setPreferenceGroup(mPreferenceGroup);
+        mController.onDeviceRemoved(mPreference1);
+        verify(mPreferenceGroup).setVisible(false);
+        assertThat(mPreferenceGroup.isVisible()).isFalse();
+    }
+
+    @Test
+    public void updateVisibility_emptyPreferenceGroup_doNothing() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(true);
+        mController.updateVisibility();
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mPreferenceGroup, times(0)).setVisible(anyBoolean());
+    }
+
+    @Test
+    public void updateVisibility_flagOff_setVisibleToFalse() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(true);
+        mPreferenceGroup.addPreference(mPreference1);
+        when(mBroadcast.isEnabled(null)).thenReturn(true);
+        mController.setPreferenceGroup(mPreferenceGroup);
+        mController.updateVisibility();
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertThat(mPreferenceGroup.getPreferenceCount() > 0).isTrue();
+        verify(mPreferenceGroup).setVisible(false);
+        assertThat(mPreferenceGroup.isVisible()).isFalse();
+    }
+
+    @Test
+    public void updateVisibility_notEmptyPreferenceGroup_noSharing_setVisibleToFalse() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(true);
+        mPreferenceGroup.addPreference(mPreference1);
+        when(mBroadcast.isEnabled(null)).thenReturn(false);
+        mController.setPreferenceGroup(mPreferenceGroup);
+        mController.updateVisibility();
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertThat(mPreferenceGroup.getPreferenceCount() > 0).isTrue();
+        verify(mPreferenceGroup).setVisible(false);
+        assertThat(mPreferenceGroup.isVisible()).isFalse();
+    }
+
+    @Test
+    public void updateVisibility_notEmptyPreferenceGroup_isSharing_setVisibleToTrue() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(true);
+        mPreferenceGroup.addPreference(mPreference1);
+        when(mBroadcast.isEnabled(null)).thenReturn(true);
+        mController.setPreferenceGroup(mPreferenceGroup);
+        mController.updateVisibility();
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertThat(mPreferenceGroup.getPreferenceCount() > 0).isTrue();
+        verify(mPreferenceGroup).setVisible(true);
+        assertThat(mPreferenceGroup.isVisible()).isTrue();
+    }
+
+    @Test
+    public void settingsObserverOnChange_updatePreferenceOrder() {
+        Settings.Secure.putInt(
+                mContentResolver, SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID, TEST_DEVICE_GROUP_ID2);
+        when(mPreference1.getProgress()).thenReturn(TEST_VOLUME_VALUE);
+        when(mPreference2.getProgress()).thenReturn(TEST_VOLUME_VALUE);
+        mController.setPreferenceGroup(mPreferenceGroup);
+        mController.onDeviceAdded(mPreference1);
+        mController.onDeviceAdded(mPreference2);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        Settings.Secure.putInt(
+                mContentResolver, SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID, TEST_DEVICE_GROUP_ID1);
+        mContentObserver.onChange(true);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mPreference1).setOrder(0);
+        verify(mPreference2).setOrder(1);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumePreferenceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumePreferenceTest.java
new file mode 100644
index 0000000..8ceb0eb
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDeviceVolumePreferenceTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+
+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 org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioSharingDeviceVolumePreferenceTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock private CachedBluetoothDevice mCachedDevice;
+    private Context mContext;
+    private AudioSharingDeviceVolumePreference mPreference;
+
+    @Before
+    public void setup() {
+        mContext = ApplicationProvider.getApplicationContext();
+        mPreference = new AudioSharingDeviceVolumePreference(mContext, mCachedDevice);
+    }
+
+    @Test
+    public void getCachedDevice_returnsDevice() {
+        assertThat(mPreference.getCachedDevice()).isEqualTo(mCachedDevice);
+    }
+
+    @Test
+    public void initialize_setupMaxMin() {
+        mPreference.initialize();
+        assertThat(mPreference.getMax()).isEqualTo(AudioSharingDeviceVolumePreference.MAX_VOLUME);
+        assertThat(mPreference.getMin()).isEqualTo(AudioSharingDeviceVolumePreference.MIN_VOLUME);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java
new file mode 100644
index 0000000..4336e77
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowAlertDialogCompat.class,
+            ShadowBluetoothAdapter.class,
+        })
+public class AudioSharingDialogFragmentTest {
+
+    @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private static final String TEST_DEVICE_NAME1 = "test1";
+    private static final String TEST_DEVICE_NAME2 = "test2";
+    private static final String TEST_DEVICE_NAME3 = "test3";
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM1 =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME1, /* groupId= */ 1, /* isActive= */ false);
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM2 =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME2, /* groupId= */ 2, /* isActive= */ false);
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM3 =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME3, /* groupId= */ 3, /* isActive= */ false);
+
+    private Fragment mParent;
+    private AudioSharingDialogFragment mFragment;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+    @Before
+    public void setUp() {
+        ShadowAlertDialogCompat.reset();
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mFragment = new AudioSharingDialogFragment();
+        mParent = new Fragment();
+        FragmentController.setupFragment(
+                mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+    }
+
+    @Test
+    public void onCreateDialog_flagOff_dialogNotExist() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNull();
+    }
+
+    @Test
+    public void onCreateDialog_flagOn_noConnectedDevice() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        TextView description = dialog.findViewById(R.id.description_text);
+        ImageView image = dialog.findViewById(R.id.description_image);
+        Button shareBtn = dialog.findViewById(R.id.positive_btn);
+        Button cancelBtn = dialog.findViewById(R.id.negative_btn);
+        assertThat(dialog.isShowing()).isTrue();
+        assertThat(description.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(description.getText().toString())
+                .isEqualTo(mParent.getString(R.string.audio_sharing_dialog_connect_device_content));
+        assertThat(image.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(shareBtn.getVisibility()).isEqualTo(View.GONE);
+        assertThat(cancelBtn.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void onCreateDialog_noConnectedDevice_dialogDismiss() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        dialog.findViewById(android.R.id.button2).performClick();
+        shadowMainLooper().idle();
+
+        assertThat(dialog.isShowing()).isFalse();
+    }
+
+    @Test
+    public void onCreateDialog_flagOn_singleConnectedDevice() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+        list.add(TEST_DEVICE_ITEM1);
+        mFragment.show(mParent, list, (item) -> {});
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        TextView title = dialog.findViewById(R.id.title_text);
+        TextView description = dialog.findViewById(R.id.description_text);
+        ImageView image = dialog.findViewById(R.id.description_image);
+        Button shareBtn = dialog.findViewById(R.id.positive_btn);
+        Button cancelBtn = dialog.findViewById(R.id.negative_btn);
+        assertThat(dialog.isShowing()).isTrue();
+        assertThat(title.getText().toString())
+                .isEqualTo(
+                        mParent.getString(
+                                R.string.audio_sharing_share_with_dialog_title, TEST_DEVICE_NAME1));
+        assertThat(description.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(description.getText().toString())
+                .isEqualTo(mParent.getString(R.string.audio_sharing_dialog_share_content));
+        assertThat(image.getVisibility()).isEqualTo(View.GONE);
+        assertThat(shareBtn.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(cancelBtn.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void onCreateDialog_singleConnectedDevice_dialogDismiss() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+        list.add(TEST_DEVICE_ITEM1);
+        mFragment.show(mParent, list, (item) -> {});
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        dialog.findViewById(R.id.negative_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+    }
+
+    @Test
+    public void onCreateDialog_singleConnectedDevice_shareClicked() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+        list.add(TEST_DEVICE_ITEM1);
+        AtomicBoolean isShareBtnClicked = new AtomicBoolean(false);
+        mFragment.show(mParent, list, (item) -> isShareBtnClicked.set(true));
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        dialog.findViewById(R.id.positive_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+        assertThat(isShareBtnClicked.get()).isTrue();
+    }
+
+    @Test
+    public void onCreateDialog_flagOn_multipleConnectedDevice() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+        list.add(TEST_DEVICE_ITEM1);
+        list.add(TEST_DEVICE_ITEM2);
+        list.add(TEST_DEVICE_ITEM3);
+        mFragment.show(mParent, list, (item) -> {});
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        TextView description = dialog.findViewById(R.id.description_text);
+        ImageView image = dialog.findViewById(R.id.description_image);
+        Button shareBtn = dialog.findViewById(R.id.positive_btn);
+        Button cancelBtn = dialog.findViewById(R.id.negative_btn);
+        RecyclerView recyclerView = dialog.findViewById(R.id.device_btn_list);
+        assertThat(dialog.isShowing()).isTrue();
+        assertThat(description.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(description.getText().toString())
+                .isEqualTo(mParent.getString(R.string.audio_sharing_dialog_share_more_content));
+        assertThat(image.getVisibility()).isEqualTo(View.GONE);
+        assertThat(shareBtn.getVisibility()).isEqualTo(View.GONE);
+        assertThat(cancelBtn.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(recyclerView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(recyclerView.getAdapter().getItemCount()).isEqualTo(3);
+    }
+
+    @Test
+    public void onCreateDialog_multipleConnectedDevice_dialogDismiss() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+        list.add(TEST_DEVICE_ITEM1);
+        list.add(TEST_DEVICE_ITEM2);
+        list.add(TEST_DEVICE_ITEM3);
+        mFragment.show(mParent, list, (item) -> {});
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        dialog.findViewById(R.id.negative_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandlerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandlerTest.java
new file mode 100644
index 0000000..570af1f
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHandlerTest.java
@@ -0,0 +1,385 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
+import android.os.Looper;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LeAudioProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.flags.Flags;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.truth.Correspondence;
+
+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 org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+        })
+public class AudioSharingDialogHandlerTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private static final String TEST_DEVICE_NAME1 = "test1";
+    private static final String TEST_DEVICE_NAME2 = "test2";
+    private static final String TEST_DEVICE_NAME3 = "test3";
+    private static final String TEST_DEVICE_NAME4 = "test4";
+    private static final String TEST_DEVICE_ADDRESS = "xx:xx:xx:xx";
+    private static final Correspondence<Fragment, String> TAG_EQUALS =
+            Correspondence.from(
+                    (Fragment fragment, String tag) ->
+                            fragment instanceof DialogFragment
+                                    && ((DialogFragment) fragment).getTag().equals(tag),
+                    "is equal to");
+
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
+    @Mock private CachedBluetoothDeviceManager mCacheManager;
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private CachedBluetoothDevice mCachedDevice1;
+    @Mock private CachedBluetoothDevice mCachedDevice2;
+    @Mock private CachedBluetoothDevice mCachedDevice3;
+    @Mock private CachedBluetoothDevice mCachedDevice4;
+    @Mock private BluetoothDevice mDevice1;
+    @Mock private BluetoothDevice mDevice2;
+    @Mock private BluetoothDevice mDevice3;
+    @Mock private BluetoothDevice mDevice4;
+    @Mock private LeAudioProfile mLeAudioProfile;
+    private Fragment mParentFragment;
+    @Mock private BluetoothLeBroadcastReceiveState mState;
+    private Context mContext;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private AudioSharingDialogHandler mHandler;
+
+    @Before
+    public void setup() {
+        mContext = ApplicationProvider.getApplicationContext();
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mLocalBtManager = Utils.getLocalBtManager(mContext);
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mLocalBtManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
+        when(mLocalBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        when(mLocalBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(mState.getBisSyncState()).thenReturn(bisSyncState);
+        when(mLeAudioProfile.isEnabled(any())).thenReturn(true);
+        when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
+        when(mCachedDevice1.getDevice()).thenReturn(mDevice1);
+        when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile));
+        when(mCachedDevice1.getGroupId()).thenReturn(1);
+        when(mCachedDevice2.getName()).thenReturn(TEST_DEVICE_NAME2);
+        when(mCachedDevice2.getAddress()).thenReturn(TEST_DEVICE_ADDRESS);
+        when(mCachedDevice2.getDevice()).thenReturn(mDevice2);
+        when(mCachedDevice2.getProfiles()).thenReturn(List.of());
+        when(mCachedDevice2.getGroupId()).thenReturn(2);
+        when(mCachedDevice3.getName()).thenReturn(TEST_DEVICE_NAME3);
+        when(mCachedDevice3.getDevice()).thenReturn(mDevice3);
+        when(mCachedDevice3.getProfiles()).thenReturn(List.of(mLeAudioProfile));
+        when(mCachedDevice3.getGroupId()).thenReturn(3);
+        when(mCachedDevice4.getName()).thenReturn(TEST_DEVICE_NAME4);
+        when(mCachedDevice4.getDevice()).thenReturn(mDevice4);
+        when(mCachedDevice4.getProfiles()).thenReturn(List.of(mLeAudioProfile));
+        when(mCachedDevice4.getGroupId()).thenReturn(4);
+        when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mCacheManager);
+        when(mCacheManager.findDevice(mDevice1)).thenReturn(mCachedDevice1);
+        when(mCacheManager.findDevice(mDevice2)).thenReturn(mCachedDevice2);
+        when(mCacheManager.findDevice(mDevice3)).thenReturn(mCachedDevice3);
+        when(mCacheManager.findDevice(mDevice4)).thenReturn(mCachedDevice4);
+        mParentFragment = new Fragment();
+        FragmentController.setupFragment(
+                mParentFragment,
+                FragmentActivity.class,
+                0 /* containerViewId */,
+                null /* bundle */);
+        mHandler = new AudioSharingDialogHandler(mContext, mParentFragment);
+    }
+
+    @Test
+    public void handleUserTriggeredNonLeaDeviceConnected_noSharing_setActive() {
+        setUpBroadcast(false);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice2);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
+        mHandler.handleDeviceConnected(mCachedDevice2, /* userTriggered= */ true);
+        shadowOf(Looper.getMainLooper()).idle();
+        verify(mCachedDevice2).setActive();
+    }
+
+    @Test
+    public void handleUserTriggeredNonLeaDeviceConnected_sharing_showStopDialog() {
+        setUpBroadcast(true);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice2);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mState));
+        mHandler.handleDeviceConnected(mCachedDevice2, /* userTriggered= */ true);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments())
+                .comparingElementsUsing(TAG_EQUALS)
+                .containsExactly(AudioSharingStopDialogFragment.tag());
+    }
+
+    @Test
+    public void handleUserTriggeredLeaDeviceConnected_noSharingNoTwoLeaDevices_setActive() {
+        setUpBroadcast(false);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice1);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
+        mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true);
+        shadowOf(Looper.getMainLooper()).idle();
+        verify(mCachedDevice1).setActive();
+    }
+
+    @Test
+    public void handleUserTriggeredLeaDeviceConnected_noSharingTwoLeaDevices_showJoinDialog() {
+        setUpBroadcast(false);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice1, mDevice3);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
+        mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments())
+                .comparingElementsUsing(TAG_EQUALS)
+                .containsExactly(AudioSharingJoinDialogFragment.tag());
+    }
+
+    @Test
+    public void handleUserTriggeredLeaDeviceConnected_sharing_showJoinDialog() {
+        setUpBroadcast(true);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice1, mDevice3);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of());
+        when(mAssistant.getAllSources(mDevice3)).thenReturn(ImmutableList.of(mState));
+        mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments())
+                .comparingElementsUsing(TAG_EQUALS)
+                .containsExactly(AudioSharingJoinDialogFragment.tag());
+    }
+
+    @Test
+    public void
+            handleUserTriggeredLeaDeviceConnected_sharingWithTwoLeaDevices_showDisconnectDialog() {
+        setUpBroadcast(true);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice1, mDevice3, mDevice4);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of());
+        when(mAssistant.getAllSources(mDevice3)).thenReturn(ImmutableList.of(mState));
+        when(mAssistant.getAllSources(mDevice4)).thenReturn(ImmutableList.of(mState));
+        mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments())
+                .comparingElementsUsing(TAG_EQUALS)
+                .containsExactly(AudioSharingDisconnectDialogFragment.tag());
+    }
+
+    @Test
+    public void handleNonLeaDeviceConnected_noSharing_doNothing() {
+        setUpBroadcast(false);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice2);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
+        mHandler.handleDeviceConnected(mCachedDevice2, /* userTriggered= */ false);
+        shadowOf(Looper.getMainLooper()).idle();
+        verify(mCachedDevice2, times(0)).setActive();
+    }
+
+    @Test
+    public void handleNonLeaDeviceConnected_sharing_showStopDialog() {
+        setUpBroadcast(true);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice2);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mState));
+        mHandler.handleDeviceConnected(mCachedDevice2, /* userTriggered= */ false);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments())
+                .comparingElementsUsing(TAG_EQUALS)
+                .containsExactly(AudioSharingStopDialogFragment.tag());
+    }
+
+    @Test
+    public void handleLeaDeviceConnected_noSharingNoTwoLeaDevices_doNothing() {
+        setUpBroadcast(false);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice1);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
+        mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ false);
+        shadowOf(Looper.getMainLooper()).idle();
+        verify(mCachedDevice1, times(0)).setActive();
+    }
+
+    @Test
+    public void handleLeaDeviceConnected_noSharingTwoLeaDevices_showJoinDialog() {
+        setUpBroadcast(false);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice1, mDevice3);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
+        mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ false);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments())
+                .comparingElementsUsing(TAG_EQUALS)
+                .containsExactly(AudioSharingJoinDialogFragment.tag());
+    }
+
+    @Test
+    public void handleLeaDeviceConnected_sharing_showJoinDialog() {
+        setUpBroadcast(true);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice1, mDevice3);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of());
+        when(mAssistant.getAllSources(mDevice3)).thenReturn(ImmutableList.of(mState));
+        mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ false);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments())
+                .comparingElementsUsing(TAG_EQUALS)
+                .containsExactly(AudioSharingJoinDialogFragment.tag());
+    }
+
+    @Test
+    public void handleLeaDeviceConnected_sharingWithTwoLeaDevices_showDisconnectDialog() {
+        setUpBroadcast(true);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice1, mDevice3, mDevice4);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(mDevice1)).thenReturn(ImmutableList.of());
+        when(mAssistant.getAllSources(mDevice3)).thenReturn(ImmutableList.of(mState));
+        when(mAssistant.getAllSources(mDevice4)).thenReturn(ImmutableList.of(mState));
+        mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ false);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments())
+                .comparingElementsUsing(TAG_EQUALS)
+                .containsExactly(AudioSharingDisconnectDialogFragment.tag());
+    }
+
+    @Test
+    public void closeOpeningDialogsForLeaDevice_closeJoinDialog() {
+        // Show join dialog
+        setUpBroadcast(false);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice1, mDevice3);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
+        mHandler.handleDeviceConnected(mCachedDevice1, /* userTriggered= */ true);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments())
+                .comparingElementsUsing(TAG_EQUALS)
+                .containsExactly(AudioSharingJoinDialogFragment.tag());
+        // Close opening dialogs
+        mHandler.closeOpeningDialogsForLeaDevice(mCachedDevice1);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments()).isEmpty();
+    }
+
+    @Test
+    public void closeOpeningDialogsForNonLeaDevice_closeStopDialog() {
+        // Show stop dialog
+        setUpBroadcast(true);
+        ImmutableList<BluetoothDevice> deviceList = ImmutableList.of(mDevice2);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(deviceList);
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mState));
+        mHandler.handleDeviceConnected(mCachedDevice2, /* userTriggered= */ true);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments())
+                .comparingElementsUsing(TAG_EQUALS)
+                .containsExactly(AudioSharingStopDialogFragment.tag());
+        // Close opening dialogs
+        mHandler.closeOpeningDialogsForNonLeaDevice(mCachedDevice2);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mParentFragment.getChildFragmentManager().getFragments()).isEmpty();
+    }
+
+    private void setUpBroadcast(boolean isBroadcasting) {
+        when(mBroadcast.isEnabled(any())).thenReturn(isBroadcasting);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHelperTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHelperTest.java
new file mode 100644
index 0000000..53de62e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogHelperTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Typeface;
+import android.util.TypedValue;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
+
+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 org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioSharingDialogHelperTest {
+    private static final String TAG = "test";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock FragmentManager mFragmentManager;
+    @Mock DialogFragment mFragment;
+    @Mock AlertDialog mDialog;
+    @Mock TextView mTextView;
+
+    @Test
+    public void updateMessageStyle_updateStyle() {
+        when(mDialog.findViewById(android.R.id.message)).thenReturn(mTextView);
+        AudioSharingDialogHelper.updateMessageStyle(mDialog);
+        Typeface typeface = Typeface.create(Typeface.DEFAULT_FAMILY, Typeface.NORMAL);
+        verify(mTextView).setTypeface(typeface);
+        verify(mTextView).setTextDirection(View.TEXT_DIRECTION_LOCALE);
+        verify(mTextView).setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
+        verify(mTextView).setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
+    }
+
+    @Test
+    public void getDialogIfShowing_notShowing_returnNull() {
+        when(mFragmentManager.findFragmentByTag(TAG)).thenReturn(mFragment);
+        when(mFragment.getDialog()).thenReturn(mDialog);
+        when(mDialog.isShowing()).thenReturn(false);
+        assertThat(AudioSharingDialogHelper.getDialogIfShowing(mFragmentManager, TAG)).isNull();
+    }
+
+    @Test
+    public void getDialogIfShowing_showing_returnDialog() {
+        when(mFragmentManager.findFragmentByTag(TAG)).thenReturn(mFragment);
+        when(mFragment.getDialog()).thenReturn(mDialog);
+        when(mDialog.isShowing()).thenReturn(true);
+        assertThat(AudioSharingDialogHelper.getDialogIfShowing(mFragmentManager, TAG))
+                .isEqualTo(mDialog);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java
new file mode 100644
index 0000000..348efbe
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothStatusCodes;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.widget.Button;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settings.R;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.flags.Flags;
+
+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 org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowAlertDialogCompat.class,
+            ShadowBluetoothAdapter.class,
+        })
+public class AudioSharingDisconnectDialogFragmentTest {
+
+    @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private static final String TEST_DEVICE_NAME1 = "test1";
+    private static final String TEST_DEVICE_NAME2 = "test2";
+    private static final String TEST_DEVICE_NAME3 = "test3";
+    private static final int TEST_GROUP_ID1 = 1;
+    private static final int TEST_GROUP_ID2 = 2;
+    private static final int TEST_GROUP_ID3 = 3;
+    private static final String TEST_ADDRESS1 = "XX:11";
+    private static final String TEST_ADDRESS3 = "XX:33";
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM1 =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME1, TEST_GROUP_ID1, /* isActive= */ true);
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM2 =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME2, TEST_GROUP_ID2, /* isActive= */ false);
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM3 =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME3, TEST_GROUP_ID3, /* isActive= */ false);
+
+    @Mock private BluetoothDevice mDevice1;
+    @Mock private BluetoothDevice mDevice3;
+
+    @Mock private CachedBluetoothDevice mCachedDevice1;
+    @Mock private CachedBluetoothDevice mCachedDevice3;
+    private Fragment mParent;
+    private AudioSharingDisconnectDialogFragment mFragment;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private ArrayList<AudioSharingDeviceItem> mDeviceItems = new ArrayList<>();
+
+    @Before
+    public void setUp() {
+        AlertDialog latestAlertDialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        if (latestAlertDialog != null) {
+            latestAlertDialog.dismiss();
+            ShadowAlertDialogCompat.reset();
+        }
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        when(mDevice1.getAnonymizedAddress()).thenReturn(TEST_ADDRESS1);
+        when(mDevice3.getAnonymizedAddress()).thenReturn(TEST_ADDRESS3);
+        when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
+        when(mCachedDevice1.getDevice()).thenReturn(mDevice1);
+        when(mCachedDevice1.getGroupId()).thenReturn(TEST_GROUP_ID1);
+        when(mCachedDevice3.getName()).thenReturn(TEST_DEVICE_NAME3);
+        when(mCachedDevice3.getDevice()).thenReturn(mDevice3);
+        when(mCachedDevice3.getGroupId()).thenReturn(TEST_GROUP_ID3);
+        mFragment = new AudioSharingDisconnectDialogFragment();
+        mParent = new Fragment();
+        FragmentController.setupFragment(
+                mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+    }
+
+    @Test
+    public void onCreateDialog_flagOff_dialogNotExist() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mDeviceItems = new ArrayList<>();
+        mDeviceItems.add(TEST_DEVICE_ITEM1);
+        mDeviceItems.add(TEST_DEVICE_ITEM2);
+        mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNull();
+    }
+
+    @Test
+    public void onCreateDialog_flagOn_dialogShowBtnForTwoDevices() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mDeviceItems = new ArrayList<>();
+        mDeviceItems.add(TEST_DEVICE_ITEM1);
+        mDeviceItems.add(TEST_DEVICE_ITEM2);
+        mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog.isShowing()).isTrue();
+        RecyclerView view = dialog.findViewById(R.id.device_btn_list);
+        assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
+    }
+
+    @Test
+    public void onCreateDialog_dialogIsShowingForSameGroup_updateDialog() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mDeviceItems = new ArrayList<>();
+        mDeviceItems.add(TEST_DEVICE_ITEM1);
+        mDeviceItems.add(TEST_DEVICE_ITEM2);
+        mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
+        shadowMainLooper().idle();
+        AtomicBoolean isItemBtnClicked = new AtomicBoolean(false);
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog.isShowing()).isTrue();
+        RecyclerView view = dialog.findViewById(R.id.device_btn_list);
+        assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
+        Button btn1 =
+                view.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.device_button);
+        assertThat(btn1.getText().toString())
+                .isEqualTo(
+                        mParent.getString(
+                                R.string.audio_sharing_disconnect_device_button_label,
+                                TEST_DEVICE_NAME1));
+        Button btn2 =
+                view.findViewHolderForAdapterPosition(1).itemView.findViewById(R.id.device_button);
+        assertThat(btn2.getText().toString())
+                .isEqualTo(
+                        mParent.getString(
+                                R.string.audio_sharing_disconnect_device_button_label,
+                                TEST_DEVICE_NAME2));
+
+        // Update dialog content for device with same group
+        mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> isItemBtnClicked.set(true));
+        shadowMainLooper().idle();
+        dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog.isShowing()).isTrue();
+        btn1 = view.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.device_button);
+        btn1.performClick();
+        assertThat(isItemBtnClicked.get()).isTrue();
+    }
+
+    @Test
+    public void onCreateDialog_dialogIsShowingForNewGroup_updateDialog() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mDeviceItems = new ArrayList<>();
+        mDeviceItems.add(TEST_DEVICE_ITEM1);
+        mDeviceItems.add(TEST_DEVICE_ITEM2);
+        mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog.isShowing()).isTrue();
+        RecyclerView view = dialog.findViewById(R.id.device_btn_list);
+        assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
+
+        // Show new dialog for device with new group
+        ArrayList<AudioSharingDeviceItem> newDeviceItems = new ArrayList<>();
+        newDeviceItems.add(TEST_DEVICE_ITEM2);
+        newDeviceItems.add(TEST_DEVICE_ITEM3);
+        mFragment.show(mParent, newDeviceItems, mCachedDevice1, (item) -> {});
+        shadowMainLooper().idle();
+        dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog.isShowing()).isTrue();
+        view = dialog.findViewById(R.id.device_btn_list);
+        assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
+        Button btn1 =
+                view.findViewHolderForAdapterPosition(0).itemView.findViewById(R.id.device_button);
+        assertThat(btn1.getText().toString())
+                .isEqualTo(
+                        mParent.getString(
+                                R.string.audio_sharing_disconnect_device_button_label,
+                                TEST_DEVICE_NAME2));
+        Button btn2 =
+                view.findViewHolderForAdapterPosition(1).itemView.findViewById(R.id.device_button);
+        assertThat(btn2.getText().toString())
+                .isEqualTo(
+                        mParent.getString(
+                                R.string.audio_sharing_disconnect_device_button_label,
+                                TEST_DEVICE_NAME3));
+    }
+
+    @Test
+    public void onCreateDialog_clickCancel_dialogDismiss() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mDeviceItems = new ArrayList<>();
+        mDeviceItems.add(TEST_DEVICE_ITEM1);
+        mDeviceItems.add(TEST_DEVICE_ITEM2);
+        mFragment.show(mParent, mDeviceItems, mCachedDevice3, (item) -> {});
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog.isShowing()).isTrue();
+        dialog.findViewById(R.id.negative_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImplTest.java
deleted file mode 100644
index 1965bff..0000000
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingFeatureProviderImplTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.settings.connecteddevice.audiosharing;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.content.Context;
-
-import androidx.test.core.app.ApplicationProvider;
-
-import com.android.settings.connecteddevice.AvailableMediaDeviceGroupController;
-import com.android.settings.dashboard.DashboardFragment;
-import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
-
-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 org.robolectric.RobolectricTestRunner;
-
-@RunWith(RobolectricTestRunner.class)
-public class AudioSharingFeatureProviderImplTest {
-    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
-
-    @Mock private CachedBluetoothDevice mCachedDevice;
-    @Mock private LocalBluetoothManager mLocalBtManager;
-    @Mock private DashboardFragment mFragment;
-    private Context mContext;
-    private AudioSharingFeatureProviderImpl mFeatureProvider;
-
-    @Before
-    public void setUp() {
-        mContext = ApplicationProvider.getApplicationContext();
-        mFeatureProvider = new AudioSharingFeatureProviderImpl();
-    }
-
-    @Test
-    public void createAudioSharingDevicePreferenceController_returnsNull() {
-        assertThat(
-                        mFeatureProvider.createAudioSharingDevicePreferenceController(
-                                mContext, mFragment, /* lifecycle= */ null))
-                .isNull();
-    }
-
-    @Test
-    public void createAvailableMediaDeviceGroupController_returnsNull() {
-        assertThat(
-                        mFeatureProvider.createAvailableMediaDeviceGroupController(
-                                mContext, /* fragment= */ null, /* lifecycle= */ null))
-                .isInstanceOf(AvailableMediaDeviceGroupController.class);
-    }
-
-    @Test
-    public void isAudioSharingFilterMatched_returnsFalse() {
-        assertThat(mFeatureProvider.isAudioSharingFilterMatched(mCachedDevice, mLocalBtManager))
-                .isFalse();
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.java
new file mode 100644
index 0000000..5d43ccc
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settings.R;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.flags.Flags;
+
+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 org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowAlertDialogCompat.class,
+            ShadowBluetoothAdapter.class,
+        })
+public class AudioSharingJoinDialogFragmentTest {
+
+    @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private static final String TEST_DEVICE_NAME1 = "test1";
+    private static final String TEST_DEVICE_NAME2 = "test2";
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM1 =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME1, /* groupId= */ 1, /* isActive= */ true);
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM2 =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME2, /* groupId= */ 2, /* isActive= */ false);
+    private static final AudioSharingJoinDialogFragment.DialogEventListener EMPTY_EVENT_LISTENER =
+            new AudioSharingJoinDialogFragment.DialogEventListener() {
+                @Override
+                public void onShareClick() {}
+
+                @Override
+                public void onCancelClick() {}
+            };
+
+    @Mock private CachedBluetoothDevice mCachedDevice1;
+    @Mock private CachedBluetoothDevice mCachedDevice2;
+    private Fragment mParent;
+    private AudioSharingJoinDialogFragment mFragment;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+    @Before
+    public void setUp() {
+        AlertDialog latestAlertDialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        if (latestAlertDialog != null) {
+            latestAlertDialog.dismiss();
+            ShadowAlertDialogCompat.reset();
+        }
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
+        when(mCachedDevice2.getName()).thenReturn(TEST_DEVICE_NAME2);
+        mFragment = new AudioSharingJoinDialogFragment();
+        mParent = new Fragment();
+        FragmentController.setupFragment(
+                mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+    }
+
+    @Test
+    public void onCreateDialog_flagOff_dialogNotExist() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, new ArrayList<>(), mCachedDevice2, EMPTY_EVENT_LISTENER);
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNull();
+    }
+
+    @Test
+    public void onCreateDialog_flagOn_dialogShowTextForSingleDevice() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, new ArrayList<>(), mCachedDevice2, EMPTY_EVENT_LISTENER);
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        assertThat(shadowDialog.getMessage().toString()).isEqualTo(TEST_DEVICE_NAME2);
+    }
+
+    @Test
+    public void onCreateDialog_flagOn_dialogShowTextForTwoDevice() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+        list.add(TEST_DEVICE_ITEM1);
+        mFragment.show(mParent, list, mCachedDevice2, EMPTY_EVENT_LISTENER);
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        assertThat(shadowDialog.getMessage().toString())
+                .isEqualTo(
+                        mParent.getString(
+                                R.string.audio_sharing_share_dialog_subtitle,
+                                TEST_DEVICE_NAME1,
+                                TEST_DEVICE_NAME2));
+    }
+
+    @Test
+    public void onCreateDialog_dialogIsShowing_updateDialog() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+        list.add(TEST_DEVICE_ITEM1);
+        mFragment.show(mParent, list, mCachedDevice2, EMPTY_EVENT_LISTENER);
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+
+        // Update the content
+        ArrayList<AudioSharingDeviceItem> list2 = new ArrayList<>();
+        list2.add(TEST_DEVICE_ITEM2);
+        mFragment.show(mParent, list2, mCachedDevice1, EMPTY_EVENT_LISTENER);
+        shadowMainLooper().idle();
+        dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        assertThat(shadowDialog.getMessage().toString())
+                .isEqualTo(
+                        mParent.getString(
+                                R.string.audio_sharing_share_dialog_subtitle,
+                                TEST_DEVICE_NAME2,
+                                TEST_DEVICE_NAME1));
+    }
+
+    @Test
+    public void onCreateDialog_clickCancel_dialogDismiss() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, new ArrayList<>(), mCachedDevice2, EMPTY_EVENT_LISTENER);
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        dialog.findViewById(R.id.negative_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+    }
+
+    @Test
+    public void onCreateDialog_clickBtn_callbackTriggered() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        AtomicBoolean isShareBtnClicked = new AtomicBoolean(false);
+        mFragment.show(
+                mParent,
+                new ArrayList<>(),
+                mCachedDevice2,
+                new AudioSharingJoinDialogFragment.DialogEventListener() {
+                    @Override
+                    public void onShareClick() {
+                        isShareBtnClicked.set(true);
+                    }
+
+                    @Override
+                    public void onCancelClick() {}
+                });
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        dialog.findViewById(R.id.positive_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+        assertThat(isShareBtnClicked.get()).isTrue();
+    }
+
+    @Test
+    public void onCreateDialog_clickCancel_callbackTriggered() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        AtomicBoolean isCancelBtnClicked = new AtomicBoolean(false);
+        mFragment.show(
+                mParent,
+                new ArrayList<>(),
+                mCachedDevice2,
+                new AudioSharingJoinDialogFragment.DialogEventListener() {
+                    @Override
+                    public void onShareClick() {}
+
+                    @Override
+                    public void onCancelClick() {
+                        isCancelBtnClicked.set(true);
+                    }
+                });
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        dialog.findViewById(R.id.negative_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+        assertThat(isCancelBtnClicked.get()).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceControllerTest.java
new file mode 100644
index 0000000..f811930
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPlaySoundPreferenceControllerTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothAdapter.class})
+public class AudioSharingPlaySoundPreferenceControllerTest {
+    private static final String PREF_KEY = "audio_sharing_play_sound";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Spy Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private PreferenceScreen mScreen;
+    @Mock private Ringtone mRingtone;
+
+    private AudioSharingPlaySoundPreferenceController mController;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private Lifecycle mLifecycle;
+    private LifecycleOwner mLifecycleOwner;
+    private Preference mPreference;
+
+    @Before
+    public void setUp() {
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        when(mRingtone.getStreamType()).thenReturn(AudioManager.STREAM_MUSIC);
+        when(mRingtone.getAudioAttributes()).thenReturn(new AudioAttributes.Builder().build());
+        mController = new AudioSharingPlaySoundPreferenceController(mContext);
+        mController.setRingtone(mRingtone);
+        mPreference = new Preference(mContext);
+        when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOn_available() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOff_unsupported() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_nullRingtone_unsupported() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setRingtone(null);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+    }
+
+    @Test
+    public void displayPreference_visible() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mPreference.setVisible(false);
+        mController.displayPreference(mScreen);
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+
+    @Test
+    public void displayPreference_nullRingtone_invisible() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mPreference.setVisible(true);
+        mController.setRingtone(null);
+        mController.displayPreference(mScreen);
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void displayPreference_flagOff_invisible() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mPreference.setVisible(true);
+        mController.displayPreference(mScreen);
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void getPreferenceKey_returnsCorrectKey() {
+        assertThat(mController.getPreferenceKey()).isEqualTo(PREF_KEY);
+    }
+
+    @Test
+    public void clickPreference_ringtoneIsNull_doNothing() {
+        mController.setRingtone(null);
+        when(mRingtone.isPlaying()).thenReturn(false);
+        doNothing().when(mRingtone).play();
+        mController.displayPreference(mScreen);
+        mPreference.performClick();
+        verify(mRingtone, times(0)).play();
+    }
+
+    @Test
+    public void clickPreference_isPlaying_doNothing() {
+        when(mRingtone.isPlaying()).thenReturn(true);
+        doNothing().when(mRingtone).play();
+        mController.displayPreference(mScreen);
+        mPreference.performClick();
+        verify(mRingtone, times(0)).play();
+    }
+
+    @Test
+    public void clickPreference_notPlaying_play() {
+        when(mRingtone.isPlaying()).thenReturn(false);
+        doNothing().when(mRingtone).play();
+        mController.displayPreference(mScreen);
+        mPreference.performClick();
+        verify(mRingtone).play();
+    }
+
+    @Test
+    public void nonStop_isPlaying_stop() {
+        when(mRingtone.isPlaying()).thenReturn(true);
+        doNothing().when(mRingtone).stop();
+        mController.onStop(mLifecycleOwner);
+        verify(mRingtone).stop();
+    }
+
+    @Test
+    public void nonStop_notPlaying_doNothing() {
+        when(mRingtone.isPlaying()).thenReturn(false);
+        doNothing().when(mRingtone).stop();
+        mController.onStop(mLifecycleOwner);
+        verify(mRingtone, times(0)).stop();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceControllerTest.java
new file mode 100644
index 0000000..b8bee1a
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceControllerTest.java
@@ -0,0 +1,169 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static android.bluetooth.BluetoothAdapter.STATE_OFF;
+import static android.bluetooth.BluetoothAdapter.STATE_ON;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
+import android.os.Looper;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settings.testutils.shadow.ShadowThreadUtils;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+            ShadowThreadUtils.class
+        })
+public class AudioSharingPreferenceControllerTest {
+    private static final String PREF_KEY = "audio_sharing_settings";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Spy Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private PreferenceScreen mScreen;
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private BluetoothEventManager mBtEventManager;
+    @Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    private AudioSharingPreferenceController mController;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private LocalBluetoothManager mLocalBluetoothManager;
+    private Lifecycle mLifecycle;
+    private LifecycleOwner mLifecycleOwner;
+    private Preference mPreference;
+
+    @Before
+    public void setUp() {
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+        when(mLocalBluetoothManager.getEventManager()).thenReturn(mBtEventManager);
+        when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
+        when(mLocalBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        mController = new AudioSharingPreferenceController(mContext, PREF_KEY);
+        mPreference = new Preference(mContext);
+        when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference);
+    }
+
+    @Test
+    public void onStart_registerCallback() {
+        mController.onStart(mLifecycleOwner);
+        verify(mBtEventManager).registerCallback(mController);
+        verify(mBroadcast).registerServiceCallBack(any(), any(BluetoothLeBroadcast.Callback.class));
+    }
+
+    @Test
+    public void onStop_unregisterCallback() {
+        mController.onStop(mLifecycleOwner);
+        verify(mBtEventManager).unregisterCallback(mController);
+        verify(mBroadcast).unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOn() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOff() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+    }
+
+    @Test
+    public void getSummary_broadcastOn() {
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        assertThat(mController.getSummary().toString())
+                .isEqualTo(mContext.getString(R.string.audio_sharing_summary_on));
+    }
+
+    @Test
+    public void getSummary_broadcastOff() {
+        when(mBroadcast.isEnabled(any())).thenReturn(false);
+        assertThat(mController.getSummary().toString())
+                .isEqualTo(mContext.getString(R.string.audio_sharing_summary_off));
+    }
+
+    @Test
+    public void onBluetoothStateChanged_refreshSummary() {
+        mController.displayPreference(mScreen);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        mController.onBluetoothStateChanged(STATE_ON);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.getSummary().toString())
+                .isEqualTo(mContext.getString(R.string.audio_sharing_summary_on));
+
+        when(mBroadcast.isEnabled(any())).thenReturn(false);
+        mController.onBluetoothStateChanged(STATE_OFF);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.getSummary().toString())
+                .isEqualTo(mContext.getString(R.string.audio_sharing_summary_off));
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiverTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiverTest.java
new file mode 100644
index 0000000..d750297
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingReceiverTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_STATE_CHANGE;
+import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.BROADCAST_STATE_OFF;
+import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.BROADCAST_STATE_ON;
+import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_LE_AUDIO_SHARING_STATE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settingslib.R;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.flags.Flags;
+
+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 org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowApplication;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class})
+public class AudioSharingReceiverTest {
+    private static final String ACTION_LE_AUDIO_SHARING_STOP =
+            "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STOP";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private Context mContext;
+    private ShadowApplication mShadowApplication;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private LocalBluetoothManager mLocalBluetoothManager;
+    @Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private NotificationManager mNm;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.getApplication();
+        mShadowApplication = Shadow.extract(mContext);
+        mShadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm);
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+        when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
+        when(mLocalBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+    }
+
+    @Test
+    public void broadcastReceiver_isRegistered() {
+        List<ShadowApplication.Wrapper> registeredReceivers =
+                mShadowApplication.getRegisteredReceivers();
+
+        int matchedCount =
+                registeredReceivers.stream()
+                        .filter(
+                                receiver ->
+                                        AudioSharingReceiver.class
+                                                .getSimpleName()
+                                                .equals(
+                                                        receiver.broadcastReceiver
+                                                                .getClass()
+                                                                .getSimpleName()))
+                        .collect(Collectors.toList())
+                        .size();
+        assertThat(matchedCount).isEqualTo(1);
+    }
+
+    @Test
+    public void broadcastReceiver_receiveAudioSharingStateChangeIntentFlagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+
+        Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE);
+        intent.setPackage(mContext.getPackageName());
+        intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, BROADCAST_STATE_ON);
+        AudioSharingReceiver audioSharingReceiver = getAudioSharingReceiver(intent);
+        audioSharingReceiver.onReceive(mContext, intent);
+
+        verifyNoInteractions(mNm);
+    }
+
+    @Test
+    public void broadcastReceiver_receiveAudioSharingStateChangeIntentNoState_doNothing() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+
+        Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE);
+        intent.setPackage(mContext.getPackageName());
+        AudioSharingReceiver audioSharingReceiver = getAudioSharingReceiver(intent);
+        audioSharingReceiver.onReceive(mContext, intent);
+
+        verifyNoInteractions(mNm);
+    }
+
+    @Test
+    public void broadcastReceiver_receiveAudioSharingStateChangeIntentOnState_showNotification() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+
+        Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE);
+        intent.setPackage(mContext.getPackageName());
+        intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, BROADCAST_STATE_ON);
+        AudioSharingReceiver audioSharingReceiver = getAudioSharingReceiver(intent);
+        audioSharingReceiver.onReceive(mContext, intent);
+
+        verify(mNm, times(1))
+                .notify(eq(R.drawable.ic_bt_le_audio_sharing), any(Notification.class));
+    }
+
+    @Test
+    public void
+            broadcastReceiver_receiveAudioSharingStateChangeIntentOffState_cancelNotification() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+
+        Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE);
+        intent.setPackage(mContext.getPackageName());
+        intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, BROADCAST_STATE_OFF);
+        AudioSharingReceiver audioSharingReceiver = getAudioSharingReceiver(intent);
+        audioSharingReceiver.onReceive(mContext, intent);
+
+        verify(mNm, times(1)).cancel(R.drawable.ic_bt_le_audio_sharing);
+    }
+
+    @Test
+    public void broadcastReceiver_receiveAudioSharingStopIntentFlagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+
+        Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STOP);
+        intent.setPackage(mContext.getPackageName());
+        AudioSharingReceiver audioSharingReceiver = getAudioSharingReceiver(intent);
+        audioSharingReceiver.onReceive(mContext, intent);
+
+        verifyNoInteractions(mBroadcast);
+    }
+
+    @Test
+    public void broadcastReceiver_receiveAudioSharingStopIntent_stopBroadcast() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        int broadcastId = 1;
+        when(mBroadcast.getLatestBroadcastId()).thenReturn(broadcastId);
+
+        Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STOP);
+        intent.setPackage(mContext.getPackageName());
+        AudioSharingReceiver audioSharingReceiver = getAudioSharingReceiver(intent);
+        audioSharingReceiver.onReceive(mContext, intent);
+
+        verify(mBroadcast, times(1)).stopBroadcast(broadcastId);
+    }
+
+    private AudioSharingReceiver getAudioSharingReceiver(Intent intent) {
+        assertThat(mShadowApplication.hasReceiverForIntent(intent)).isTrue();
+        List<BroadcastReceiver> receiversForIntent =
+                mShadowApplication.getReceiversForIntent(intent);
+        assertThat(receiversForIntent).hasSize(1);
+        BroadcastReceiver broadcastReceiver = receiversForIntent.get(0);
+        assertThat(broadcastReceiver).isInstanceOf(AudioSharingReceiver.class);
+        return (AudioSharingReceiver) broadcastReceiver;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java
new file mode 100644
index 0000000..84d7a31
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothStatusCodes;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settings.R;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.flags.Flags;
+
+import com.google.common.collect.ImmutableList;
+
+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 org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowAlertDialogCompat.class,
+            ShadowBluetoothAdapter.class,
+        })
+public class AudioSharingStopDialogFragmentTest {
+
+    @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private static final String TEST_DEVICE_NAME1 = "test1";
+    private static final String TEST_DEVICE_NAME2 = "test2";
+    private static final String TEST_DEVICE_NAME3 = "test3";
+    private static final int TEST_DEVICE_GROUP_ID1 = 1;
+    private static final int TEST_DEVICE_GROUP_ID2 = 2;
+    private static final int TEST_DEVICE_GROUP_ID3 = 3;
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM2 =
+            new AudioSharingDeviceItem(
+                    TEST_DEVICE_NAME2, TEST_DEVICE_GROUP_ID2, /* isActive= */ false);
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM3 =
+            new AudioSharingDeviceItem(
+                    TEST_DEVICE_NAME3, TEST_DEVICE_GROUP_ID3, /* isActive= */ false);
+
+    @Mock private CachedBluetoothDevice mCachedDevice1;
+    @Mock private CachedBluetoothDevice mCachedDevice2;
+    @Mock private BluetoothDevice mDevice1;
+    @Mock private BluetoothDevice mDevice2;
+    private Fragment mParent;
+    private AudioSharingStopDialogFragment mFragment;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+    @Before
+    public void setUp() {
+        AlertDialog latestAlertDialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        if (latestAlertDialog != null) {
+            latestAlertDialog.dismiss();
+            ShadowAlertDialogCompat.reset();
+        }
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
+        when(mCachedDevice1.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID1);
+        when(mCachedDevice1.getDevice()).thenReturn(mDevice1);
+        when(mCachedDevice2.getName()).thenReturn(TEST_DEVICE_NAME2);
+        when(mCachedDevice2.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID2);
+        when(mCachedDevice2.getDevice()).thenReturn(mDevice2);
+        mFragment = new AudioSharingStopDialogFragment();
+        mParent = new Fragment();
+        FragmentController.setupFragment(
+                mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+    }
+
+    @Test
+    public void onCreateDialog_flagOff_dialogNotExist() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNull();
+    }
+
+    @Test
+    public void onCreateDialog_oneDeviceInSharing_showDialogWithCorrectMessage() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, ImmutableList.of(TEST_DEVICE_ITEM2), mCachedDevice1, () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+        TextView view = dialog.findViewById(R.id.description_text);
+        assertThat(view.getText().toString())
+                .isEqualTo(
+                        mParent.getString(
+                                R.string.audio_sharing_stop_dialog_content, TEST_DEVICE_NAME2));
+    }
+
+    @Test
+    public void onCreateDialog_twoDeviceInSharing_showDialogWithCorrectMessage() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(
+                mParent,
+                ImmutableList.of(TEST_DEVICE_ITEM2, TEST_DEVICE_ITEM3),
+                mCachedDevice1,
+                () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+        TextView view = dialog.findViewById(R.id.description_text);
+        assertThat(view.getText().toString())
+                .isEqualTo(
+                        mParent.getString(
+                                R.string.audio_sharing_stop_dialog_with_two_content,
+                                TEST_DEVICE_NAME2,
+                                TEST_DEVICE_NAME3));
+    }
+
+    @Test
+    public void onCreateDialog_dialogIsShowingForSameDevice_updateDialog() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+        TextView view = dialog.findViewById(R.id.description_text);
+        assertThat(view.getText().toString())
+                .isEqualTo(mParent.getString(R.string.audio_sharing_stop_dialog_with_more_content));
+
+        // Update the content
+        AtomicBoolean isStopBtnClicked = new AtomicBoolean(false);
+        mFragment.show(
+                mParent, ImmutableList.of(), mCachedDevice1, () -> isStopBtnClicked.set(true));
+        shadowMainLooper().idle();
+        dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+
+        dialog.findViewById(android.R.id.button1).performClick();
+        shadowMainLooper().idle();
+        assertThat(dialog.isShowing()).isFalse();
+        assertThat(isStopBtnClicked.get()).isTrue();
+    }
+
+    @Test
+    public void onCreateDialog_dialogIsShowingForNewDevice_showNewDialog() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+        TextView view = dialog.findViewById(R.id.description_text);
+        assertThat(view.getText().toString())
+                .isEqualTo(mParent.getString(R.string.audio_sharing_stop_dialog_with_more_content));
+        TextView title = dialog.findViewById(R.id.title_text);
+        assertThat(title.getText().toString())
+                .isEqualTo(
+                        mParent.getString(
+                                R.string.audio_sharing_stop_dialog_title, TEST_DEVICE_NAME1));
+
+        // Show new dialog
+        mFragment.show(mParent, ImmutableList.of(), mCachedDevice2, () -> {});
+        shadowMainLooper().idle();
+        dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+        view = dialog.findViewById(R.id.description_text);
+        assertThat(view.getText().toString())
+                .isEqualTo(mParent.getString(R.string.audio_sharing_stop_dialog_with_more_content));
+        title = dialog.findViewById(R.id.title_text);
+        assertThat(title.getText().toString())
+                .isEqualTo(
+                        mParent.getString(
+                                R.string.audio_sharing_stop_dialog_title, TEST_DEVICE_NAME2));
+    }
+
+    @Test
+    public void onCreateDialog_clickCancel_dialogDismiss() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, ImmutableList.of(), mCachedDevice1, () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        dialog.findViewById(android.R.id.button2).performClick();
+        shadowMainLooper().idle();
+        assertThat(dialog.isShowing()).isFalse();
+    }
+
+    @Test
+    public void onCreateDialog_clickShare_callbackTriggered() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        AtomicBoolean isStopBtnClicked = new AtomicBoolean(false);
+        mFragment.show(
+                mParent, ImmutableList.of(), mCachedDevice1, () -> isStopBtnClicked.set(true));
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        dialog.findViewById(android.R.id.button1).performClick();
+        shadowMainLooper().idle();
+        assertThat(dialog.isShowing()).isFalse();
+        assertThat(isStopBtnClicked.get()).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java
new file mode 100644
index 0000000..0b8f121
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Looper;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.widget.CompoundButton;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settings.testutils.shadow.ShadowThreadUtils;
+import com.android.settings.widget.SettingsMainSwitchBar;
+import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.flags.Flags;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.concurrent.Executor;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+            ShadowThreadUtils.class,
+        })
+public class AudioSharingSwitchBarControllerTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Spy Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private CachedBluetoothDeviceManager mDeviceManager;
+    @Mock private LocalBluetoothProfileManager mBtProfileManager;
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private VolumeControlProfile mVolumeControl;
+    @Mock private CompoundButton mBtnView;
+    @Mock private CachedBluetoothDevice mCachedDevice;
+    @Mock private BluetoothDevice mDevice;
+    private SettingsMainSwitchBar mSwitchBar;
+    private AudioSharingSwitchBarController mController;
+    private AudioSharingSwitchBarController.OnAudioSharingStateChangedListener mListener;
+    private Lifecycle mLifecycle;
+    private LifecycleOwner mLifecycleOwner;
+    private boolean mOnAudioSharingStateChanged;
+    private boolean mOnAudioSharingServiceConnected;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private LocalBluetoothManager mLocalBluetoothManager;
+
+    @Before
+    public void setUp() {
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+        when(mLocalBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
+        when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mDeviceManager);
+        when(mDeviceManager.findDevice(mDevice)).thenReturn(mCachedDevice);
+        when(mCachedDevice.getDevice()).thenReturn(mDevice);
+        when(mCachedDevice.getGroupId()).thenReturn(1);
+        when(mCachedDevice.getName()).thenReturn("test");
+        when(mBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        when(mBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        when(mBtProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
+        when(mVolumeControl.isProfileReady()).thenReturn(true);
+        when(mBroadcast.isProfileReady()).thenReturn(true);
+        doNothing()
+                .when(mBroadcast)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
+        doNothing()
+                .when(mBroadcast)
+                .unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
+        when(mAssistant.isProfileReady()).thenReturn(true);
+        doNothing()
+                .when(mAssistant)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        doNothing()
+                .when(mAssistant)
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+        mSwitchBar = new SettingsMainSwitchBar(mContext);
+        mSwitchBar.setDisabledByAdmin(mock(RestrictedLockUtils.EnforcedAdmin.class));
+        mOnAudioSharingStateChanged = false;
+        mOnAudioSharingServiceConnected = false;
+        mListener =
+                new AudioSharingSwitchBarController.OnAudioSharingStateChangedListener() {
+                    @Override
+                    public void onAudioSharingStateChanged() {
+                        mOnAudioSharingStateChanged = true;
+                    }
+
+                    @Override
+                    public void onAudioSharingProfilesConnected() {
+                        mOnAudioSharingServiceConnected = true;
+                    }
+                };
+        mController = new AudioSharingSwitchBarController(mContext, mSwitchBar, mListener);
+    }
+
+    @Test
+    public void bluetoothOff_switchDisabled() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mSwitchBar.isEnabled()).isTrue();
+        mContext.registerReceiver(
+                mController.mReceiver,
+                mController.mIntentFilter,
+                Context.RECEIVER_EXPORTED_UNAUDITED);
+        mShadowBluetoothAdapter.setEnabled(false);
+        when(mBroadcast.isEnabled(null)).thenReturn(false);
+        Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED);
+        intent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
+        mContext.sendBroadcast(intent);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertThat(mSwitchBar.isEnabled()).isFalse();
+        assertThat(mSwitchBar.isChecked()).isFalse();
+        assertThat(mOnAudioSharingStateChanged).isTrue();
+        assertThat(mOnAudioSharingServiceConnected).isFalse();
+    }
+
+    @Test
+    public void onServiceConnected_switchEnabled() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isEnabled(null)).thenReturn(true);
+        mController.onServiceConnected();
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertThat(mSwitchBar.isEnabled()).isTrue();
+        assertThat(mSwitchBar.isChecked()).isTrue();
+        assertThat(mOnAudioSharingStateChanged).isTrue();
+        assertThat(mOnAudioSharingServiceConnected).isTrue();
+        verify(mBtProfileManager).removeServiceListener(mController);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOn() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOff() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+    }
+
+    @Test
+    public void onStart_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mContext, times(0))
+                .registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class), anyInt());
+        verify(mBroadcast, times(0))
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
+        verify(mAssistant, times(0))
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mBtProfileManager, times(0)).addServiceListener(mController);
+    }
+
+    @Test
+    public void onStart_flagOnProfileNotReady_registerProfileCallback() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isEnabled(null)).thenReturn(false);
+        when(mBroadcast.isProfileReady()).thenReturn(false);
+        mController.onStart(mLifecycleOwner);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mContext)
+                .registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class), anyInt());
+        verify(mBroadcast, times(0))
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
+        verify(mAssistant, times(0))
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mBtProfileManager).addServiceListener(mController);
+        assertThat(mSwitchBar.isChecked()).isFalse();
+        assertThat(mSwitchBar.isEnabled()).isFalse();
+    }
+
+    @Test
+    public void onStart_flagOn_registerCallback() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isEnabled(null)).thenReturn(true);
+        mController.onStart(mLifecycleOwner);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mContext)
+                .registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class), anyInt());
+        verify(mBroadcast)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
+        verify(mAssistant)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mBtProfileManager, times(0)).addServiceListener(mController);
+        assertThat(mSwitchBar.isChecked()).isTrue();
+        assertThat(mSwitchBar.isEnabled()).isTrue();
+    }
+
+    @Test
+    public void onStop_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStop(mLifecycleOwner);
+        verify(mContext, times(0)).unregisterReceiver(any(BroadcastReceiver.class));
+        verify(mBroadcast, times(0))
+                .unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
+        verify(mAssistant, times(0))
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+        verify(mBtProfileManager, times(0)).removeServiceListener(mController);
+    }
+
+    @Test
+    public void onStop_flagOn_notRegistered_doNothing() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(false);
+        doNothing().when(mContext).unregisterReceiver(any(BroadcastReceiver.class));
+        mController.onStop(mLifecycleOwner);
+
+        verify(mContext).unregisterReceiver(any(BroadcastReceiver.class));
+        verify(mBtProfileManager).removeServiceListener(mController);
+        verify(mBroadcast, times(0))
+                .unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
+        verify(mAssistant, times(0))
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+    }
+
+    @Test
+    public void onStop_flagOn_registered_unregisterCallback() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(true);
+        mContext.registerReceiver(
+                mController.mReceiver,
+                mController.mIntentFilter,
+                Context.RECEIVER_EXPORTED_UNAUDITED);
+        mController.onStop(mLifecycleOwner);
+        verify(mContext).unregisterReceiver(mController.mReceiver);
+        verify(mBtProfileManager).removeServiceListener(mController);
+        verify(mBroadcast).unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
+        verify(mAssistant)
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+    }
+
+    @Test
+    public void onCheckedChangedToChecked_sharing_doNothing() {
+        when(mBtnView.isEnabled()).thenReturn(true);
+        when(mBroadcast.isEnabled(null)).thenReturn(true);
+        mController.onCheckedChanged(mBtnView, /* isChecked= */ true);
+        verify(mBroadcast, times(0)).startPrivateBroadcast();
+    }
+
+    @Test
+    public void onCheckedChangedToChecked_noConnectedLeaDevices_notStartAudioSharing() {
+        when(mBtnView.isEnabled()).thenReturn(true);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(ImmutableList.of());
+        doNothing().when(mBroadcast).startPrivateBroadcast();
+        mController.onCheckedChanged(mBtnView, /* isChecked= */ true);
+        assertThat(mSwitchBar.isChecked()).isFalse();
+        verify(mBroadcast, times(0)).startPrivateBroadcast();
+    }
+
+    @Test
+    public void onCheckedChangedToChecked_notSharing_withConnectedLeaDevices_startAudioSharing() {
+        when(mBtnView.isEnabled()).thenReturn(true);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(ImmutableList.of(mDevice));
+        doNothing().when(mBroadcast).startPrivateBroadcast();
+        mController.onCheckedChanged(mBtnView, /* isChecked= */ true);
+        verify(mBroadcast).startPrivateBroadcast();
+    }
+
+    @Test
+    public void onCheckedChangedToUnChecked_notSharing_doNothing() {
+        when(mBtnView.isEnabled()).thenReturn(true);
+        when(mBroadcast.isEnabled(null)).thenReturn(false);
+        when(mBroadcast.getLatestBroadcastId()).thenReturn(1);
+        mController.onCheckedChanged(mBtnView, /* isChecked= */ false);
+        verify(mBroadcast, times(0)).stopBroadcast(anyInt());
+    }
+
+    @Test
+    public void onCheckedChangedToUnChecked_sharing_stopAudioSharing() {
+        when(mBtnView.isEnabled()).thenReturn(true);
+        when(mBroadcast.isEnabled(null)).thenReturn(true);
+        when(mBroadcast.getLatestBroadcastId()).thenReturn(1);
+        doNothing().when(mBroadcast).stopBroadcast(anyInt());
+        mController.onCheckedChanged(mBtnView, /* isChecked= */ false);
+        verify(mBroadcast).stopBroadcast(1);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragmentTest.java
new file mode 100644
index 0000000..53e0d71
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragmentTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+import java.util.ArrayList;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowAlertDialogCompat.class,
+            ShadowBluetoothAdapter.class,
+        })
+public class CallsAndAlarmsDialogFragmentTest {
+    @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private static final String TEST_DEVICE_NAME1 = "test1";
+    private static final String TEST_DEVICE_NAME2 = "test2";
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM1 =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME1, /* groupId= */ 1, /* isActive= */ true);
+
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM2 =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME2, /* groupId= */ 1, /* isActive= */ true);
+
+    private Fragment mParent;
+    private CallsAndAlarmsDialogFragment mFragment;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+    @Before
+    public void setUp() {
+        ShadowAlertDialogCompat.reset();
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mFragment = new CallsAndAlarmsDialogFragment();
+        mParent = new Fragment();
+        FragmentController.setupFragment(
+                mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+    }
+
+    @Test
+    public void onCreateDialog_flagOff_dialogNotExist() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNull();
+    }
+
+    @Test
+    public void onCreateDialog_showCorrectItems() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        ArrayList<AudioSharingDeviceItem> deviceItemList = new ArrayList<>();
+        deviceItemList.add(TEST_DEVICE_ITEM1);
+        deviceItemList.add(TEST_DEVICE_ITEM2);
+        mFragment.show(mParent, deviceItemList, (item) -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog.getListView().getCount()).isEqualTo(2);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceControllerTest.java
new file mode 100644
index 0000000..614cb5b
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceControllerTest.java
@@ -0,0 +1,381 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID;
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastAssistant;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Looper;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settings.testutils.shadow.ShadowThreadUtils;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.flags.Flags;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+            ShadowThreadUtils.class,
+        })
+public class CallsAndAlarmsPreferenceControllerTest {
+    private static final String PREF_KEY = "calls_and_alarms";
+    private static final String TEST_DEVICE_NAME1 = "test1";
+    private static final String TEST_DEVICE_NAME2 = "test2";
+    private static final int TEST_DEVICE_GROUP_ID1 = 1;
+    private static final int TEST_DEVICE_GROUP_ID2 = 2;
+
+    private static final String TEST_SETTINGS_KEY =
+            "bluetooth_le_broadcast_fallback_active_group_id";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Spy Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private PreferenceScreen mScreen;
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private BluetoothEventManager mBtEventManager;
+    @Mock private LocalBluetoothProfileManager mBtProfileManager;
+    @Mock private CachedBluetoothDeviceManager mCacheManager;
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private VolumeControlProfile mVolumeControl;
+    @Mock private BluetoothDevice mDevice1;
+    @Mock private BluetoothDevice mDevice2;
+    @Mock private BluetoothDevice mDevice3;
+    @Mock private CachedBluetoothDevice mCachedDevice1;
+    @Mock private CachedBluetoothDevice mCachedDevice2;
+    @Mock private CachedBluetoothDevice mCachedDevice3;
+    @Mock private BluetoothLeBroadcastReceiveState mState;
+    @Mock private ContentResolver mContentResolver;
+    private CallsAndAlarmsPreferenceController mController;
+    @Spy private ContentObserver mContentObserver;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private LocalBluetoothManager mBtManager;
+    private Lifecycle mLifecycle;
+    private LifecycleOwner mLifecycleOwner;
+    private Preference mPreference;
+
+    @Before
+    public void setUp() {
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mBtManager = Utils.getLocalBtManager(mContext);
+        when(mBtManager.getEventManager()).thenReturn(mBtEventManager);
+        when(mBtManager.getProfileManager()).thenReturn(mBtProfileManager);
+        when(mBtManager.getCachedDeviceManager()).thenReturn(mCacheManager);
+        when(mBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        when(mBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        when(mBtProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
+        when(mBroadcast.isProfileReady()).thenReturn(true);
+        when(mAssistant.isProfileReady()).thenReturn(true);
+        when(mVolumeControl.isProfileReady()).thenReturn(true);
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(mState.getBisSyncState()).thenReturn(bisSyncState);
+        when(mContext.getContentResolver()).thenReturn(mContentResolver);
+        mController = new CallsAndAlarmsPreferenceController(mContext);
+        mController.init(null);
+        mContentObserver = mController.getSettingsObserver();
+        mPreference = new Preference(mContext);
+        when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference);
+    }
+
+    @Test
+    public void onStart_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mBtEventManager, times(0)).registerCallback(mController);
+        verify(mContentResolver, times(0))
+                .registerContentObserver(
+                        Settings.Secure.getUriFor(SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID),
+                        false,
+                        mContentObserver);
+        verify(mAssistant, times(0))
+                .registerServiceCallBack(any(), any(BluetoothLeBroadcastAssistant.Callback.class));
+    }
+
+    @Test
+    public void onStart_flagOn_registerCallback() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mBtEventManager).registerCallback(mController);
+        verify(mContentResolver)
+                .registerContentObserver(
+                        Settings.Secure.getUriFor(SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID),
+                        false,
+                        mContentObserver);
+        verify(mAssistant)
+                .registerServiceCallBack(any(), any(BluetoothLeBroadcastAssistant.Callback.class));
+    }
+
+    @Test
+    public void onStop_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(true);
+        mController.onStop(mLifecycleOwner);
+        verify(mBtEventManager, times(0)).unregisterCallback(mController);
+        verify(mContentResolver, times(0)).unregisterContentObserver(mContentObserver);
+        verify(mAssistant, times(0))
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+    }
+
+    @Test
+    public void onStop_flagOn_notRegistered_doNothing() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(false);
+        mController.onStop(mLifecycleOwner);
+        verify(mBtEventManager, times(0)).unregisterCallback(mController);
+        verify(mContentResolver, times(0)).unregisterContentObserver(mContentObserver);
+        verify(mAssistant, times(0))
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+    }
+
+    @Test
+    public void onStop_flagOn_registered_unregisterCallback() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.setCallbacksRegistered(true);
+        mController.onStop(mLifecycleOwner);
+        verify(mBtEventManager).unregisterCallback(mController);
+        verify(mContentResolver).unregisterContentObserver(mContentObserver);
+        verify(mAssistant)
+                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOn() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOff() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+    }
+
+    @Test
+    public void updateVisibility_flagOff_invisible() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        mController.displayPreference(mScreen);
+        mController.updateVisibility();
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void updateVisibility_broadcastOffBluetoothOff_invisible() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isEnabled(any())).thenReturn(false);
+        mShadowBluetoothAdapter.setEnabled(false);
+        mController.displayPreference(mScreen);
+        mController.updateVisibility();
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void updateVisibility_broadcastOnBluetoothOff_invisible() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        mShadowBluetoothAdapter.setEnabled(false);
+        mController.displayPreference(mScreen);
+        mController.updateVisibility();
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void updateVisibility_broadcastOffBluetoothOn_invisible() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isEnabled(any())).thenReturn(false);
+        mController.displayPreference(mScreen);
+        mController.updateVisibility();
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void updateVisibility_broadcastOnBluetoothOn_visible() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        mController.displayPreference(mScreen);
+        mController.updateVisibility();
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_noDeviceInSharing_updateSummary() {
+        Settings.Secure.putInt(mContentResolver, TEST_SETTINGS_KEY, TEST_DEVICE_GROUP_ID1);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(ImmutableList.of());
+        mController.displayPreference(mScreen);
+        mPreference.setSummary("test");
+        mController.onProfileConnectionStateChanged(
+                mCachedDevice1,
+                BluetoothAdapter.STATE_DISCONNECTED,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.getSummary().toString()).isEmpty();
+    }
+
+    @Test
+    public void onFallbackDeviceChanged_updateSummary() {
+        Settings.Secure.putInt(mContentResolver, TEST_SETTINGS_KEY, TEST_DEVICE_GROUP_ID1);
+        when(mCachedDevice1.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID1);
+        when(mCachedDevice1.getDevice()).thenReturn(mDevice1);
+        when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
+        when(mCacheManager.findDevice(mDevice1)).thenReturn(mCachedDevice1);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(ImmutableList.of(mDevice1));
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mState));
+        mController.displayPreference(mScreen);
+        mContentObserver.onChange(true);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.getSummary().toString())
+                .isEqualTo(
+                        mContext.getString(
+                                R.string.audio_sharing_call_audio_description, TEST_DEVICE_NAME1));
+    }
+
+    @Test
+    public void displayPreference_showCorrectSummary() {
+        Settings.Secure.putInt(mContentResolver, TEST_SETTINGS_KEY, TEST_DEVICE_GROUP_ID1);
+        when(mCachedDevice1.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID1);
+        when(mCachedDevice1.getDevice()).thenReturn(mDevice1);
+        when(mCachedDevice2.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID1);
+        when(mCachedDevice2.getDevice()).thenReturn(mDevice2);
+        when(mCachedDevice1.getMemberDevice()).thenReturn(ImmutableSet.of(mCachedDevice2));
+        when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
+        when(mCachedDevice3.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID2);
+        when(mCachedDevice3.getDevice()).thenReturn(mDevice3);
+        when(mCachedDevice3.getName()).thenReturn(TEST_DEVICE_NAME2);
+        when(mCacheManager.findDevice(mDevice1)).thenReturn(mCachedDevice1);
+        when(mCacheManager.findDevice(mDevice2)).thenReturn(mCachedDevice2);
+        when(mCacheManager.findDevice(mDevice3)).thenReturn(mCachedDevice3);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(ImmutableList.of(mDevice1, mDevice2, mDevice3));
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mState));
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.getSummary().toString())
+                .isEqualTo(
+                        mContext.getString(
+                                R.string.audio_sharing_call_audio_description, TEST_DEVICE_NAME1));
+    }
+
+    @Test
+    public void displayPreference_noFallbackDeviceInSharing_showEmptySummary() {
+        Settings.Secure.putInt(mContentResolver, TEST_SETTINGS_KEY, TEST_DEVICE_GROUP_ID2);
+        when(mCachedDevice1.getGroupId()).thenReturn(TEST_DEVICE_GROUP_ID1);
+        when(mCachedDevice1.getDevice()).thenReturn(mDevice1);
+        when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1);
+        when(mCacheManager.findDevice(mDevice1)).thenReturn(mCachedDevice1);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(ImmutableList.of(mDevice1));
+        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mState));
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.getSummary().toString()).isEmpty();
+    }
+
+    @Test
+    public void displayPreference_noFallbackDevice_showEmptySummary() {
+        Settings.Secure.putInt(
+                mContentResolver, TEST_SETTINGS_KEY, BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        when(mAssistant.getDevicesMatchingConnectionStates(
+                        new int[] {BluetoothProfile.STATE_CONNECTED}))
+                .thenReturn(ImmutableList.of());
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.getSummary().toString()).isEmpty();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/StreamSettingsCategoryControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/StreamSettingsCategoryControllerTest.java
new file mode 100644
index 0000000..50dde0f
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/StreamSettingsCategoryControllerTest.java
@@ -0,0 +1,247 @@
+/*
+ * 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.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Looper;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+        })
+public class StreamSettingsCategoryControllerTest {
+    private static final String KEY = "audio_sharing_stream_settings_category";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Spy Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private LocalBluetoothProfileManager mBtProfileManager;
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private VolumeControlProfile mVolumeControl;
+    @Mock private PreferenceScreen mScreen;
+
+    private StreamSettingsCategoryController mController;
+    private Lifecycle mLifecycle;
+    private LifecycleOwner mLifecycleOwner;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private LocalBluetoothManager mLocalBluetoothManager;
+    private Preference mPreference;
+
+    @Before
+    public void setUp() {
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
+        when(mLocalBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
+        when(mBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        when(mBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        when(mBtProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
+        when(mBroadcast.isProfileReady()).thenReturn(true);
+        when(mAssistant.isProfileReady()).thenReturn(true);
+        when(mVolumeControl.isProfileReady()).thenReturn(true);
+        mController = new StreamSettingsCategoryController(mContext, KEY);
+        mPreference = new Preference(mContext);
+        when(mScreen.findPreference(KEY)).thenReturn(mPreference);
+    }
+
+    @Test
+    public void bluetoothOff_updateVisibility() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mContext.registerReceiver(
+                mController.mReceiver,
+                mController.mIntentFilter,
+                Context.RECEIVER_EXPORTED_UNAUDITED);
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isTrue();
+
+        mShadowBluetoothAdapter.setEnabled(false);
+        Intent intent = new Intent(BluetoothAdapter.ACTION_STATE_CHANGED);
+        intent.putExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF);
+        mContext.sendBroadcast(intent);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOn() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOff() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+    }
+
+    @Test
+    public void onStart_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mContext, times(0))
+                .registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class), anyInt());
+        verify(mBtProfileManager, times(0)).addServiceListener(mController);
+    }
+
+    @Test
+    public void onStart_flagOn_registerCallback() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mContext)
+                .registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class), anyInt());
+        verify(mBtProfileManager, times(0)).addServiceListener(mController);
+    }
+
+    @Test
+    public void onStart_flagOnProfileNotReady_registerProfileManagerCallback() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isProfileReady()).thenReturn(false);
+        mController.onStart(mLifecycleOwner);
+        verify(mContext)
+                .registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class), anyInt());
+        verify(mBtProfileManager).addServiceListener(mController);
+    }
+
+    @Test
+    public void onStop_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStop(mLifecycleOwner);
+        verify(mContext, times(0)).unregisterReceiver(any(BroadcastReceiver.class));
+        verify(mBtProfileManager, times(0)).removeServiceListener(mController);
+    }
+
+    @Test
+    public void onStop_flagOn_unregisterCallback() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        doNothing().when(mContext).unregisterReceiver(any(BroadcastReceiver.class));
+        mController.onStop(mLifecycleOwner);
+        verify(mContext).unregisterReceiver(any(BroadcastReceiver.class));
+        verify(mBtProfileManager).removeServiceListener(mController);
+    }
+
+    @Test
+    public void displayPreference_flagOff_preferenceInvisible() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mPreference.setVisible(true);
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void displayPreference_BluetoothOff_preferenceInvisible() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mPreference.setVisible(true);
+        mShadowBluetoothAdapter.setEnabled(false);
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void displayPreference_BluetoothOnProfileNotReady_preferenceInvisible() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mPreference.setVisible(true);
+        when(mBroadcast.isProfileReady()).thenReturn(false);
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isFalse();
+    }
+
+    @Test
+    public void displayPreference_BluetoothOnProfileReady_preferenceVisible() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mPreference.setVisible(false);
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+
+    @Test
+    public void onServiceConnected_updateVisibility() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isProfileReady()).thenReturn(false);
+        mController.displayPreference(mScreen);
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isFalse();
+
+        when(mBroadcast.isProfileReady()).thenReturn(true);
+        mController.onServiceConnected();
+        shadowOf(Looper.getMainLooper()).idle();
+        assertThat(mPreference.isVisible()).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java
new file mode 100644
index 0000000..2fddff5
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AddSourceBadCodeState.AUDIO_STREAM_ADD_SOURCE_BAD_CODE_STATE_SUMMARY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AddSourceBadCodeStateTest {
+    private AddSourceBadCodeState mInstance;
+
+    @Before
+    public void setUp() {
+        mInstance = AddSourceBadCodeState.getInstance();
+    }
+
+    @Test
+    public void testGetInstance() {
+        assertThat(mInstance).isNotNull();
+        assertThat(mInstance).isInstanceOf(SyncedState.class);
+    }
+
+    @Test
+    public void testGetSummary() {
+        int summary = mInstance.getSummary();
+        assertThat(summary).isEqualTo(AUDIO_STREAM_ADD_SOURCE_BAD_CODE_STATE_SUMMARY);
+    }
+
+    @Test
+    public void testGetStateEnum() {
+        AudioStreamsProgressCategoryController.AudioStreamState stateEnum =
+                mInstance.getStateEnum();
+        assertThat(stateEnum)
+                .isEqualTo(
+                        AudioStreamsProgressCategoryController.AudioStreamState
+                                .ADD_SOURCE_BAD_CODE);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java
new file mode 100644
index 0000000..d8b1fcf
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AddSourceFailedState.AUDIO_STREAM_ADD_SOURCE_FAILED_STATE_SUMMARY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AddSourceFailedStateTest {
+    private AddSourceFailedState mInstance;
+
+    @Before
+    public void setUp() {
+        mInstance = AddSourceFailedState.getInstance();
+    }
+
+    @Test
+    public void testGetInstance() {
+        assertThat(mInstance).isNotNull();
+        assertThat(mInstance).isInstanceOf(SyncedState.class);
+    }
+
+    @Test
+    public void testGetSummary() {
+        int summary = mInstance.getSummary();
+        assertThat(summary).isEqualTo(AUDIO_STREAM_ADD_SOURCE_FAILED_STATE_SUMMARY);
+    }
+
+    @Test
+    public void testGetStateEnum() {
+        AudioStreamsProgressCategoryController.AudioStreamState stateEnum =
+                mInstance.getStateEnum();
+        assertThat(stateEnum)
+                .isEqualTo(
+                        AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java
new file mode 100644
index 0000000..00357b4
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AddSourceWaitForResponseState.ADD_SOURCE_WAIT_FOR_RESPONSE_TIMEOUT_MILLIS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+
+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 org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowLooper;
+
+import java.util.concurrent.TimeUnit;
+
+@RunWith(RobolectricTestRunner.class)
+public class AddSourceWaitForResponseStateTest {
+    private static final int BROADCAST_ID = 1;
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Mock private AudioStreamPreference mMockPreference;
+    @Mock private AudioStreamsProgressCategoryController mMockController;
+    @Mock private AudioStreamsHelper mMockHelper;
+    @Mock private BluetoothLeBroadcastMetadata mMockMetadata;
+    private AddSourceWaitForResponseState mInstance;
+
+    @Before
+    public void setUp() {
+        mInstance = AddSourceWaitForResponseState.getInstance();
+    }
+
+    @Test
+    public void testGetInstance() {
+        assertThat(mInstance).isNotNull();
+        assertThat(mInstance).isInstanceOf(AudioStreamStateHandler.class);
+    }
+
+    @Test
+    public void testGetSummary() {
+        int summary = mInstance.getSummary();
+        assertThat(summary)
+                .isEqualTo(
+                        AddSourceWaitForResponseState
+                                .AUDIO_STREAM_ADD_SOURCE_WAIT_FOR_RESPONSE_STATE_SUMMARY);
+    }
+
+    @Test
+    public void testGetStateEnum() {
+        AudioStreamsProgressCategoryController.AudioStreamState stateEnum =
+                mInstance.getStateEnum();
+        assertThat(stateEnum)
+                .isEqualTo(
+                        AudioStreamsProgressCategoryController.AudioStreamState
+                                .ADD_SOURCE_WAIT_FOR_RESPONSE);
+    }
+
+    @Test
+    public void testPerformAction_metadataIsNull_doNothing() {
+        when(mMockPreference.getAudioStreamMetadata()).thenReturn(null);
+
+        mInstance.performAction(mMockPreference, mMockController, mMockHelper);
+
+        verify(mMockHelper, never()).addSource(any());
+    }
+
+    @Test
+    public void testPerformAction_metadataIsNotNull_addSource() {
+        when(mMockPreference.getAudioStreamMetadata()).thenReturn(mMockMetadata);
+
+        mInstance.performAction(mMockPreference, mMockController, mMockHelper);
+
+        verify(mMockHelper).addSource(mMockMetadata);
+        verify(mMockController, never()).handleSourceFailedToConnect(anyInt());
+    }
+
+    @Test
+    public void testPerformAction_timeout_addSource_sourceFailedToConnect() {
+        when(mMockPreference.getAudioStreamMetadata()).thenReturn(mMockMetadata);
+        when(mMockPreference.isShown()).thenReturn(true);
+        when(mMockPreference.getAudioStreamState()).thenReturn(mInstance.getStateEnum());
+        when(mMockPreference.getAudioStreamBroadcastId()).thenReturn(BROADCAST_ID);
+
+        mInstance.performAction(mMockPreference, mMockController, mMockHelper);
+        ShadowLooper.idleMainLooper(ADD_SOURCE_WAIT_FOR_RESPONSE_TIMEOUT_MILLIS, TimeUnit.SECONDS);
+
+        verify(mMockHelper).addSource(mMockMetadata);
+        verify(mMockController).handleSourceFailedToConnect(BROADCAST_ID);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java
new file mode 100644
index 0000000..adcc617
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamButtonControllerTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+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.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.content.Context;
+import android.view.View;
+
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper;
+import com.android.settings.testutils.shadow.ShadowThreadUtils;
+import com.android.settingslib.widget.ActionButtonsPreference;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowThreadUtils.class,
+            ShadowAudioStreamsHelper.class,
+        })
+public class AudioStreamButtonControllerTest {
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private static final String KEY = "audio_stream_button";
+    private static final int BROADCAST_ID = 1;
+    @Spy Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private AudioStreamsHelper mAudioStreamsHelper;
+    @Mock private PreferenceScreen mScreen;
+    @Mock private BluetoothLeBroadcastReceiveState mBroadcastReceiveState;
+    @Mock private ActionButtonsPreference mPreference;
+    private AudioStreamButtonController mController;
+
+    @Before
+    public void setUp() {
+        ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
+        mController = new AudioStreamButtonController(mContext, KEY);
+        mController.init(BROADCAST_ID);
+        when(mScreen.findPreference(KEY)).thenReturn(mPreference);
+        when(mPreference.getContext()).thenReturn(mContext);
+        when(mPreference.setButton1Text(anyInt())).thenReturn(mPreference);
+        when(mPreference.setButton1Icon(anyInt())).thenReturn(mPreference);
+        when(mPreference.setButton1Enabled(anyBoolean())).thenReturn(mPreference);
+        when(mPreference.setButton1OnClickListener(any(View.OnClickListener.class)))
+                .thenReturn(mPreference);
+    }
+
+    @Test
+    public void testDisplayPreference_sourceConnected_setDisconnectButton() {
+        when(mAudioStreamsHelper.getAllConnectedSources())
+                .thenReturn(List.of(mBroadcastReceiveState));
+        when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
+
+        mController.displayPreference(mScreen);
+
+        verify(mPreference).setButton1Enabled(true);
+        verify(mPreference).setButton1Text(R.string.audio_streams_disconnect);
+        verify(mPreference).setButton1Icon(com.android.settings.R.drawable.ic_settings_close);
+        verify(mPreference).setButton1OnClickListener(any(View.OnClickListener.class));
+    }
+
+    @Test
+    public void testDisplayPreference_sourceNotConnected_setConnectButton() {
+        when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
+
+        mController.displayPreference(mScreen);
+
+        verify(mPreference).setButton1Enabled(true);
+        verify(mPreference).setButton1Text(R.string.audio_streams_connect);
+        verify(mPreference).setButton1Icon(com.android.settings.R.drawable.ic_add_24dp);
+        verify(mPreference).setButton1OnClickListener(any(View.OnClickListener.class));
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java
new file mode 100644
index 0000000..0af9c17
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamHeaderControllerTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController.AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.content.Context;
+
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowEntityHeaderController;
+import com.android.settings.testutils.shadow.ShadowThreadUtils;
+import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.widget.LayoutPreference;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowEntityHeaderController.class,
+            ShadowThreadUtils.class,
+            ShadowAudioStreamsHelper.class,
+        })
+public class AudioStreamHeaderControllerTest {
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private static final String KEY = "audio_stream_header";
+    private static final int BROADCAST_ID = 1;
+    private static final String BROADCAST_NAME = "broadcast name";
+    @Spy Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private AudioStreamsHelper mAudioStreamsHelper;
+    @Mock private PreferenceScreen mScreen;
+    @Mock private BluetoothLeBroadcastReceiveState mBroadcastReceiveState;
+    @Mock private AudioStreamDetailsFragment mFragment;
+    @Mock private LayoutPreference mPreference;
+    @Mock private EntityHeaderController mHeaderController;
+    private AudioStreamHeaderController mController;
+
+    @Before
+    public void setUp() {
+        ShadowEntityHeaderController.setUseMock(mHeaderController);
+        ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
+        mController = new AudioStreamHeaderController(mContext, KEY);
+        mController.init(mFragment, BROADCAST_NAME, BROADCAST_ID);
+        when(mScreen.findPreference(KEY)).thenReturn(mPreference);
+        when(mScreen.getContext()).thenReturn(mContext);
+        when(mPreference.getContext()).thenReturn(mContext);
+    }
+
+    @Test
+    public void testDisplayPreference_sourceConnected_setSummary() {
+        when(mAudioStreamsHelper.getAllConnectedSources())
+                .thenReturn(List.of(mBroadcastReceiveState));
+        when(mBroadcastReceiveState.getBroadcastId()).thenReturn(BROADCAST_ID);
+
+        mController.displayPreference(mScreen);
+
+        verify(mHeaderController).setLabel(BROADCAST_NAME);
+        verify(mHeaderController)
+                .setSummary(mContext.getString(AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY));
+        verify(mHeaderController).done(true);
+    }
+
+    @Test
+    public void testDisplayPreference_sourceNotConnected_setSummary() {
+        when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(Collections.emptyList());
+
+        mController.displayPreference(mScreen);
+
+        verify(mHeaderController).setLabel(BROADCAST_NAME);
+        verify(mHeaderController).setSummary(AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY);
+        verify(mHeaderController).done(true);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/MediaControlHelperTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/MediaControlHelperTest.java
new file mode 100644
index 0000000..113fc72
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/MediaControlHelperTest.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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static org.mockito.Mockito.*;
+
+import android.content.Context;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+
+import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowLocalMediaManager;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.media.BluetoothMediaDevice;
+import com.android.settingslib.media.LocalMediaManager;
+
+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 org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowAudioStreamsHelper.class,
+            ShadowLocalMediaManager.class,
+        })
+public class MediaControlHelperTest {
+    private static final String FAKE_PACKAGE = "fake_package";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private Context mContext;
+    @Mock private LocalBluetoothManager mLocalBluetoothManager;
+    @Mock private MediaSessionManager mMediaSessionManager;
+    @Mock private MediaController mMediaController;
+    @Mock private CachedBluetoothDevice mCachedBluetoothDevice;
+    @Mock private PlaybackState mPlaybackState;
+    @Mock private LocalMediaManager mLocalMediaManager;
+    @Mock private BluetoothMediaDevice mBluetoothMediaDevice;
+
+    @Before
+    public void setUp() {
+        mContext = spy(RuntimeEnvironment.application);
+        when(mContext.getSystemService(MediaSessionManager.class)).thenReturn(mMediaSessionManager);
+        when(mMediaSessionManager.getActiveSessions(any())).thenReturn(List.of(mMediaController));
+        when(mMediaController.getPackageName()).thenReturn(FAKE_PACKAGE);
+        when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState);
+        ShadowAudioStreamsHelper.resetCachedBluetoothDevice();
+        ShadowLocalMediaManager.setUseMock(mLocalMediaManager);
+    }
+
+    @Test
+    public void testStart_noBluetoothManager_doNothing() {
+        MediaControlHelper helper = new MediaControlHelper(mContext, null);
+        helper.start();
+
+        verify(mLocalMediaManager, never()).startScan();
+    }
+
+    @Test
+    public void testStart_noConnectedDevice_doNothing() {
+        MediaControlHelper helper = new MediaControlHelper(mContext, mLocalBluetoothManager);
+        helper.start();
+
+        verify(mLocalMediaManager, never()).startScan();
+    }
+
+    @Test
+    public void testStart_isStopped_onDeviceListUpdate_shouldNotStopMedia() {
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(
+                mCachedBluetoothDevice);
+        when(mPlaybackState.getState()).thenReturn(PlaybackState.STATE_STOPPED);
+        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(null);
+
+        MediaControlHelper helper = new MediaControlHelper(mContext, mLocalBluetoothManager);
+        helper.start();
+        ShadowLocalMediaManager.onDeviceListUpdate();
+
+        verify(mMediaController, never()).getTransportControls();
+    }
+
+    @Test
+    public void testStart_isPlaying_onDeviceListUpdate_noDevice_shouldNotStopMedia() {
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(
+                mCachedBluetoothDevice);
+        when(mPlaybackState.getState()).thenReturn(PlaybackState.STATE_PLAYING);
+        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(null);
+
+        MediaControlHelper helper = new MediaControlHelper(mContext, mLocalBluetoothManager);
+        helper.start();
+        ShadowLocalMediaManager.onDeviceListUpdate();
+
+        verify(mMediaController, never()).getTransportControls();
+    }
+
+    @Test
+    public void testStart_isPlaying_onDeviceListUpdate_deviceMatch_shouldStopMedia() {
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(
+                mCachedBluetoothDevice);
+        when(mPlaybackState.getState()).thenReturn(PlaybackState.STATE_PLAYING);
+        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mBluetoothMediaDevice);
+        when(mBluetoothMediaDevice.getCachedDevice()).thenReturn(mCachedBluetoothDevice);
+
+        MediaControlHelper helper = new MediaControlHelper(mContext, mLocalBluetoothManager);
+        helper.start();
+        ShadowLocalMediaManager.onDeviceListUpdate();
+
+        verify(mMediaController).getTransportControls();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java
new file mode 100644
index 0000000..0f0bafe
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.SourceAddedState.AUDIO_STREAM_SOURCE_ADDED_STATE_SUMMARY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class SourceAddedStateTest {
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private SourceAddedState mInstance;
+
+    @Before
+    public void setUp() {
+        mInstance = SourceAddedState.getInstance();
+    }
+
+    @Test
+    public void testGetInstance() {
+        assertThat(mInstance).isNotNull();
+        assertThat(mInstance).isInstanceOf(SourceAddedState.class);
+    }
+
+    @Test
+    public void testGetSummary() {
+        int summary = mInstance.getSummary();
+        assertThat(summary).isEqualTo(AUDIO_STREAM_SOURCE_ADDED_STATE_SUMMARY);
+    }
+
+    @Test
+    public void testGetStateEnum() {
+        AudioStreamsProgressCategoryController.AudioStreamState stateEnum =
+                mInstance.getStateEnum();
+        assertThat(stateEnum)
+                .isEqualTo(AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SyncedStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SyncedStateTest.java
new file mode 100644
index 0000000..64e1bc4
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SyncedStateTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamStateHandler.EMPTY_STRING_RES;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
+
+import android.app.AlertDialog;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+
+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 org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowAlertDialog;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowAlertDialog.class,
+        })
+public class SyncedStateTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Mock private AudioStreamsProgressCategoryController mMockController;
+    @Mock private AudioStreamPreference mMockPreference;
+    @Mock private BluetoothLeBroadcastMetadata mMockMetadata;
+    private Context mMockContext;
+    private SyncedState mInstance;
+
+    @Before
+    public void setUp() {
+        ShadowAlertDialog.reset();
+        mMockContext = spy(ApplicationProvider.getApplicationContext());
+        mInstance = SyncedState.getInstance();
+    }
+
+    @Test
+    public void testGetInstance() {
+        assertThat(mInstance).isNotNull();
+        assertThat(mInstance).isInstanceOf(AudioStreamStateHandler.class);
+    }
+
+    @Test
+    public void testGetSummary() {
+        int summary = mInstance.getSummary();
+        assertThat(summary).isEqualTo(EMPTY_STRING_RES);
+    }
+
+    @Test
+    public void testGetStateEnum() {
+        AudioStreamsProgressCategoryController.AudioStreamState stateEnum =
+                mInstance.getStateEnum();
+        assertThat(stateEnum)
+                .isEqualTo(AudioStreamsProgressCategoryController.AudioStreamState.SYNCED);
+    }
+
+    @Test
+    public void testGetOnClickListener_isNotEncrypted_handleSourceAddRequest() {
+        Preference.OnPreferenceClickListener listener =
+                mInstance.getOnClickListener(mMockController);
+        when(mMockPreference.getAudioStreamMetadata()).thenReturn(mMockMetadata);
+
+        listener.onPreferenceClick(mMockPreference);
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
+        assertThat(dialog).isNull();
+        verify(mMockController).handleSourceAddRequest(mMockPreference, mMockMetadata);
+    }
+
+    @Test
+    public void testGetOnClickListener_isEncrypted_passwordDialogShowing() {
+        Preference.OnPreferenceClickListener listener =
+                mInstance.getOnClickListener(mMockController);
+        when(mMockPreference.getAudioStreamMetadata()).thenReturn(mMockMetadata);
+        when(mMockPreference.getContext()).thenReturn(mMockContext);
+        when(mMockMetadata.isEncrypted()).thenReturn(true);
+
+        listener.onPreferenceClick(mMockPreference);
+        shadowMainLooper().idle();
+
+        AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+        verify(mMockController, never()).handleSourceAddRequest(mMockPreference, mMockMetadata);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java
new file mode 100644
index 0000000..5297182
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.WaitForSyncState.AUDIO_STREAM_WAIT_FOR_SYNC_STATE_SUMMARY;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.WaitForSyncState.WAIT_FOR_SYNC_TIMEOUT_MILLIS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+
+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 org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowLooper;
+
+import java.util.concurrent.TimeUnit;
+
+@RunWith(RobolectricTestRunner.class)
+public class WaitForSyncStateTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Mock private AudioStreamPreference mMockPreference;
+    @Mock private AudioStreamsProgressCategoryController mMockController;
+    @Mock private AudioStreamsHelper mMockHelper;
+    @Mock private BluetoothLeBroadcastMetadata mMockMetadata;
+    private WaitForSyncState mInstance;
+
+    @Before
+    public void setUp() {
+        mInstance = WaitForSyncState.getInstance();
+    }
+
+    @Test
+    public void testGetInstance() {
+        assertThat(mInstance).isNotNull();
+        assertThat(mInstance).isInstanceOf(AudioStreamStateHandler.class);
+    }
+
+    @Test
+    public void testGetSummary() {
+        int summary = mInstance.getSummary();
+        assertThat(summary).isEqualTo(AUDIO_STREAM_WAIT_FOR_SYNC_STATE_SUMMARY);
+    }
+
+    @Test
+    public void testGetStateEnum() {
+        AudioStreamsProgressCategoryController.AudioStreamState stateEnum =
+                mInstance.getStateEnum();
+        assertThat(stateEnum)
+                .isEqualTo(AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC);
+    }
+
+    @Test
+    public void testPerformAction_timeout_stateNotMatching_doNothing() {
+        when(mMockPreference.isShown()).thenReturn(true);
+        when(mMockPreference.getAudioStreamState())
+                .thenReturn(AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN);
+
+        mInstance.performAction(mMockPreference, mMockController, mMockHelper);
+        ShadowLooper.idleMainLooper(WAIT_FOR_SYNC_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+
+        verify(mMockController, never()).handleSourceLost(anyInt());
+    }
+
+    @Test
+    public void testPerformAction_timeout_stateMatching_sourceLost() {
+        when(mMockPreference.isShown()).thenReturn(true);
+        when(mMockPreference.getAudioStreamState())
+                .thenReturn(AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC);
+        when(mMockPreference.getAudioStreamBroadcastId()).thenReturn(1);
+        when(mMockPreference.getAudioStreamMetadata()).thenReturn(mMockMetadata);
+
+        mInstance.performAction(mMockPreference, mMockController, mMockHelper);
+        ShadowLooper.idleMainLooper(WAIT_FOR_SYNC_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+
+        verify(mMockController).handleSourceLost(1);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java
new file mode 100644
index 0000000..0dff64d
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java
@@ -0,0 +1,61 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams.testshadows;
+
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+
+import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsHelper;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.List;
+import java.util.Optional;
+
+@Implements(value = AudioStreamsHelper.class, callThroughByDefault = false)
+public class ShadowAudioStreamsHelper {
+    private static AudioStreamsHelper sMockHelper;
+    private static Optional<CachedBluetoothDevice> sCachedBluetoothDevice;
+
+    public static void setUseMock(AudioStreamsHelper mockAudioStreamsHelper) {
+        sMockHelper = mockAudioStreamsHelper;
+    }
+
+    /** Resets {@link CachedBluetoothDevice} */
+    public static void resetCachedBluetoothDevice() {
+        sCachedBluetoothDevice = Optional.empty();
+    }
+
+    public static void setCachedBluetoothDeviceInSharingOrLeConnected(
+            CachedBluetoothDevice cachedBluetoothDevice) {
+        sCachedBluetoothDevice = Optional.of(cachedBluetoothDevice);
+    }
+
+    @Implementation
+    public List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() {
+        return sMockHelper.getAllConnectedSources();
+    }
+
+    /** Gets {@link CachedBluetoothDevice} in sharing or le connected */
+    @Implementation
+    public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected(
+            LocalBluetoothManager manager) {
+        return sCachedBluetoothDevice;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowEntityHeaderController.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowEntityHeaderController.java
new file mode 100644
index 0000000..951fb26
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowEntityHeaderController.java
@@ -0,0 +1,43 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams.testshadows;
+
+import android.app.Activity;
+import android.view.View;
+
+import androidx.fragment.app.Fragment;
+
+import com.android.settings.widget.EntityHeaderController;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+@Implements(value = EntityHeaderController.class, callThroughByDefault = false)
+public class ShadowEntityHeaderController {
+    private static EntityHeaderController sMockController;
+
+    public static void setUseMock(EntityHeaderController mockController) {
+        sMockController = mockController;
+    }
+
+    /** Returns new instance of {@link EntityHeaderController} */
+    @Implementation
+    public static EntityHeaderController newInstance(
+            Activity activity, Fragment fragment, View header) {
+        return sMockController;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowLocalMediaManager.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowLocalMediaManager.java
new file mode 100644
index 0000000..02f12c2
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowLocalMediaManager.java
@@ -0,0 +1,59 @@
+/*
+ * 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.settings.connecteddevice.audiosharing.audiostreams.testshadows;
+
+import com.android.settingslib.media.LocalMediaManager;
+import com.android.settingslib.media.MediaDevice;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.Collections;
+
+@Implements(value = LocalMediaManager.class, callThroughByDefault = false)
+public class ShadowLocalMediaManager {
+
+    private static LocalMediaManager sMockManager;
+    private static LocalMediaManager.DeviceCallback sDeviceCallback;
+
+    public static void setUseMock(LocalMediaManager mockLocalMediaManager) {
+        sMockManager = mockLocalMediaManager;
+    }
+
+    /** Triggers onDeviceListUpdate of {@link LocalMediaManager.DeviceCallback} */
+    public static void onDeviceListUpdate() {
+        sDeviceCallback.onDeviceListUpdate(Collections.emptyList());
+    }
+
+    /** Starts scan */
+    @Implementation
+    public void startScan() {
+        sMockManager.startScan();
+    }
+
+    /** Registers {@link  LocalMediaManager.DeviceCallback} */
+    @Implementation
+    public void registerCallback(LocalMediaManager.DeviceCallback deviceCallback) {
+        sMockManager.registerCallback(deviceCallback);
+        sDeviceCallback = deviceCallback;
+    }
+
+    @Implementation
+    public MediaDevice getCurrentConnectedDevice() {
+        return sMockManager.getCurrentConnectedDevice();
+    }
+}
diff --git a/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java
index 71f8e58..e0f4b9e 100644
--- a/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/robotests/testutils/com/android/settings/testutils/FakeFeatureFactory.java
@@ -27,7 +27,6 @@
 import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider;
 import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
 import com.android.settings.bluetooth.BluetoothFeatureProvider;
-import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider;
 import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider;
 import com.android.settings.connecteddevice.stylus.StylusFeatureProvider;
 import com.android.settings.dashboard.DashboardFeatureProvider;
@@ -103,7 +102,6 @@
     public FastPairFeatureProvider mFastPairFeatureProvider;
     public PrivateSpaceLoginFeatureProvider mPrivateSpaceLoginFeatureProvider;
     public DisplayFeatureProvider mDisplayFeatureProvider;
-    public AudioSharingFeatureProvider mAudioSharingFeatureProvider;
     public SyncAcrossDevicesFeatureProvider mSyncAcrossDevicesFeatureProvider;
 
     /**
@@ -154,7 +152,6 @@
         mFastPairFeatureProvider = mock(FastPairFeatureProvider.class);
         mPrivateSpaceLoginFeatureProvider = mock(PrivateSpaceLoginFeatureProvider.class);
         mDisplayFeatureProvider = mock(DisplayFeatureProvider.class);
-        mAudioSharingFeatureProvider = mock(AudioSharingFeatureProvider.class);
         mSyncAcrossDevicesFeatureProvider = mock(SyncAcrossDevicesFeatureProvider.class);
     }
 
@@ -340,11 +337,6 @@
     }
 
     @Override
-    public AudioSharingFeatureProvider getAudioSharingFeatureProvider() {
-        return mAudioSharingFeatureProvider;
-    }
-
-    @Override
     public SyncAcrossDevicesFeatureProvider getSyncAcrossDevicesFeatureProvider() {
         return mSyncAcrossDevicesFeatureProvider;
     }
diff --git a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
index e1dcda2..5ca24a3 100644
--- a/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
+++ b/tests/spa_unit/src/com/android/settings/testutils/FakeFeatureFactory.kt
@@ -25,7 +25,6 @@
 import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider
 import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider
 import com.android.settings.bluetooth.BluetoothFeatureProvider
-import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider
 import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider
 import com.android.settings.connecteddevice.stylus.StylusFeatureProvider
 import com.android.settings.dashboard.DashboardFeatureProvider
@@ -151,8 +150,6 @@
         get() = TODO("Not yet implemented")
     override val displayFeatureProvider: DisplayFeatureProvider
         get() = TODO("Not yet implemented")
-    override val audioSharingFeatureProvider: AudioSharingFeatureProvider
-        get() = TODO("Not yet implemented")
     override val syncAcrossDevicesFeatureProvider: SyncAcrossDevicesFeatureProvider
         get() = TODO("Not yet implemented")
 }
diff --git a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
index cc129fd..b8dd5ac 100644
--- a/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
+++ b/tests/unit/src/com/android/settings/testutils/FakeFeatureFactory.java
@@ -27,7 +27,6 @@
 import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider;
 import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
 import com.android.settings.bluetooth.BluetoothFeatureProvider;
-import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider;
 import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider;
 import com.android.settings.connecteddevice.stylus.StylusFeatureProvider;
 import com.android.settings.dashboard.DashboardFeatureProvider;
@@ -102,7 +101,6 @@
     public FastPairFeatureProvider mFastPairFeatureProvider;
     public PrivateSpaceLoginFeatureProvider mPrivateSpaceLoginFeatureProvider;
     public DisplayFeatureProvider mDisplayFeatureProvider;
-    public AudioSharingFeatureProvider mAudioSharingFeatureProvider;
     public SyncAcrossDevicesFeatureProvider mSyncAcrossDevicesFeatureProvider;
 
     /** Call this in {@code @Before} method of the test class to use fake factory. */
@@ -155,7 +153,6 @@
         mFastPairFeatureProvider = mock(FastPairFeatureProvider.class);
         mPrivateSpaceLoginFeatureProvider = mock(PrivateSpaceLoginFeatureProvider.class);
         mDisplayFeatureProvider = mock(DisplayFeatureProvider.class);
-        mAudioSharingFeatureProvider = mock(AudioSharingFeatureProvider.class);
         mSyncAcrossDevicesFeatureProvider = mock(SyncAcrossDevicesFeatureProvider.class);
     }
 
@@ -341,11 +338,6 @@
     }
 
     @Override
-    public AudioSharingFeatureProvider getAudioSharingFeatureProvider() {
-        return mAudioSharingFeatureProvider;
-    }
-
-    @Override
     public SyncAcrossDevicesFeatureProvider getSyncAcrossDevicesFeatureProvider() {
         return mSyncAcrossDevicesFeatureProvider;
     }