Merge "feat(MultiFingerMultiTap): Update shortcut string to double tap from triple tap" into main
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f88c55d..2b49148 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2874,7 +2874,6 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
             <intent-filter>
-                <action android:name="com.android.settings.action.SETTINGS" />
                 <action android:name="com.android.intent.action.SHOW_CONTRAST_DIALOG" />
             </intent-filter>
             <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
@@ -4186,6 +4185,40 @@
                        android:value="@string/menu_key_apps"/>
         </activity>
 
+        <!-- @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") -->
+        <activity-alias
+            android:name="MediaRoutingControlActivity"
+            android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
+            android:exported="true"
+            android:targetActivity=".spa.SpaBridgeActivity"
+            android:label="@string/media_routing_control_title">
+            <intent-filter android:priority="1">
+                <action android:name="android.settings.REQUEST_MEDIA_ROUTING_CONTROL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.spa.DESTINATION"
+                       android:value="TogglePermissionAppList/MediaRoutingControl"/>
+            <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
+                       android:value="@string/menu_key_apps"/>
+        </activity-alias>
+
+        <activity-alias
+            android:name="AppMediaRoutingControlActivity"
+            android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
+            android:exported="true"
+            android:targetActivity=".spa.SpaAppBridgeActivity"
+            android:label="@string/media_routing_control_title">
+            <intent-filter android:priority="1">
+                <action android:name="android.settings.REQUEST_MEDIA_ROUTING_CONTROL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="package" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.spa.DESTINATION"
+                       android:value="TogglePermissionAppInfoPage/MediaRoutingControl"/>
+            <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
+                       android:value="@string/menu_key_apps"/>
+        </activity-alias>
+
         <!-- Keep compatibility with old WebView-picker implementation -->
         <activity-alias android:name=".WebViewImplementation"
                   android:targetActivity="Settings$WebViewAppPickerActivity"
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/layout/dialog_audio_sharing.xml b/res/layout/dialog_audio_sharing.xml
index 3b11020..3ea2c01 100644
--- a/res/layout/dialog_audio_sharing.xml
+++ b/res/layout/dialog_audio_sharing.xml
@@ -19,38 +19,64 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:padding="24dp"
-    android:orientation="vertical">
+    android:orientation="vertical"
+    android:paddingHorizontal="?android:dialogPreferredPadding"
+    android:paddingBottom="?android:dialogPreferredPadding">
 
     <TextView
-        style="@style/DeviceAudioSharingText"
         android:id="@+id/share_audio_subtitle1"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textAlignment="center"
-        android:layout_gravity="center"/>
-
-    <TextView
         style="@style/DeviceAudioSharingText"
-        android:id="@+id/share_audio_subtitle2"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textAlignment="center"
-        android:layout_gravity="center"/>
-
-    <com.android.internal.widget.RecyclerView
-        android:visibility="visible"
-        android:id="@+id/btn_list"
-        android:nestedScrollingEnabled="false"
-        android:overScrollMode="never"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"/>
-
-    <Button
-        android:id="@+id/cancel_btn"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_gravity="center"
-        android:text="@string/cancel"/>
+        android:paddingBottom="14dp"
+        android:textFontWeight="500"
+        android:visibility="gone" />
+
+    <TextView
+        android:id="@+id/share_audio_subtitle2"
+        style="@style/DeviceAudioSharingText"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:paddingBottom="24dp"
+        android:textFontWeight="400" />
+
+    <ImageView
+        android:id="@+id/share_audio_guidance"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:src="@drawable/audio_sharing_guidance"
+        android:visibility="gone" />
+
+    <com.android.internal.widget.RecyclerView
+        android:id="@+id/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/share_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/cancel_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:text="Not now"
+        android:visibility="gone" />
 </LinearLayout>
\ No newline at end of file
diff --git a/res/layout/dialog_audio_sharing_disconnect.xml b/res/layout/dialog_audio_sharing_disconnect.xml
index 09bac40..54dee40 100644
--- a/res/layout/dialog_audio_sharing_disconnect.xml
+++ b/res/layout/dialog_audio_sharing_disconnect.xml
@@ -19,29 +19,31 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:padding="24dp"
-    android:orientation="vertical">
+    android:orientation="vertical"
+    android:paddingHorizontal="?android:dialogPreferredPadding"
+    android:paddingBottom="?android:dialogPreferredPadding">
 
     <TextView
         android:id="@+id/share_audio_disconnect_description"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:textAlignment="center"
-        android:layout_gravity="center"/>
-
-    <com.android.internal.widget.RecyclerView
-        android:visibility="visible"
-        android:id="@+id/device_btn_list"
-        android:nestedScrollingEnabled="false"
-        android:overScrollMode="never"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"/>
-
-    <Button
-        android:id="@+id/cancel_btn"
+        style="@style/DeviceAudioSharingText"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_gravity="center"
-        android:text="@string/cancel"/>
+        android:paddingBottom="24dp" />
+
+    <com.android.internal.widget.RecyclerView
+        android:id="@+id/device_btn_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center" />
+
+    <Button
+        android:id="@+id/cancel_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:text="@string/cancel" />
 </LinearLayout>
\ No newline at end of file
diff --git a/res/layout/dialog_audio_sharing_join.xml b/res/layout/dialog_audio_sharing_join.xml
index 42d964a..4bdab7f 100644
--- a/res/layout/dialog_audio_sharing_join.xml
+++ b/res/layout/dialog_audio_sharing_join.xml
@@ -19,35 +19,44 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:padding="24dp"
-    android:orientation="vertical">
+    android:orientation="vertical"
+    android:paddingHorizontal="?android:dialogPreferredPadding"
+    android:paddingBottom="?android:dialogPreferredPadding">
 
     <TextView
         android:id="@+id/share_audio_subtitle1"
+        style="@style/DeviceAudioSharingText"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:textAlignment="center"
-        android:layout_gravity="center"/>
+        android:layout_gravity="center"
+        android:paddingBottom="14dp"
+        android:textFontWeight="500" />
 
     <TextView
         android:id="@+id/share_audio_subtitle2"
+        style="@style/DeviceAudioSharingText"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:textAlignment="center"
-        android:layout_gravity="center"/>
+        android:layout_gravity="center"
+        android:paddingBottom="24dp"
+        android:textFontWeight="400" />
 
     <Button
         android:id="@+id/share_btn"
+        style="@style/SettingsLibActionButton"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_gravity="center"
-        android:text=""/>
+        android:layout_marginTop="4dp"
+        android:background="@drawable/audio_sharing_rounded_bg_ripple" />
 
     <Button
         android:id="@+id/cancel_btn"
+        style="@style/SettingsLibActionButton"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_gravity="center"
-        android:text="@string/cancel"/>
-
+        android:layout_marginTop="4dp"
+        android:background="@drawable/audio_sharing_rounded_bg_ripple"
+        android:text="Not now" />
 </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..0513c4b
--- /dev/null
+++ b/res/layout/dialog_custom_title_audio_sharing.xml
@@ -0,0 +1,41 @@
+<?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="32dp"
+        android:layout_height="32dp"
+        android:layout_gravity="center"
+        android:tint="?android:attr/colorControlNormal" />
+
+    <TextView
+        android:id="@+id/title_text"
+        style="?android:attr/windowTitleStyle"
+        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/privatespace_account_login_error.xml b/res/layout/privatespace_account_login_error.xml
index a38dd50..beae22f 100644
--- a/res/layout/privatespace_account_login_error.xml
+++ b/res/layout/privatespace_account_login_error.xml
@@ -22,7 +22,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:filterTouchesWhenObscured="true"
-    app:sucHeaderText="@string/privatespace_retry_signin_title"
-    app:sudDescriptionText="@string/privatespace_retry_summary"
+    app:sucHeaderText="@string/private_space_retry_signin_title"
+    app:sudDescriptionText="@string/private_space_retry_summary"
     android:icon="@drawable/ic_error_red">
 </com.google.android.setupdesign.GlifLayout>
diff --git a/res/layout/privatespace_advancing_screen.xml b/res/layout/privatespace_advancing_screen.xml
index 5b69593..3a85b16 100644
--- a/res/layout/privatespace_advancing_screen.xml
+++ b/res/layout/privatespace_advancing_screen.xml
@@ -17,7 +17,7 @@
 <com.google.android.setupdesign.GlifLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/privatesapce_autoadvance_screen"
+    android:id="@+id/private_space_autoadvance_screen"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:icon="@drawable/ic_privatespace_icon">
@@ -39,7 +39,7 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:textSize="14sp"
-            android:text="@string/privatespace_setting_up_text"
+            android:text="@string/private_space_setting_up_text"
             android:layout_marginBottom="24dp"/>
 
     </LinearLayout>
diff --git a/res/layout/privatespace_creation_error.xml b/res/layout/privatespace_creation_error.xml
index af11f3a..0ec0cb6 100644
--- a/res/layout/privatespace_creation_error.xml
+++ b/res/layout/privatespace_creation_error.xml
@@ -17,11 +17,10 @@
 <com.google.android.setupdesign.GlifLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/privatespace_setup_error"
+    android:id="@+id/private_space_setup_error"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:filterTouchesWhenObscured="true"
-    app:sucHeaderText="@string/privatespace_error_screen_title"
-    app:sudDescriptionText="@string/privatespace_error_screen_summary"
+    app:sucHeaderText="@string/private_space_error_screen_title"
     android:icon="@drawable/ic_warning_circle_red">
 </com.google.android.setupdesign.GlifLayout>
diff --git a/res/layout/privatespace_education_screen.xml b/res/layout/privatespace_education_screen.xml
index adb65c9..350e780 100644
--- a/res/layout/privatespace_education_screen.xml
+++ b/res/layout/privatespace_education_screen.xml
@@ -21,8 +21,8 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:filterTouchesWhenObscured="true"
-    app:sucHeaderText="@string/privatespace_setup_title"
-    app:sudDescriptionText="@string/privatespace_hide_apps_summary"
+    app:sucHeaderText="@string/private_space_setup_title"
+    app:sudDescriptionText="@string/private_space_hide_apps_summary"
     android:icon="@drawable/ic_privatespace_icon">
     <ScrollView
         android:id="@+id/main_clear_scrollview"
@@ -43,7 +43,29 @@
             android:src="@drawable/privatespace_placeholder_image"/>
         <TextView
             style="@style/PrivateSpaceSetupSubHeaderStyle"
-            android:text="@string/privatespace_how_title"/>
+            android:text="@string/private_space_how_title"/>
+        <RelativeLayout
+            style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
+            <ImageView
+                android:id="@+id/lockIcon"
+                style="@style/PrivateSpaceBulletPointIconStyle"
+                android:src="@drawable/ic_lock_closed" />
+            <TextView
+                style="@style/PrivateSpaceBulletPointTextFontStyle"
+                android:layout_toRightOf="@+id/lockIcon"
+                android:text="@string/private_space_protected_lock_text"/>
+        </RelativeLayout>
+        <RelativeLayout
+            style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
+            <ImageView
+                android:id="@+id/bellIcon"
+                style="@style/PrivateSpaceBulletPointIconStyle"
+                android:src="@drawable/ic_notifications" />
+            <TextView
+                style="@style/PrivateSpaceBulletPointTextFontStyle"
+                android:layout_toRightOf="@+id/bellIcon"
+                android:text="@string/private_space_hidden_notifications_text"/>
+        </RelativeLayout>
         <RelativeLayout
             style="@style/PrivateSpaceSetupBulletPointLayoutStyle"
             android:layout_width="fill_parent"
@@ -55,29 +77,7 @@
             <TextView
                 style="@style/PrivateSpaceBulletPointTextFontStyle"
                 android:layout_toRightOf="@+id/appsIcon"
-                android:text="@string/privatespace_access_bottom_text"/>
-        </RelativeLayout>
-        <RelativeLayout
-            style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
-            <ImageView
-                android:id="@+id/lockIcon"
-                style="@style/PrivateSpaceBulletPointIconStyle"
-                android:src="@drawable/ic_lock_closed" />
-            <TextView
-                style="@style/PrivateSpaceBulletPointTextFontStyle"
-                android:layout_toRightOf="@+id/lockIcon"
-                android:text="@string/privatespace_protected_lock_text"/>
-        </RelativeLayout>
-        <RelativeLayout
-            style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
-            <ImageView
-                android:id="@+id/bellIcon"
-                style="@style/PrivateSpaceBulletPointIconStyle"
-                android:src="@drawable/ic_notifications" />
-            <TextView
-                style="@style/PrivateSpaceBulletPointTextFontStyle"
-                android:layout_toRightOf="@+id/bellIcon"
-                android:text="@string/privatespace_hidden_notifications_text"/>
+                android:text="@string/private_space_access_bottom_text"/>
         </RelativeLayout>
         <Space
             android:layout_width="wrap_content"
@@ -94,7 +94,7 @@
                 style="@style/PrivateSpaceBulletPointTextFontStyle"
                 android:textSize = "14sp"
                 android:layout_toRightOf="@+id/infoIcon"
-                android:text="@string/privatespace_apps_permission_text"/>
+                android:text="@string/private_space_apps_permission_text"/>
         </RelativeLayout>
     </LinearLayout>
     </ScrollView>
diff --git a/res/layout/privatespace_setlock_screen.xml b/res/layout/privatespace_setlock_screen.xml
index 5caf4ae..7211948 100644
--- a/res/layout/privatespace_setlock_screen.xml
+++ b/res/layout/privatespace_setlock_screen.xml
@@ -21,8 +21,8 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:filterTouchesWhenObscured="true"
-    app:sucHeaderText="@string/privatespace_lockscreen_title"
-    app:sudDescriptionText="@string/privatespace_lockscreen_summary"
+    app:sucHeaderText="@string/private_space_lockscreen_title"
+    app:sudDescriptionText="@string/private_space_lockscreen_summary"
     android:icon="@drawable/ic_lock">
     <com.google.android.setupdesign.view.FillContentLayout
         style="@style/SudContentFrame"
diff --git a/res/layout/privatespace_setup_success.xml b/res/layout/privatespace_setup_success.xml
index 00b6fec..e3e6b68 100644
--- a/res/layout/privatespace_setup_success.xml
+++ b/res/layout/privatespace_setup_success.xml
@@ -17,11 +17,11 @@
 <com.google.android.setupdesign.GlifLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/privatespace_setup_success"
+    android:id="@+id/private_space_setup_success"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:filterTouchesWhenObscured="true"
-    app:sucHeaderText="@string/privatespace_success_title"
-    app:sudDescriptionText="@string/privatespace_access_text"
+    app:sucHeaderText="@string/private_space_success_title"
+    app:sudDescriptionText="@string/private_space_access_text"
     android:icon="@drawable/ic_privatespace_done">
 </com.google.android.setupdesign.GlifLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index faaa375..33cae6d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1253,63 +1253,61 @@
     <!-- Title of the Alert Dialog when no screen lock is set [CHAR LIMIT=30] -->
     <string name="no_device_lock_title">Set a screen lock</string>
     <!-- Summary of the alert when no screen lock is set [CHAR LIMIT=90] -->
-    <string name="no_device_lock_summary">To use Private Space, set a screen lock on this device.</string>
+    <string name="no_device_lock_summary">To use your private space, set a screen lock on this device</string>
     <!-- Action label for dialog when no screen lock is set [CHAR LIMIT=30] -->
     <string name="no_device_lock_action_label">Set screen lock</string>
     <!-- Action label to cancel Alert dialog when no screen lock is set [CHAR LIMIT=30] -->
     <string name="no_device_lock_cancel">Cancel</string>
-    <!-- Action label to cancel Private Space Setup flow [CHAR LIMIT=30] -->
-    <string name="privatespace_cancel_label">Cancel</string>
-    <!-- Label for Private Space setup button to create Private Space [CHAR LIMIT=30] -->
-    <string name="privatespace_setup_button_label">Set up</string>
+    <!-- Action label to cancel private space Setup flow [CHAR LIMIT=30] -->
+    <string name="private_space_cancel_label">Cancel</string>
+    <!-- Label for private space setup button to create private space [CHAR LIMIT=30] -->
+    <string name="private_space_setup_button_label">Set up</string>
     <!-- Title for Private Space setup education screen. [CHAR LIMIT=50] -->
-    <string name="privatespace_setup_title">Set up Private Space</string>
-    <!-- Summary for the Private Space setup education screen. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_hide_apps_summary">Hide private apps in a secure space that only you can access</string>
-    <!-- Text shown in Private Space setup screen which explains how the Private Space works [CHAR LIMIT=50] -->
-    <string name="privatespace_how_title">How it works</string>
-    <!-- Text shown in Private Space setup screen which explains Private Space can be accessed from bottom of all apps list. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_access_bottom_text">You can access Private Space from the bottom of your apps list</string>
-    <!-- Text shown in Private Space setup screen which explains Private Space apps are protected by a lock. [CHAR LIMIT=60] -->
-    <string name="privatespace_protected_lock_text">Apps in Private Space are protected by a lock</string>
-    <!-- Text shown in Private Space setup screen which explains notifications from Private Space apps will not be shown when Private Space is locked. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_hidden_notifications_text">Notifications from apps in Private Space are hidden when it\u2019s locked</string>
-    <!-- Text shown in Private Space setup screen which explains that the permissions granted to Private Space apps will not be shown in settings when Private Space is locked. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_apps_permission_text">Private Space apps won\u2019t appear in permission manager, privacy dashboard, and other settings when Private Space is locked</string>
-    <!-- Text shown at the bottom in Private Space auto advancing  screens. [CHAR LIMIT=60] -->
-    <string name="privatespace_setting_up_text">Setting up Private Space\u2026</string>
-    <!-- Title for Private Space setup in auto advancing screen informing private space is hidden when locked. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_apps_hidden_title">Usage info for Private Space apps is hidden when it\u2019s locked</string>
-    <!-- Title for Private Space setup in auto advancing screen informing private space can be accessed from apps list. [CHAR LIMIT=60] -->
-    <string name="privatespace_access_from_apps_title">Access Private Space from your apps list</string>
-    <!-- Title for Private Space setup in auto advancing screen informing some system apps are already installed in Private Space. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_system_apps_installed_title">Some system apps are already installed in Private Space</string>
-    <!-- Title for Private Space creation error screen. [CHAR LIMIT=60] -->
-    <string name="privatespace_error_screen_title">Couldn\u2019t set up Private Space</string>
-    <!-- Summary for the Private Space creation error screen. [CHAR LIMIT=60] -->
-    <string name="privatespace_error_screen_summary">Try again now, or come back later</string>
+    <string name="private_space_setup_title">Set up a private space</string>
+    <!-- Summary for the private space setup education screen. [CHAR LIMIT=NONE] -->
+    <string name="private_space_hide_apps_summary">Keep private apps in a separate space that you can hide or lock</string>
+    <!-- Text shown in private space setup screen which explains how the private space works [CHAR LIMIT=50] -->
+    <string name="private_space_how_title">How it works</string>
+    <!-- Text shown in private space setup screen which explains private space can be accessed from bottom of all apps list. [CHAR LIMIT=NONE] -->
+    <string name="private_space_access_bottom_text">You can access your private space from the bottom of your apps list</string>
+    <!-- Text shown in private space setup screen which explains private space apps are protected by a lock. [CHAR LIMIT=60] -->
+    <string name="private_space_protected_lock_text">Apps in your private space are protected by a lock</string>
+    <!-- Text shown in private space setup screen which explains notifications from private space apps will not be shown when private space is locked. [CHAR LIMIT=NONE] -->
+    <string name="private_space_hidden_notifications_text">Notifications from apps in your private space are hidden when it\u2019s locked</string>
+    <!-- This is info text to help explain in private space setup screen that the permissions granted to private space apps will not be shown in settings when private space is locked. [CHAR LIMIT=NONE] -->
+    <string name="private_space_apps_permission_text">Apps in your private space won\'t appear in permission manager, privacy dashboard, and other settings when your private space is locked.\n\nYour private space can\'t be moved to a new device. You\'ll need to set up another private space if you want to use it on another device.\n\nAnyone that connects your device to a computer or installs harmful apps on your device may be able to access your private space.</string>
+    <!-- Text shown at the bottom in private space auto advancing  screens. [CHAR LIMIT=60] -->
+    <string name="private_space_setting_up_text">Setting up private space\u2026</string>
+    <!-- Title for private space setup in auto advancing screen informing private space notifications are hidden when locked. [CHAR LIMIT=NONE] -->
+    <string name="private_space_notifications_hidden_title">Notifications from apps in private space are hidden when it\u2019s locked</string>
+    <!-- Title for private space setup in auto advancing screen informing photos/files from private space can be shared when unlocked. [CHAR LIMIT=NONE] -->
+    <string name="private_space_share_photos_title">Unlock your space to share photos or files from private space apps</string>
+    <!-- Title for private space setup in auto advancing screen informing some system apps are already installed in private space. [CHAR LIMIT=NONE] -->
+    <string name="private_space_apps_installed_title">Some apps are already installed in your private space</string>
+    <!-- Title for private space creation error screen. [CHAR LIMIT=60] -->
+    <string name="private_space_error_screen_title">Couldn\u2019t set up private space</string>
     <!-- Label for button to retry creating private space again on creation error. [CHAR LIMIT=30] -->
-    <string name="privatespace_tryagain_label">Try Again</string>
-    <!-- Title for Private Space lock setup screen. [CHAR LIMIT=50] -->
-    <string name="privatespace_lockscreen_title">Use screen lock to unlock?</string>
-    <!-- Summary for the Private Space lock setup screen. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_lockscreen_summary">You can unlock Private Space the same way you unlock your device, or choose a different lock</string>
-    <!-- Action label to use existing device lock for Private Space. [CHAR LIMIT=50] -->
-    <string name="privatespace_use_screenlock_label">Use screen lock</string>
-    <!-- Label for Private Space lock setup button to choose a new lock. [CHAR LIMIT=50] -->
-    <string name="privatespace_set_lock_label">Choose new lock</string>
-    <!-- Title for Private Space setup success screen. [CHAR LIMIT=30] -->
-    <string name="privatespace_success_title">All set!</string>
-    <!-- Summary for the Private Space setup success screen. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_access_text">You can access Private Space from your apps list</string>
-    <!-- Label for Private Space done button to show a toast, finish setup and launch All apps [CHAR LIMIT=30] -->
-    <string name="privatespace_done_label">Done</string>
+    <string name="private_space_tryagain_label">Try Again</string>
+    <!-- Title for private space lock setup screen. [CHAR LIMIT=50] -->
+    <string name="private_space_lockscreen_title">Use screen lock to unlock private space?</string>
+    <!-- Summary for the private space lock setup screen. [CHAR LIMIT=NONE] -->
+    <string name="private_space_lockscreen_summary">You can unlock your private space the same way you unlock your device, or choose a different lock</string>
+    <!-- Action label to use existing device lock for private space. [CHAR LIMIT=50] -->
+    <string name="private_space_use_screenlock_label">Use screen lock</string>
+    <!-- Label for private space lock setup button to choose a new lock. [CHAR LIMIT=50] -->
+    <string name="private_space_set_lock_label">Choose new lock</string>
+    <!-- Title for private space setup success screen. [CHAR LIMIT=30] -->
+    <string name="private_space_success_title">All set!</string>
+    <!-- Summary for the private space setup success screen. [CHAR LIMIT=NONE] -->
+    <string name="private_space_access_text">To access your private space, go to your apps list then scroll down</string>
+    <!-- Label for private space done button to show a toast, finish setup and launch All apps [CHAR LIMIT=30] -->
+    <string name="private_space_done_label">Done</string>
     <!-- Toast to show on private space setup completion informing user to scroll down All apps to access private space. [CHAR LIMIT=60] -->
-    <string name="scrolldown_to_access">Scroll down to access Private Space</string>
-    <!-- Title for Private Space account login error screen. [CHAR LIMIT=60] -->
-    <string name="privatespace_retry_signin_title">Sign in to set up Private Space</string>
-    <!-- Summary for the Private Space account login error screen. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_retry_summary">You need to sign in to an account to set up Private Space</string>
+    <string name="private_space_scrolldown_to_access">Scroll down to find private space</string>
+    <!-- Title for private space account login error screen. [CHAR LIMIT=60] -->
+    <string name="private_space_retry_signin_title">Sign in to set up a private space</string>
+    <!-- Summary for the private space account login error screen. [CHAR LIMIT=NONE] -->
+    <string name="private_space_retry_summary">You need to sign in to an account to set up a private space</string>
     <!-- private space lock setup screen title. This title is asking the user to choose a type of screen lock (such as a pattern, PIN, or password) that they need to enter to unlock private space. [CHAR LIMIT=60] -->
     <string name="private_space_lock_setup_title">Choose a lock for your private space</string>
     <!-- private space lock setup screen description [CHAR LIMIT=NONE] -->
@@ -3270,6 +3268,8 @@
     <string name="error_mnc_not23">MNC field must be 2 or 3 digits.</string>
     <!-- APN error dialog messages: -->
     <string name="error_adding_apn_type">Carrier does not allow adding APNs of type %s.</string>
+    <!-- APN error messages: -->
+    <string name="error_mmsc_valid">MMSC field must be valid.</string>
     <!-- The message of dialog indicated restoring default APN settings in progress -->
     <string name="restore_default_apn">Restoring default APN settings.</string>
     <!-- APNs screen menu option to reset default APN settings -->
@@ -3896,6 +3896,8 @@
     <string name="controls_label">Controls</string>
     <!-- Manage applications, text label for button to kill / force stop an application -->
     <string name="force_stop">Force stop</string>
+    <!-- Manage applications, text label for button to archive an application. Archiving means uninstalling the app without deleting user's personal data and replacing the app with a stub app with minimum size. So, the user can unarchive the app later and not lose any personal data. -->
+    <string name="archive">Archive</string>
     <!-- Manage applications, individual application info screen,label under Storage heading.  The total storage space taken up by this app. -->
     <string name="total_size_label">Total</string>
     <!-- Manage applications, individual application info screen, label under Storage heading. The amount of space taken up by the application itself (for example, the java compield files and things like that) -->
@@ -4006,6 +4008,11 @@
     <!-- Manage applications, text for Move button -->
     <string name="move_app">Move</string>
 
+    <!-- Toast message when archiving an app failed. -->
+    <string name="archiving_failed">Archiving failed</string>
+    <!-- Toast message when archiving an app succeeded. -->
+    <string name="archiving_succeeded">Archived <xliff:g id="package_label" example="Translate">%1$s</xliff:g></string>
+
     <!-- Text of pop up message if the request for a "migrate primary storage" operation
          (see storage_menu_migrate) is denied as another is already in progress. [CHAR LIMIT=75] -->
     <string name="another_migration_already_in_progress">Another migration is already in progress.</string>
@@ -9479,6 +9486,14 @@
     <!-- Description of allowing overlay setting [CHAR LIMIT=NONE] -->
     <string name="allow_overlay_description">Allow this app to display on top of other apps you\u2019re using. This app will be able to see where you tap or change what\u2019s displayed on the screen.</string>
 
+    <!-- Change Media Output settings -->
+    <!-- Title for Change Media Output screen [CHAR LIMIT=30] -->
+    <string name="media_routing_control_title">Change media output</string>
+    <!-- Label for setting which controls whether app can change media outputs for other apps [CHAR LIMIT=45] -->
+    <string name="allow_media_routing_control">Allow app to switch media output</string>
+    <!-- Description for allowing change media output setting [CHAR LIMIT=NONE] -->
+    <string name="allow_media_routing_description">Allow this app to choose which connected device plays audio or video from other apps. If allowed, this app can access a list of available devices such as headphones and speakers and choose which output device is used to stream or cast audio or video.</string>
+
     <!-- Manager External Storage settings title [CHAR LIMIT=40] -->
     <string name="manage_external_storage_title">All files access</string>
     <!-- Label for a setting which controls whether an app can manage external storage [CHAR LIMIT=45] -->
diff --git a/res/xml/privatespace_setup_education_entries.xml b/res/xml/privatespace_setup_education_entries.xml
deleted file mode 100644
index 18fb19d..0000000
--- a/res/xml/privatespace_setup_education_entries.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?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.
-  -->
-
-<ItemGroup
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <Item
-        android:enabled="false"
-        android:icon="@drawable/ic_apps"
-        android:title="@string/privatespace_access_bottom_text"
-        app:sudIconTint="?android:attr/textColorSecondary"/>
-
-    <Item
-        android:enabled="false"
-        android:icon="@drawable/ic_lock_24dp"
-        android:title="@string/privatespace_protected_lock_text"
-        app:sudIconTint="?android:attr/textColorSecondary"/>
-
-    <Item
-        android:enabled="false"
-        android:icon="@drawable/ic_notifications"
-        android:title="@string/privatespace_hidden_notifications_text"
-        app:sudIconTint="?android:attr/textColorSecondary"/>
-</ItemGroup>
diff --git a/res/xml/special_access.xml b/res/xml/special_access.xml
index 3f3d75d..743a122 100644
--- a/res/xml/special_access.xml
+++ b/res/xml/special_access.xml
@@ -95,6 +95,13 @@
         settings:controller="com.android.settings.applications.specialaccess.notificationaccess.NotificationAccessController" />
 
     <Preference
+        android:key="media_routing_control"
+        android:title="@string/media_routing_control_title"
+        android:order="-1100"
+        settings:controller="com.android.settings.applications.specialaccess.MediaRoutingControlPreferenceController" >
+    </Preference>
+
+    <Preference
         android:key="use_full_screen_intent"
         android:title="@string/full_screen_intent_title"
         settings:controller="com.android.settings.spa.app.specialaccess.UseFullScreenIntentPreferenceController" />
diff --git a/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceController.java b/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceController.java
new file mode 100644
index 0000000..72011ba
--- /dev/null
+++ b/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceController.java
@@ -0,0 +1,53 @@
+/*
+ * 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.applications.specialaccess;
+
+import android.Manifest;
+import android.content.Context;
+import android.text.TextUtils;
+
+import androidx.preference.Preference;
+
+import com.android.media.flags.Flags;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.spa.SpaActivity;
+import com.android.settings.spa.app.specialaccess.MediaRoutingControlAppListProvider;
+
+/**
+ * This controller manages features availability for special app access for
+ * {@link Manifest.permission#MEDIA_ROUTING_CONTROL} permission.
+ */
+public class MediaRoutingControlPreferenceController extends BasePreferenceController {
+    public MediaRoutingControlPreferenceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return Flags.enablePrivilegedRoutingForMediaRoutingControl()
+                ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+    }
+
+    @Override
+    public boolean handlePreferenceTreeClick(Preference preference) {
+        if (TextUtils.equals(preference.getKey(), mPreferenceKey)) {
+            SpaActivity.startSpaActivity(
+                    mContext, MediaRoutingControlAppListProvider.INSTANCE.getAppListRoute());
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/settings/biometrics/BiometricEnrollBase.java b/src/com/android/settings/biometrics/BiometricEnrollBase.java
index 7df1fe1..c9c8cff 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollBase.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java
@@ -67,7 +67,6 @@
     public static final String EXTRA_FINISHED_ENROLL_FACE = "finished_enrolling_face";
     public static final String EXTRA_FINISHED_ENROLL_FINGERPRINT = "finished_enrolling_fingerprint";
     public static final String EXTRA_LAUNCHED_POSTURE_GUIDANCE = "launched_posture_guidance";
-    public static final String KEY_CALIBRATOR_UUID = "calibrator_uuid";
 
     /**
      * Used by the choose fingerprint wizard to indicate the wizard is
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
index 063d55d..6e90885 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
@@ -70,6 +70,7 @@
 import com.android.settings.biometrics.BiometricsEnrollEnrolling;
 import com.android.settings.biometrics.fingerprint.feature.SfpsEnrollmentFeature;
 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settings.flags.Flags;
 import com.android.settings.overlay.FeatureFactory;
 import com.android.settingslib.display.DisplayDensityUtils;
 
@@ -197,6 +198,8 @@
 
     @NonNull
     private SfpsEnrollmentFeature mSfpsEnrollmentFeature = new EmptySfpsEnrollmentFeature();
+    @Nullable
+    private UdfpsEnrollCalibrator mCalibrator;
 
     @VisibleForTesting
     protected boolean shouldShowLottie() {
@@ -245,6 +248,12 @@
 
             setContentView(layout);
             setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
+
+            if (Flags.udfpsEnrollCalibration()) {
+                mCalibrator = FeatureFactory.getFeatureFactory().getFingerprintFeatureProvider()
+                        .getUdfpsEnrollCalibrator(getApplicationContext(), savedInstanceState,
+                                getIntent());
+            }
         } else if (mCanAssumeSfps) {
             mSfpsEnrollmentFeature = FeatureFactory.getFeatureFactory()
                     .getFingerprintFeatureProvider().getSfpsEnrollmentFeature();
@@ -364,6 +373,11 @@
         super.onSaveInstanceState(outState);
         outState.putBoolean(KEY_STATE_CANCELED, mIsCanceled);
         outState.putInt(KEY_STATE_PREVIOUS_ROTATION, mPreviousRotation);
+        if (Flags.udfpsEnrollCalibration()) {
+            if (mCalibrator != null) {
+                mCalibrator.onSaveInstanceState(outState);
+            }
+        }
     }
 
     private void restoreSavedState(Bundle savedInstanceState) {
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
index 276845c..b71330a 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
@@ -25,8 +25,6 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
 import android.util.Log;
 import android.view.OrientationEventListener;
 import android.view.Surface;
@@ -35,15 +33,12 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.lifecycle.Observer;
 
 import com.android.settings.R;
 import com.android.settings.Utils;
 import com.android.settings.biometrics.BiometricEnrollBase;
 import com.android.settings.biometrics.BiometricEnrollSidecar;
 import com.android.settings.biometrics.BiometricUtils;
-import com.android.settings.biometrics.fingerprint.UdfpsEnrollCalibrator.Result;
-import com.android.settings.biometrics.fingerprint.UdfpsEnrollCalibrator.Status;
 import com.android.settings.flags.Flags;
 import com.android.settings.overlay.FeatureFactory;
 import com.android.settings.password.ChooseLockSettingsHelper;
@@ -56,7 +51,6 @@
 import com.google.android.setupcompat.template.FooterButton;
 
 import java.util.List;
-import java.util.UUID;
 
 /**
  * Activity explaining the fingerprint sensor location for fingerprint enrollment.
@@ -85,8 +79,6 @@
     private boolean mIsReverseDefaultRotation;
     @Nullable
     private UdfpsEnrollCalibrator mCalibrator;
-    @Nullable
-    private Observer<Status> mCalibratorStatusObserver;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -172,13 +164,15 @@
         if (mCanAssumeUdfps) {
             if (Flags.udfpsEnrollCalibration()) {
                 mCalibrator = FeatureFactory.getFeatureFactory().getFingerprintFeatureProvider()
-                        .getUdfpsEnrollCalibrator(
-                                (savedInstanceState != null)
-                                ? savedInstanceState.getParcelable(KEY_CALIBRATOR_UUID, UUID.class)
-                                : getIntent().getSerializableExtra(KEY_CALIBRATOR_UUID, UUID.class)
-                        );
-                if (mCalibrator == null
-                        || mCalibrator.getStatusLiveData().getValue() == Status.FINISHED) {
+                        .getUdfpsEnrollCalibrator(getApplicationContext(), savedInstanceState,
+                                getIntent());
+                if (mCalibrator != null) {
+                    mCalibrator.onFindSensorPage(
+                            getLifecycle(),
+                            getSupportFragmentManager(),
+                            this::enableUdfpsLottieAndNextButton
+                    );
+                } else {
                     enableUdfpsLottieAndNextButton();
                 }
             } else {
@@ -193,14 +187,19 @@
     }
 
     private void enableUdfpsLottieAndNextButton() {
-        mFooterBarMixin.setPrimaryButton(
-                new FooterButton.Builder(this)
-                        .setText(R.string.security_settings_udfps_enroll_find_sensor_start_button)
-                        .setListener(this::onStartButtonClick)
-                        .setButtonType(FooterButton.ButtonType.NEXT)
-                        .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
-                        .build()
-        );
+        if (isFinishing()) {
+            return;
+        }
+
+        if (mFooterBarMixin.getPrimaryButton() == null) {
+            mFooterBarMixin.setPrimaryButton(new FooterButton.Builder(this)
+                    .setText(R.string.security_settings_udfps_enroll_find_sensor_start_button)
+                    .setListener(this::onStartButtonClick)
+                    .setButtonType(FooterButton.ButtonType.NEXT)
+                    .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
+                    .build()
+            );
+        }
         if (mIllustrationLottie != null) {
             mIllustrationLottie.setOnClickListener(this::onStartButtonClick);
         }
@@ -285,7 +284,7 @@
         outState.putBoolean(SAVED_STATE_IS_NEXT_CLICKED, mNextClicked);
         if (Flags.udfpsEnrollCalibration()) {
             if (mCalibrator != null) {
-                outState.putSerializable(KEY_CALIBRATOR_UUID, mCalibrator.getUuid());
+                mCalibrator.onSaveInstanceState(outState);
             }
         }
     }
@@ -317,39 +316,6 @@
         if (mAnimation != null) {
             mAnimation.startAnimation();
         }
-        if (Flags.udfpsEnrollCalibration()) {
-            if (mCalibrator != null) {
-                final Status current = mCalibrator.getStatusLiveData().getValue();
-                if (current == Status.PROCESSING) {
-                    if (mCalibratorStatusObserver == null) {
-                        mCalibratorStatusObserver = status -> {
-                            if (status == Status.GOT_RESULT) {
-                                onGotCalibrationResult();
-                            }
-                        };
-                    }
-                    mCalibrator.getStatusLiveData().observe(this, mCalibratorStatusObserver);
-                } else if (current == Status.GOT_RESULT) {
-                    onGotCalibrationResult();
-                }
-            }
-        }
-    }
-
-    private void onGotCalibrationResult() {
-        if (Flags.udfpsEnrollCalibration()) {
-            if (mCalibrator != null) {
-                mCalibrator.setFinished();
-                if (mCalibrator.getResult() == Result.NEED_CALIBRATION) {
-                    UdfpsEnrollCalibrationDialog.newInstance(
-                            mCalibrator.getCalibrationDialogTitleTextId(),
-                            mCalibrator.getCalibrationDialogMessageTextId(),
-                            mCalibrator.getCalibrationDialogDismissButtonTextId()
-                    ).show(getSupportFragmentManager(), "findsensor-calibration-dialog");
-                }
-            }
-            new Handler(Looper.getMainLooper()).post(this::enableUdfpsLottieAndNextButton);
-        }
     }
 
     private void stopLookingForFingerprint() {
@@ -407,12 +373,6 @@
         if (mAnimation != null) {
             mAnimation.pauseAnimation();
         }
-        if (Flags.udfpsEnrollCalibration()) {
-            if (mCalibrator != null && mCalibratorStatusObserver != null) {
-                mCalibrator.getStatusLiveData().removeObserver(mCalibratorStatusObserver);
-                mCalibratorStatusObserver = null;
-            }
-        }
     }
 
     @Override
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
index dc3c65e..bd52b64 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
@@ -57,7 +57,6 @@
 import com.google.android.setupdesign.util.DeviceHelper;
 
 import java.util.List;
-import java.util.UUID;
 
 public class FingerprintEnrollIntroduction extends BiometricEnrollIntroduction {
 
@@ -92,12 +91,7 @@
 
         if (Flags.udfpsEnrollCalibration()) {
             mCalibrator = FeatureFactory.getFeatureFactory().getFingerprintFeatureProvider()
-                    .getUdfpsEnrollCalibrator(
-                            (savedInstanceState != null)
-                                    ? savedInstanceState.getParcelable(
-                                        KEY_CALIBRATOR_UUID, UUID.class)
-                                    : null
-                        );
+                    .getUdfpsEnrollCalibrator(getApplicationContext(), savedInstanceState, null);
         }
 
         final ImageView iconFingerprint = findViewById(R.id.icon_fingerprint);
@@ -175,7 +169,7 @@
         super.onSaveInstanceState(outState);
         if (Flags.udfpsEnrollCalibration()) {
             if (mCalibrator != null) {
-                outState.putSerializable(KEY_CALIBRATOR_UUID, mCalibrator.getUuid());
+                mCalibrator.onSaveInstanceState(outState);
             }
         }
     }
@@ -391,7 +385,7 @@
         }
         if (Flags.udfpsEnrollCalibration()) {
             if (mCalibrator != null) {
-                intent.putExtra(KEY_CALIBRATOR_UUID, mCalibrator.getUuid());
+                intent.putExtras(mCalibrator.getExtrasForNextIntent());
             }
         }
         return intent;
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java b/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java
index 5a2bf8b..e770220 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProvider.java
@@ -16,12 +16,15 @@
 
 package com.android.settings.biometrics.fingerprint;
 
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.settings.biometrics.fingerprint.feature.SfpsEnrollmentFeature;
 
-import java.util.UUID;
-
 public interface FingerprintFeatureProvider {
     /**
      * Gets the feature implementation of SFPS enrollment.
@@ -29,11 +32,16 @@
      */
     SfpsEnrollmentFeature getSfpsEnrollmentFeature();
 
+
     /**
-     * Gets calibrator to calibrate the FPS before enrolling udfps
-     * @param uuid unique id for passed between different activities
-     * @return udfps calibrator
+     * Gets calibrator for udfps pre-enroll
+     * @param appContext application context
+     * @param activitySavedInstanceState activity savedInstanceState
+     * @param activityIntent activity intent
      */
     @Nullable
-    UdfpsEnrollCalibrator getUdfpsEnrollCalibrator(@Nullable UUID uuid);
+    default UdfpsEnrollCalibrator getUdfpsEnrollCalibrator(@NonNull Context appContext,
+            @Nullable Bundle activitySavedInstanceState, @Nullable Intent activityIntent) {
+        return null;
+    }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProviderImpl.java b/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProviderImpl.java
index 1baabc6..9745ca3 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProviderImpl.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintFeatureProviderImpl.java
@@ -21,8 +21,6 @@
 import com.android.settings.biometrics.fingerprint.feature.SfpsEnrollmentFeature;
 import com.android.settings.biometrics.fingerprint.feature.SfpsEnrollmentFeatureImpl;
 
-import java.util.UUID;
-
 public class FingerprintFeatureProviderImpl implements FingerprintFeatureProvider {
 
     @Nullable
@@ -35,10 +33,4 @@
         }
         return mSfpsEnrollmentFeatureImpl;
     }
-
-    @Nullable
-    @Override
-    public UdfpsEnrollCalibrator getUdfpsEnrollCalibrator(@Nullable UUID uuid) {
-        return null;
-    }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
index 308b3d5..6904342 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
@@ -93,6 +93,7 @@
 import com.google.android.setupdesign.util.DeviceHelper;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 
@@ -876,6 +877,8 @@
         @Override
         protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
             if (!isFingerprintHardwareDetected(context)) {
+                Log.e(TAG, "Fingerprint hardware is not detected");
+                mControllers = Collections.emptyList();
                 return null;
             }
 
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrationDialog.kt b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrationDialog.kt
deleted file mode 100644
index 892996a..0000000
--- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrationDialog.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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.biometrics.fingerprint
-
-import android.app.Dialog
-import android.content.DialogInterface
-import android.os.Bundle
-import androidx.annotation.StringRes
-import androidx.appcompat.app.AlertDialog
-import androidx.fragment.app.DialogFragment
-import com.android.settings.R
-
-class UdfpsEnrollCalibrationDialog : DialogFragment() {
-
-    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
-            AlertDialog.Builder(requireActivity(), R.style.Theme_AlertDialog)
-                    .setTitle(arguments!!.getInt(KEY_TITLE_TEXT_ID))
-                    .setMessage(arguments!!.getInt(KEY_MESSAGE_TEXT_ID))
-                    .setPositiveButton(arguments!!.getInt(KEY_DISMISS_BUTTON_TEXT_ID)) {
-                        dialog: DialogInterface?, _: Int -> dialog?.dismiss()
-                    }
-                    .create().also {
-                        isCancelable = false
-                    }
-
-    companion object {
-
-        private const val KEY_TITLE_TEXT_ID = "title_text_id"
-        private const val KEY_MESSAGE_TEXT_ID = "message_text_id"
-        private const val KEY_DISMISS_BUTTON_TEXT_ID = "dismiss_button_text_id"
-
-        @JvmStatic
-        fun newInstance(
-                @StringRes titleTextId: Int,
-                @StringRes messageTextId: Int,
-                @StringRes dismissButtonTextId: Int
-        ) = UdfpsEnrollCalibrationDialog().apply {
-            arguments = Bundle().apply {
-                putInt(KEY_TITLE_TEXT_ID, titleTextId)
-                putInt(KEY_MESSAGE_TEXT_ID, messageTextId)
-                putInt(KEY_DISMISS_BUTTON_TEXT_ID, dismissButtonTextId)
-            }
-        }
-    }
-}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
index c0626d3..9809bcc 100644
--- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollCalibrator.kt
@@ -1,36 +1,18 @@
 package com.android.settings.biometrics.fingerprint
 
-import androidx.annotation.StringRes
-import androidx.lifecycle.LiveData
-import java.util.UUID
+import android.os.Bundle
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.Lifecycle
 
 interface UdfpsEnrollCalibrator {
 
-    enum class Status {
-        PROCESSING,
-        GOT_RESULT,
-        FINISHED,
-    }
+    val extrasForNextIntent: Bundle
 
-    enum class Result {
-        NEED_CALIBRATION,
-        NO_NEED_CALIBRATION,
-    }
+    fun onSaveInstanceState(outState: Bundle)
 
-    val uuid: UUID
-
-    val statusLiveData: LiveData<Status>
-
-    val result: Result?
-
-    fun setFinished()
-
-    @get:StringRes
-    val calibrationDialogTitleTextId: Int
-
-    @get:StringRes
-    val calibrationDialogMessageTextId: Int
-
-    @get:StringRes
-    val calibrationDialogDismissButtonTextId: Int
+    fun onFindSensorPage(
+            lifecycle: Lifecycle,
+            fragmentManager: FragmentManager,
+            enableEnrollingRunnable: Runnable
+    )
 }
\ No newline at end of file
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
index 5cd86b4..e44939d 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java
@@ -22,6 +22,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.Button;
+import android.widget.ImageView;
 import android.widget.TextView;
 
 import androidx.appcompat.app.AlertDialog;
@@ -33,8 +34,10 @@
 import com.android.settings.R;
 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
 
+import com.google.common.collect.Iterables;
+
 import java.util.ArrayList;
-import java.util.Locale;
+import java.util.stream.Collectors;
 
 public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
     private static final String TAG = "AudioSharingDialog";
@@ -54,8 +57,6 @@
 
     private static DialogEventListener sListener;
 
-    private View mRootView;
-
     @Override
     public int getMetricsCategory() {
         return SettingsEnums.DIALOG_START_AUDIO_SHARING;
@@ -90,40 +91,62 @@
         ArrayList<AudioSharingDeviceItem> deviceItems =
                 arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS);
         final AlertDialog.Builder builder =
-                new AlertDialog.Builder(getActivity()).setTitle("Share audio").setCancelable(false);
-        mRootView =
-                LayoutInflater.from(builder.getContext())
-                        .inflate(R.layout.dialog_audio_sharing, /* parent= */ null);
-        TextView subTitle1 = mRootView.findViewById(R.id.share_audio_subtitle1);
-        TextView subTitle2 = mRootView.findViewById(R.id.share_audio_subtitle2);
+                new AlertDialog.Builder(getActivity()).setCancelable(false);
+        LayoutInflater inflater = LayoutInflater.from(builder.getContext());
+        View customTitle = inflater.inflate(R.layout.dialog_custom_title_audio_sharing, null);
+        ImageView icon = customTitle.findViewById(R.id.title_icon);
+        icon.setImageResource(R.drawable.ic_bt_audio_sharing);
+        TextView title = customTitle.findViewById(R.id.title_text);
+        View rootView = inflater.inflate(R.layout.dialog_audio_sharing, /* parent= */ null);
+        TextView subTitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+        TextView subTitle2 = rootView.findViewById(R.id.share_audio_subtitle2);
+        RecyclerView recyclerView = rootView.findViewById(R.id.btn_list);
+        Button shareBtn = rootView.findViewById(R.id.share_btn);
+        Button cancelBtn = rootView.findViewById(R.id.cancel_btn);
         if (deviceItems.isEmpty()) {
-            subTitle1.setVisibility(View.INVISIBLE);
+            title.setText("Share your audio");
             subTitle2.setText(
-                    "To start sharing audio, connect additional headphones that support LE audio");
-        } else {
+                    "To start sharing audio, "
+                            + "connect two pairs of headphones that support LE Audio");
+            ImageView image = rootView.findViewById(R.id.share_audio_guidance);
+            image.setVisibility(View.VISIBLE);
+            builder.setNegativeButton("Close", null);
+        } else if (deviceItems.size() == 1) {
+            title.setText("Share your audio");
             subTitle1.setText(
-                    String.format(
-                            Locale.US,
-                            "%d additional device%s connected",
-                            deviceItems.size(),
-                            deviceItems.size() > 1 ? "" : "s"));
+                    deviceItems.stream()
+                            .map(AudioSharingDeviceItem::getName)
+                            .collect(Collectors.joining(" and ")));
             subTitle2.setText(
-                    "The headphones you share audio with will hear videos and music playing on this"
-                            + " phone");
+                    "This device's music and videos will play on both pairs of headphones");
+            shareBtn.setText("Share audio");
+            shareBtn.setOnClickListener(
+                    v -> {
+                        sListener.onItemClick(Iterables.getOnlyElement(deviceItems));
+                        dismiss();
+                    });
+            cancelBtn.setOnClickListener(v -> dismiss());
+            subTitle1.setVisibility(View.VISIBLE);
+            shareBtn.setVisibility(View.VISIBLE);
+            cancelBtn.setVisibility(View.VISIBLE);
+        } else {
+            title.setText("Share audio with another device");
+            subTitle2.setText(
+                    "This device's music and videos will play on the headphones you connect");
+            recyclerView.setAdapter(
+                    new AudioSharingDeviceAdapter(
+                            deviceItems,
+                            (AudioSharingDeviceItem item) -> {
+                                sListener.onItemClick(item);
+                                dismiss();
+                            }));
+            recyclerView.setLayoutManager(
+                    new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
+            recyclerView.setVisibility(View.VISIBLE);
+            cancelBtn.setOnClickListener(v -> dismiss());
+            cancelBtn.setVisibility(View.VISIBLE);
         }
-        RecyclerView recyclerView = mRootView.findViewById(R.id.btn_list);
-        recyclerView.setAdapter(
-                new AudioSharingDeviceAdapter(
-                        deviceItems,
-                        (AudioSharingDeviceItem item) -> {
-                            sListener.onItemClick(item);
-                            dismiss();
-                        }));
-        recyclerView.setLayoutManager(
-                new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
-        Button cancelBtn = mRootView.findViewById(R.id.cancel_btn);
-        cancelBtn.setOnClickListener(v -> dismiss());
-        AlertDialog dialog = builder.setView(mRootView).create();
+        AlertDialog dialog = builder.setCustomTitle(customTitle).setView(rootView).create();
         dialog.setCanceledOnTouchOutside(false);
         return dialog;
     }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
index 461c230..7eedb9a 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragment.java
@@ -22,6 +22,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.Button;
+import android.widget.ImageView;
 import android.widget.TextView;
 
 import androidx.appcompat.app.AlertDialog;
@@ -90,17 +91,18 @@
         Bundle arguments = requireArguments();
         ArrayList<AudioSharingDeviceItem> deviceItems =
                 arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS);
-        String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
         final AlertDialog.Builder builder =
-                new AlertDialog.Builder(getActivity())
-                        .setTitle("Choose headphone to disconnect")
-                        .setCancelable(false);
+                new AlertDialog.Builder(getActivity()).setCancelable(false);
+        LayoutInflater inflater = LayoutInflater.from(builder.getContext());
+        View customTitle = inflater.inflate(R.layout.dialog_custom_title_audio_sharing, null);
+        ImageView icon = customTitle.findViewById(R.id.title_icon);
+        icon.setImageResource(R.drawable.ic_bt_audio_sharing);
+        TextView title = customTitle.findViewById(R.id.title_text);
+        title.setText("Choose a device to disconnect");
         View rootView =
-                LayoutInflater.from(builder.getContext())
-                        .inflate(R.layout.dialog_audio_sharing_disconnect, /* parent= */ null);
+                inflater.inflate(R.layout.dialog_audio_sharing_disconnect, /* parent= */ null);
         TextView subTitle = rootView.findViewById(R.id.share_audio_disconnect_description);
-        subTitle.setText(
-                "To share audio with " + newDeviceName + ", disconnect another pair of headphone");
+        subTitle.setText("Only 2 devices can share audio at a time");
         RecyclerView recyclerView = rootView.findViewById(R.id.device_btn_list);
         recyclerView.setAdapter(
                 new AudioSharingDeviceAdapter(
@@ -116,7 +118,7 @@
                 v -> {
                     dismiss();
                 });
-        AlertDialog dialog = builder.setView(rootView).create();
+        AlertDialog dialog = builder.setCustomTitle(customTitle).setView(rootView).create();
         dialog.setCanceledOnTouchOutside(false);
         return dialog;
     }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java
index 2d7b4c4..f3f0fe4 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragment.java
@@ -22,6 +22,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.Button;
+import android.widget.ImageView;
 import android.widget.TextView;
 
 import androidx.appcompat.app.AlertDialog;
@@ -48,7 +49,6 @@
     }
 
     private static DialogEventListener sListener;
-    private View mRootView;
 
     @Override
     public int getMetricsCategory() {
@@ -86,14 +86,17 @@
                 arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS);
         String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
         final AlertDialog.Builder builder =
-                new AlertDialog.Builder(getActivity())
-                        .setTitle("Share audio?")
-                        .setCancelable(false);
-        mRootView =
-                LayoutInflater.from(builder.getContext())
-                        .inflate(R.layout.dialog_audio_sharing_join, null /* parent */);
-        TextView subtitle1 = mRootView.findViewById(R.id.share_audio_subtitle1);
-        TextView subtitle2 = mRootView.findViewById(R.id.share_audio_subtitle2);
+                new AlertDialog.Builder(getActivity()).setCancelable(false);
+        LayoutInflater inflater = LayoutInflater.from(builder.getContext());
+        View customTitle =
+                inflater.inflate(R.layout.dialog_custom_title_audio_sharing, /* parent= */ null);
+        ImageView icon = customTitle.findViewById(R.id.title_icon);
+        icon.setImageResource(R.drawable.ic_bt_audio_sharing);
+        TextView title = customTitle.findViewById(R.id.title_text);
+        title.setText("Share your audio");
+        View rootView = inflater.inflate(R.layout.dialog_audio_sharing_join, /* parent= */ null);
+        TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+        TextView subtitle2 = rootView.findViewById(R.id.share_audio_subtitle2);
         if (deviceItems.isEmpty()) {
             subtitle1.setText(newDeviceName);
         } else {
@@ -106,10 +109,9 @@
                                     .collect(Collectors.joining(", ")),
                             newDeviceName));
         }
-        subtitle2.setText(
-                "Connected eligible headphones will hear videos ad music playing on this phone");
-        Button shareBtn = mRootView.findViewById(R.id.share_btn);
-        Button cancelBtn = mRootView.findViewById(R.id.cancel_btn);
+        subtitle2.setText("This device's music and videos will play on both pairs of headphones");
+        Button shareBtn = rootView.findViewById(R.id.share_btn);
+        Button cancelBtn = rootView.findViewById(R.id.cancel_btn);
         shareBtn.setOnClickListener(
                 v -> {
                     sListener.onShareClick();
@@ -117,7 +119,7 @@
                 });
         shareBtn.setText("Share audio");
         cancelBtn.setOnClickListener(v -> dismiss());
-        Dialog dialog = builder.setView(mRootView).create();
+        Dialog dialog = builder.setCustomTitle(customTitle).setView(rootView).create();
         dialog.setCanceledOnTouchOutside(false);
         return dialog;
     }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java
index 31125de..1454f76 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragment.java
@@ -19,11 +19,16 @@
 import android.app.Dialog;
 import android.app.settings.SettingsEnums;
 import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
 
 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 AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
@@ -68,22 +73,20 @@
         Bundle arguments = requireArguments();
         String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
         final AlertDialog.Builder builder =
-                new AlertDialog.Builder(getActivity())
-                        .setTitle("Stop sharing audio?")
-                        .setCancelable(false);
+                new AlertDialog.Builder(getActivity()).setCancelable(false);
+        LayoutInflater inflater = LayoutInflater.from(builder.getContext());
+        View customTitle =
+                inflater.inflate(R.layout.dialog_custom_title_audio_sharing, /* parent= */ null);
+        ImageView icon = customTitle.findViewById(R.id.title_icon);
+        icon.setImageResource(R.drawable.ic_warning_24dp);
+        TextView title = customTitle.findViewById(R.id.title_text);
+        title.setText("Stop sharing audio?");
         builder.setMessage(
-                newDeviceName + " is connected, devices in audio sharing will disconnect.");
+                newDeviceName + " wants to connect, headphones in audio sharing will disconnect.");
         builder.setPositiveButton(
-                "Stop sharing",
-                (dialog, which) -> {
-                    sListener.onStopSharingClick();
-                });
-        builder.setNegativeButton(
-                "Cancel",
-                (dialog, which) -> {
-                    dismiss();
-                });
-        AlertDialog dialog = builder.create();
+                "Stop sharing", (dialog, which) -> sListener.onStopSharingClick());
+        builder.setNegativeButton("Cancel", (dialog, which) -> dismiss());
+        AlertDialog dialog = builder.setCustomTitle(customTitle).create();
         dialog.setCanceledOnTouchOutside(false);
         return dialog;
     }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
index 8f701a3..ffb0b88 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamPreference.java
@@ -16,6 +16,9 @@
 
 package com.android.settings.connecteddevice.audiosharing.audiostreams;
 
+import android.bluetooth.BluetoothLeAudioContentMetadata;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.content.Context;
 import android.util.AttributeSet;
 
@@ -24,11 +27,13 @@
 import com.android.settings.R;
 import com.android.settingslib.widget.TwoTargetPreference;
 
+import com.google.common.base.Strings;
+
 /**
  * Custom preference class for managing audio stream preferences with an optional lock icon. Extends
  * {@link TwoTargetPreference}.
  */
-public class AudioStreamPreference extends TwoTargetPreference {
+class AudioStreamPreference extends TwoTargetPreference {
     private boolean mIsConnected = false;
 
     /**
@@ -36,7 +41,7 @@
      *
      * @param isConnected Is this streams connected
      */
-    public void setIsConnected(
+    void setIsConnected(
             boolean isConnected, @Nullable OnPreferenceClickListener onPreferenceClickListener) {
         if (mIsConnected == isConnected
                 && getOnPreferenceClickListener() == onPreferenceClickListener) {
@@ -50,7 +55,7 @@
         notifyChanged();
     }
 
-    public AudioStreamPreference(Context context, @Nullable AttributeSet attrs) {
+    AudioStreamPreference(Context context, @Nullable AttributeSet attrs) {
         super(context, attrs);
         setIcon(R.drawable.ic_bt_audio_sharing);
     }
@@ -64,4 +69,34 @@
     protected int getSecondTargetResId() {
         return R.layout.preference_widget_lock;
     }
+
+    static AudioStreamPreference fromMetadata(
+            Context context, BluetoothLeBroadcastMetadata source) {
+        AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
+        preference.setTitle(getBroadcastName(source));
+        return preference;
+    }
+
+    static AudioStreamPreference fromReceiveState(
+            Context context, BluetoothLeBroadcastReceiveState state) {
+        AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
+        preference.setTitle(getBroadcastName(state));
+        return preference;
+    }
+
+    private static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
+        return source.getSubgroups().stream()
+                .map(s -> s.getContentMetadata().getProgramInfo())
+                .filter(i -> !Strings.isNullOrEmpty(i))
+                .findFirst()
+                .orElse("Broadcast Id: " + source.getBroadcastId());
+    }
+
+    private static String getBroadcastName(BluetoothLeBroadcastReceiveState state) {
+        return state.getSubgroupMetadata().stream()
+                .map(BluetoothLeAudioContentMetadata::getProgramInfo)
+                .filter(i -> !Strings.isNullOrEmpty(i))
+                .findFirst()
+                .orElse("Broadcast Id: " + state.getBroadcastId());
+    }
 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
index 788b253..84e753c 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsBroadcastAssistantCallback.java
@@ -32,7 +32,7 @@
     private static final String TAG = "AudioStreamsBroadcastAssistantCallback";
     private static final boolean DEBUG = BluetoothUtils.D;
 
-    private AudioStreamsProgressCategoryController mCategoryController;
+    private final AudioStreamsProgressCategoryController mCategoryController;
 
     public AudioStreamsBroadcastAssistantCallback(
             AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
@@ -52,6 +52,7 @@
                             + " state: "
                             + state);
         }
+        mCategoryController.handleSourceConnected(state);
     }
 
     @Override
@@ -94,7 +95,20 @@
 
     @Override
     public void onSourceAddFailed(
-            BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {}
+            BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "onSourceAddFailed() sink : "
+                            + sink.getAddress()
+                            + " source: "
+                            + source
+                            + " reason: "
+                            + reason);
+        }
+        mCategoryController.showToast(
+                String.format(Locale.US, "Failed to join broadcast, reason %d", reason));
+    }
 
     @Override
     public void onSourceAdded(BluetoothDevice sink, int sourceId, int reason) {
@@ -119,7 +133,7 @@
         if (DEBUG) {
             Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId());
         }
-        mCategoryController.addSourceFound(source);
+        mCategoryController.handleSourceFound(source);
     }
 
     @Override
@@ -127,7 +141,7 @@
         if (DEBUG) {
             Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId);
         }
-        mCategoryController.removeSourceLost(broadcastId);
+        mCategoryController.handleSourceLost(broadcastId);
     }
 
     @Override
@@ -137,8 +151,23 @@
     public void onSourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) {}
 
     @Override
-    public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {}
+    public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
+        Log.w(TAG, "onSourceRemoveFailed() sourceId : " + sourceId + " reason : " + 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) {}
+    public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
+        if (DEBUG) {
+            Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason);
+        }
+        mCategoryController.showToast(
+                String.format(
+                        Locale.US, "Source %d removed for sink %s", sourceId, sink.getAddress()));
+    }
 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
index a673cb4..a418415 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDashboardFragment.java
@@ -16,15 +16,25 @@
 
 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 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;
 
 public class AudioStreamsDashboardFragment extends DashboardFragment {
     private static final String TAG = "AudioStreamsDashboardFrag";
+    private static final boolean DEBUG = BluetoothUtils.D;
+    private AudioStreamsScanQrCodeController mAudioStreamsScanQrCodeController;
 
     public AudioStreamsDashboardFragment() {
         super();
@@ -59,7 +69,8 @@
     @Override
     public void onAttach(Context context) {
         super.onAttach(context);
-        use(AudioStreamsScanQrCodeController.class).setFragment(this);
+        mAudioStreamsScanQrCodeController = use(AudioStreamsScanQrCodeController.class);
+        mAudioStreamsScanQrCodeController.setFragment(this);
     }
 
     @Override
@@ -70,6 +81,34 @@
     @Override
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
-        // TODO(chelseahao): implementation.
+        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.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 (mAudioStreamsScanQrCodeController == null) {
+                    Log.w(TAG, "onActivityResult() AudioStreamsScanQrCodeController is null!");
+                    return;
+                }
+                mAudioStreamsScanQrCodeController.addSource(source);
+            }
+        }
     }
 }
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..5acbc1f
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
@@ -0,0 +1,156 @@
+/*
+ * 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.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.util.Log;
+
+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 java.util.List;
+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.
+ */
+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 : getActiveSinksOnAssistant(mBluetoothManager)) {
+                                if (DEBUG) {
+                                    Log.d(
+                                            TAG,
+                                            "addSource(): join broadcast broadcastId"
+                                                    + " : "
+                                                    + source.getBroadcastId()
+                                                    + " sink : "
+                                                    + sink.getAddress());
+                                }
+                                mLeBroadcastAssistant.addSource(sink, source, false);
+                            }
+                        });
+    }
+
+    /** Removes all sources from LE broadcasts associated for all active sinks. */
+    void removeSource() {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
+            return;
+        }
+        var unused =
+                ThreadUtils.postOnBackgroundThread(
+                        () -> {
+                            for (var sink : getActiveSinksOnAssistant(mBluetoothManager)) {
+                                if (DEBUG) {
+                                    Log.d(
+                                            TAG,
+                                            "removeSource(): remove all sources from sink : "
+                                                    + sink.getAddress());
+                                }
+                                var sources = mLeBroadcastAssistant.getAllSources(sink);
+                                if (!sources.isEmpty()) {
+                                    mLeBroadcastAssistant.removeSource(
+                                            sink, sources.get(0).getSourceId());
+                                }
+                            }
+                        });
+    }
+
+    /** Retrieves a list of all LE broadcast receive states from active sinks. */
+    List<BluetoothLeBroadcastReceiveState> getAllSources() {
+        if (mLeBroadcastAssistant == null) {
+            Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
+            return emptyList();
+        }
+        return getActiveSinksOnAssistant(mBluetoothManager).stream()
+                .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
+                .toList();
+    }
+
+    @Nullable
+    LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
+        return mLeBroadcastAssistant;
+    }
+
+    private static List<BluetoothDevice> getActiveSinksOnAssistant(
+            @Nullable LocalBluetoothManager manager) {
+        if (manager == null) {
+            Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!");
+            return emptyList();
+        }
+        return AudioSharingUtils.getActiveSinkOnAssistant(manager)
+                .map(
+                        cachedBluetoothDevice ->
+                                Stream.concat(
+                                                Stream.of(cachedBluetoothDevice.getDevice()),
+                                                cachedBluetoothDevice.getMemberDevice().stream()
+                                                        .map(CachedBluetoothDevice::getDevice))
+                                        .toList())
+                .orElse(emptyList());
+    }
+
+    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();
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index fef1e7b..6cf69c5 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -18,8 +18,6 @@
 
 import static java.util.Collections.emptyList;
 
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothLeAudioContentMetadata;
 import android.bluetooth.BluetoothLeBroadcastMetadata;
 import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.content.Context;
@@ -35,19 +33,12 @@
 import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 import com.android.settings.core.BasePreferenceController;
 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.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
-import java.util.stream.Stream;
 
 import javax.annotation.Nullable;
 
@@ -58,17 +49,17 @@
 
     private final Executor mExecutor;
     private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback;
-    private final LocalBluetoothManager mBluetoothManager;
+    private final AudioStreamsHelper mAudioStreamsHelper;
     private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
     private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
             new ConcurrentHashMap<>();
-    private @Nullable AudioStreamsProgressCategoryPreference mCategoryPreference;
+    private AudioStreamsProgressCategoryPreference mCategoryPreference;
 
     public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
         super(context, preferenceKey);
         mExecutor = Executors.newSingleThreadExecutor();
-        mBluetoothManager = Utils.getLocalBtManager(mContext);
-        mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager);
+        mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(mContext));
+        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
         mBroadcastAssistantCallback = new AudioStreamsBroadcastAssistantCallback(this);
     }
 
@@ -104,14 +95,10 @@
                     // Display currently connected streams
                     var unused =
                             ThreadUtils.postOnBackgroundThread(
-                                    () -> {
-                                        for (var sink :
-                                                getActiveSinksOnAssistant(mBluetoothManager)) {
-                                            mLeBroadcastAssistant
-                                                    .getAllSources(sink)
-                                                    .forEach(this::addSourceConnected);
-                                        }
-                                    });
+                                    () ->
+                                            mAudioStreamsHelper
+                                                    .getAllSources()
+                                                    .forEach(this::handleSourceConnected));
                 });
     }
 
@@ -140,31 +127,36 @@
                 });
     }
 
-    void addSourceFound(BluetoothLeBroadcastMetadata source) {
-        Preference.OnPreferenceClickListener onClickListener =
+    void handleSourceFound(BluetoothLeBroadcastMetadata source) {
+        Preference.OnPreferenceClickListener addSourceOrShowDialog =
                 preference -> {
                     if (DEBUG) {
                         Log.d(TAG, "preferenceClicked(): attempt to join broadcast");
                     }
-
-                    // TODO(chelseahao): add source to sink
+                    if (source.isEncrypted()) {
+                        ThreadUtils.postOnMainThread(
+                                () -> launchPasswordDialog(source, preference));
+                    } else {
+                        mAudioStreamsHelper.addSource(source);
+                    }
                     return true;
                 };
         mBroadcastIdToPreferenceMap.computeIfAbsent(
                 source.getBroadcastId(),
                 k -> {
-                    var p = createPreference(source, onClickListener);
+                    var preference = AudioStreamPreference.fromMetadata(mContext, source);
                     ThreadUtils.postOnMainThread(
                             () -> {
+                                preference.setIsConnected(false, addSourceOrShowDialog);
                                 if (mCategoryPreference != null) {
-                                    mCategoryPreference.addPreference(p);
+                                    mCategoryPreference.addPreference(preference);
                                 }
                             });
-                    return p;
+                    return preference;
                 });
     }
 
-    void removeSourceLost(int broadcastId) {
+    void handleSourceLost(int broadcastId) {
         var toRemove = mBroadcastIdToPreferenceMap.remove(broadcastId);
         if (toRemove != null) {
             ThreadUtils.postOnMainThread(
@@ -174,92 +166,43 @@
                         }
                     });
         }
-        // TODO(chelseahao): remove source from sink
+        mAudioStreamsHelper.removeSource();
     }
 
-    private void addSourceConnected(BluetoothLeBroadcastReceiveState state) {
+    void handleSourceConnected(BluetoothLeBroadcastReceiveState state) {
+        // TODO(chelseahao): only continue when the state indicates a successful connection
         mBroadcastIdToPreferenceMap.compute(
                 state.getBroadcastId(),
                 (k, v) -> {
-                    if (v == null) {
-                        // Create a new preference as the source has not been added.
-                        var p = createPreference(state);
-                        ThreadUtils.postOnMainThread(
-                                () -> {
-                                    if (mCategoryPreference != null) {
-                                        mCategoryPreference.addPreference(p);
-                                    }
-                                });
-                        return p;
-                    } else {
-                        // This source has been added either by scanning, or it's currently
-                        // connected to another active sink. Update its connection status to true
-                        // if needed.
-                        ThreadUtils.postOnMainThread(() -> v.setIsConnected(true, null));
-                        return v;
-                    }
+                    // True if this source has been added either by scanning, or it's currently
+                    // connected to another active sink.
+                    boolean existed = v != null;
+                    AudioStreamPreference preference =
+                            existed ? v : AudioStreamPreference.fromReceiveState(mContext, state);
+
+                    ThreadUtils.postOnMainThread(
+                            () -> {
+                                preference.setIsConnected(
+                                        true, p -> launchDetailFragment((AudioStreamPreference) p));
+                                if (mCategoryPreference != null && !existed) {
+                                    mCategoryPreference.addPreference(preference);
+                                }
+                            });
+
+                    return preference;
                 });
     }
 
-    private AudioStreamPreference createPreference(
-            BluetoothLeBroadcastMetadata source,
-            Preference.OnPreferenceClickListener onPreferenceClickListener) {
-        AudioStreamPreference preference = new AudioStreamPreference(mContext, /* attrs= */ null);
-        preference.setTitle(
-                source.getSubgroups().stream()
-                        .map(s -> s.getContentMetadata().getProgramInfo())
-                        .filter(i -> !Strings.isNullOrEmpty(i))
-                        .findFirst()
-                        .orElse("Broadcast Id: " + source.getBroadcastId()));
-        preference.setIsConnected(false, onPreferenceClickListener);
-        return preference;
-    }
-
-    private AudioStreamPreference createPreference(BluetoothLeBroadcastReceiveState state) {
-        AudioStreamPreference preference = new AudioStreamPreference(mContext, /* attrs= */ null);
-        preference.setTitle(
-                state.getSubgroupMetadata().stream()
-                        .map(BluetoothLeAudioContentMetadata::getProgramInfo)
-                        .filter(i -> !Strings.isNullOrEmpty(i))
-                        .findFirst()
-                        .orElse("Broadcast Id: " + state.getBroadcastId()));
-        preference.setIsConnected(true, null);
-        return preference;
-    }
-
-    private static List<BluetoothDevice> getActiveSinksOnAssistant(LocalBluetoothManager manager) {
-        if (manager == null) {
-            Log.w(TAG, "getActiveSinksOnAssistant(): LocalBluetoothManager is null!");
-            return emptyList();
-        }
-        return AudioSharingUtils.getActiveSinkOnAssistant(manager)
-                .map(
-                        cachedBluetoothDevice ->
-                                Stream.concat(
-                                                Stream.of(cachedBluetoothDevice.getDevice()),
-                                                cachedBluetoothDevice.getMemberDevice().stream()
-                                                        .map(CachedBluetoothDevice::getDevice))
-                                        .toList())
-                .orElse(emptyList());
-    }
-
-    private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant(
-            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();
-    }
-
     void showToast(String msg) {
         AudioSharingUtils.toastMessage(mContext, msg);
     }
+
+    private boolean launchDetailFragment(AudioStreamPreference preference) {
+        // TODO(chelseahao): impl
+        return true;
+    }
+
+    private void launchPasswordDialog(BluetoothLeBroadcastMetadata source, Preference preference) {
+        // TODO(chelseahao): impl
+    }
 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
index 12b46e5..549e725 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
@@ -16,6 +16,7 @@
 
 package com.android.settings.connecteddevice.audiosharing.audiostreams;
 
+import android.bluetooth.BluetoothLeBroadcastMetadata;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 import android.content.Intent;
@@ -41,10 +42,10 @@
 
 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 static final int REQUEST_SCAN_BT_BROADCAST_QR_CODE = 0;
     private final BluetoothCallback mBluetoothCallback =
             new BluetoothCallback() {
                 @Override
@@ -57,12 +58,14 @@
             };
 
     private final LocalBluetoothManager mLocalBtManager;
+    private final AudioStreamsHelper mAudioStreamsHelper;
     private AudioStreamsDashboardFragment mFragment;
     private Preference mPreference;
 
     public AudioStreamsScanQrCodeController(Context context, String preferenceKey) {
         super(context, preferenceKey);
         mLocalBtManager = Utils.getLocalBtManager(mContext);
+        mAudioStreamsHelper = new AudioStreamsHelper(mLocalBtManager);
     }
 
     public void setFragment(AudioStreamsDashboardFragment fragment) {
@@ -121,6 +124,10 @@
                 });
     }
 
+    void addSource(BluetoothLeBroadcastMetadata source) {
+        mAudioStreamsHelper.addSource(source);
+    }
+
     private void updateVisibility() {
         ThreadUtils.postOnBackgroundThread(
                 () -> {
diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
index a70d7d8..3321d50 100644
--- a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
+++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
@@ -57,6 +57,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Base fragment class for profile settings.
@@ -295,8 +296,7 @@
                 personalFragmentConstructor,
                 workFragmentConstructor,
                 privateFragmentConstructor,
-                new PrivateSpaceInfoProvider() {},
-                new ManagedProfileInfoProvider() {});
+                new PrivateSpaceInfoProvider() {});
     }
 
     /**
@@ -309,36 +309,35 @@
             FragmentConstructor personalFragmentConstructor,
             FragmentConstructor workFragmentConstructor,
             FragmentConstructor privateFragmentConstructor,
-            PrivateSpaceInfoProvider privateSpaceInfoProvider,
-            ManagedProfileInfoProvider managedProfileInfoProvider) {
+            PrivateSpaceInfoProvider privateSpaceInfoProvider) {
         Fragment[] result = new Fragment[0];
         ArrayList<Fragment> fragments = new ArrayList<>();
 
         try {
-            final Bundle personalOnly = bundle != null ? bundle : new Bundle();
-            personalOnly.putInt(EXTRA_PROFILE, ProfileType.PERSONAL);
-            final Fragment personalFragment =
-                    personalFragmentConstructor.constructAndGetFragment();
-            personalFragment.setArguments(personalOnly);
-            fragments.add(personalFragment);
+            UserManager userManager = context.getSystemService(UserManager.class);
+            List<UserInfo> userInfos = userManager.getProfiles(UserHandle.myUserId());
 
-            if (managedProfileInfoProvider.isManagedProfilePresent(context)) {
-                final Bundle workOnly = bundle != null ? bundle.deepCopy() : new Bundle();
-                workOnly.putInt(EXTRA_PROFILE, ProfileType.WORK);
-                final Fragment workFragment =
-                        workFragmentConstructor.constructAndGetFragment();
-                workFragment.setArguments(workOnly);
-                fragments.add(workFragment);
-            }
-
-            if (Flags.allowPrivateProfile()
-                    && !privateSpaceInfoProvider.isPrivateSpaceLocked(context)) {
-                final Bundle privateOnly = bundle != null ? bundle.deepCopy() : new Bundle();
-                privateOnly.putInt(EXTRA_PROFILE, ProfileType.PRIVATE);
-                final Fragment privateFragment =
-                        privateFragmentConstructor.constructAndGetFragment();
-                privateFragment.setArguments(privateOnly);
-                fragments.add(privateFragment);
+            for (UserInfo userInfo : userInfos) {
+                if (userInfo.getUserHandle().isSystem()) {
+                    fragments.add(createAndGetFragment(
+                            ProfileType.PERSONAL,
+                            bundle != null ? bundle : new Bundle(),
+                            personalFragmentConstructor));
+                } else if (userInfo.isManagedProfile()) {
+                    fragments.add(createAndGetFragment(
+                            ProfileType.WORK,
+                            bundle != null ? bundle.deepCopy() : new Bundle(),
+                            workFragmentConstructor));
+                } else if (Flags.allowPrivateProfile() && userInfo.isPrivateProfile()) {
+                    if (!privateSpaceInfoProvider.isPrivateSpaceLocked(context)) {
+                        fragments.add(createAndGetFragment(
+                                ProfileType.PRIVATE,
+                                bundle != null ? bundle.deepCopy() : new Bundle(),
+                                privateFragmentConstructor));
+                    }
+                } else {
+                    Log.d(TAG, "Not showing tab for unsupported user");
+                }
             }
 
             result = new Fragment[fragments.size()];
@@ -350,6 +349,14 @@
         return result;
     }
 
+    private static Fragment createAndGetFragment(
+            @ProfileType int profileType, Bundle bundle, FragmentConstructor fragmentConstructor) {
+        bundle.putInt(EXTRA_PROFILE, profileType);
+        final Fragment fragment = fragmentConstructor.constructAndGetFragment();
+        fragment.setArguments(bundle);
+        return fragment;
+    }
+
     interface FragmentConstructor {
         Fragment constructAndGetFragment();
     }
@@ -360,13 +367,6 @@
         }
     }
 
-    interface ManagedProfileInfoProvider {
-        default boolean isManagedProfilePresent(Context context) {
-            return Utils.doesProfileOfTypeExists(
-                    context.getSystemService(UserManager.class), ProfileType.WORK);
-        }
-    }
-
     static class ViewPagerAdapter extends FragmentStateAdapter {
 
         private final Fragment[] mChildFragments;
diff --git a/src/com/android/settings/network/apn/ApnEditPageProvider.kt b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
index 5026958..bb19c59 100644
--- a/src/com/android/settings/network/apn/ApnEditPageProvider.kt
+++ b/src/com/android/settings/network/apn/ApnEditPageProvider.kt
@@ -150,6 +150,7 @@
             SettingsOutlinedTextField(
                 value = apnData.mmsc,
                 label = stringResource(R.string.apn_mmsc),
+                errorMessage = validateMMSC(apnData.mmsc, context),
                 enabled = apnData.mmscEnabled
             ) { apnData = apnData.copy(mmsc = it) }
             SettingsOutlinedTextField(
diff --git a/src/com/android/settings/network/apn/ApnStatus.kt b/src/com/android/settings/network/apn/ApnStatus.kt
index d81a8e3..668ea9b 100644
--- a/src/com/android/settings/network/apn/ApnStatus.kt
+++ b/src/com/android/settings/network/apn/ApnStatus.kt
@@ -239,7 +239,12 @@
     if (apnData.customizedConfig.readOnlyApn) {
         return true
     }
-    val errorMsg = validateApnData(apnData, context)
+    var errorMsg = validateApnData(apnData, context)
+    if (errorMsg != null) {
+        //TODO: showError(this)
+        return false
+    }
+    errorMsg = validateMMSC(apnData.mmsc, context)
     if (errorMsg != null) {
         //TODO: showError(this)
         return false
@@ -529,4 +534,9 @@
 fun deleteApn(uri: Uri, context: Context) {
     val contentResolver = context.contentResolver
     contentResolver.delete(uri, null, null)
+}
+
+fun validateMMSC(mmsc: String, context: Context): String? {
+    return if (mmsc.matches(Regex("^https?:\\/\\/.+"))) null
+            else context.resources.getString(R.string.error_mmsc_valid)
 }
\ No newline at end of file
diff --git a/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java b/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
index 77d57f7..9e905f2 100644
--- a/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
+++ b/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
@@ -52,16 +52,17 @@
     private static final String TITLE_INDEX = "title_index";
     private static final int DELAY_BETWEEN_SCREENS = 5000; // 5 seconds in millis
     private static final int ANIMATION_DURATION_MILLIS = 500;
+    private static final int HEADER_TEXT_MAX_LINES = 4;
     private GlifLayout mRootView;
     private Handler mHandler;
     private int mScreenTitleIndex;
     private static final List<Pair<Integer, Integer>> HEADER_IMAGE_PAIRS =
             ImmutableList.of(
-                    new Pair(R.string.privatespace_apps_hidden_title,
+                    new Pair(R.string.private_space_notifications_hidden_title,
                             R.drawable.privatespace_setup_flow_placeholder),
-                    new Pair(R.string.privatespace_access_from_apps_title,
+                    new Pair(R.string.private_space_share_photos_title,
                             R.drawable.privatespace_setup_flow_placeholder),
-                    new Pair(R.string.privatespace_system_apps_installed_title,
+                    new Pair(R.string.private_space_apps_installed_title,
                             R.drawable.privatespace_setup_flow_placeholder));
 
     private Runnable mUpdateScreenResources =
@@ -116,6 +117,7 @@
         mRootView =
                 (GlifLayout)
                         inflater.inflate(R.layout.privatespace_advancing_screen, container, false);
+        mRootView.getHeaderTextView().setMaxLines(HEADER_TEXT_MAX_LINES);
         updateHeaderAndImage();
         mHandler = new Handler(Looper.getMainLooper());
         mHandler.postDelayed(mUpdateScreenResources, DELAY_BETWEEN_SCREENS);
diff --git a/src/com/android/settings/privatespace/PrivateProfileCreationError.java b/src/com/android/settings/privatespace/PrivateProfileCreationError.java
index 91c35a3..74beef4 100644
--- a/src/com/android/settings/privatespace/PrivateProfileCreationError.java
+++ b/src/com/android/settings/privatespace/PrivateProfileCreationError.java
@@ -47,14 +47,14 @@
         final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
         mixin.setPrimaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_tryagain_label)
+                        .setText(R.string.private_space_tryagain_label)
                         .setListener(onTryAgain())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
                         .build());
         mixin.setSecondaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_cancel_label)
+                        .setText(R.string.private_space_cancel_label)
                         .setListener(onCancel())
                         .setButtonType(FooterButton.ButtonType.CANCEL)
                         .setTheme(
diff --git a/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java b/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java
index ac3518e..e445a7f 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java
@@ -51,7 +51,7 @@
         final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
         mixin.setPrimaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_tryagain_label)
+                        .setText(R.string.private_space_tryagain_label)
                         .setListener(nextScreen())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
diff --git a/src/com/android/settings/privatespace/PrivateSpaceEducation.java b/src/com/android/settings/privatespace/PrivateSpaceEducation.java
index 887854b..eb562af 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceEducation.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceEducation.java
@@ -50,14 +50,14 @@
         final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
         mixin.setPrimaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_setup_button_label)
+                        .setText(R.string.private_space_setup_button_label)
                         .setListener(onSetup())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
                         .build());
         mixin.setSecondaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_cancel_label)
+                        .setText(R.string.private_space_cancel_label)
                         .setListener(onCancel())
                         .setButtonType(FooterButton.ButtonType.CANCEL)
                         .setTheme(
diff --git a/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java b/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
index dd4bc7f..a256ea6 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
@@ -60,14 +60,14 @@
         final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
         mixin.setPrimaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_use_screenlock_label)
+                        .setText(R.string.private_space_use_screenlock_label)
                         .setListener(onClickUse())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
                         .build());
         mixin.setSecondaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_set_lock_label)
+                        .setText(R.string.private_space_set_lock_label)
                         .setListener(onClickNewLock())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(
diff --git a/src/com/android/settings/privatespace/SetupSuccessFragment.java b/src/com/android/settings/privatespace/SetupSuccessFragment.java
index fc2ce46..0b1b9d9 100644
--- a/src/com/android/settings/privatespace/SetupSuccessFragment.java
+++ b/src/com/android/settings/privatespace/SetupSuccessFragment.java
@@ -55,7 +55,7 @@
         final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
         mixin.setPrimaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_done_label)
+                        .setText(R.string.private_space_done_label)
                         .setListener(onClickNext())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
@@ -104,6 +104,7 @@
     }
 
     private void accessPrivateSpaceToast() {
-        Toast.makeText(getContext(), R.string.scrolldown_to_access, Toast.LENGTH_SHORT).show();
+        Toast.makeText(getContext(), R.string.private_space_scrolldown_to_access,
+                Toast.LENGTH_SHORT).show();
     }
 }
diff --git a/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java b/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java
index 04101b2..54b0374 100644
--- a/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java
+++ b/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java
@@ -192,7 +192,7 @@
         new AlertDialog.Builder(mContext)
                   .setMessage(R.string.private_space_new_lock_title)
                   .setPositiveButton(
-                            R.string.privatespace_set_lock_label,
+                            R.string.private_space_set_lock_label,
                             (dialog, which) -> {
                                 Intent intent = new Intent(mContext,
                                           PrivateProfileContextHelperActivity.class);
@@ -201,7 +201,7 @@
                                           UNUNIFY_PRIVATE_LOCK_FROM_DEVICE_REQUEST,
                                           /*Options*/ null, mUserHandle);
                             })
-                  .setNegativeButton(R.string.privatespace_cancel_label,
+                  .setNegativeButton(R.string.private_space_cancel_label,
                             (DialogInterface dialog, int which) -> {
                                 mUnifyProfile.setChecked(true);
                                 dialog.dismiss();
diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
index 7ab836b..a6cf5cc 100644
--- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt
+++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
@@ -32,6 +32,7 @@
 import com.android.settings.spa.app.specialaccess.InstallUnknownAppsListProvider
 import com.android.settings.spa.app.specialaccess.LongBackgroundTasksAppListProvider
 import com.android.settings.spa.app.specialaccess.MediaManagementAppsAppListProvider
+import com.android.settings.spa.app.specialaccess.MediaRoutingControlAppListProvider
 import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListProvider
 import com.android.settings.spa.app.specialaccess.NfcTagAppsSettingsProvider
 import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider
@@ -64,6 +65,7 @@
             AllFilesAccessAppListProvider,
             DisplayOverOtherAppsAppListProvider,
             MediaManagementAppsAppListProvider,
+            MediaRoutingControlAppListProvider,
             ModifySystemSettingsAppListProvider,
             UseFullScreenIntentAppListProvider,
             PictureInPictureListProvider,
diff --git a/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt b/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt
new file mode 100644
index 0000000..913da65
--- /dev/null
+++ b/src/com/android/settings/spa/app/appinfo/AppArchiveButton.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.spa.app.appinfo
+
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInstaller
+import android.os.UserHandle
+import android.util.Log
+import android.widget.Toast
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.CloudUpload
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.settings.R
+import com.android.settingslib.spa.widget.button.ActionButton
+import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+
+class AppArchiveButton(packageInfoPresenter: PackageInfoPresenter) {
+    private companion object {
+        private const val LOG_TAG = "AppArchiveButton"
+        private const val INTENT_ACTION = "com.android.settings.archive.action"
+    }
+
+    private val context = packageInfoPresenter.context
+    private val appButtonRepository = AppButtonRepository(context)
+    private val userPackageManager = packageInfoPresenter.userPackageManager
+    private val packageInstaller = userPackageManager.packageInstaller
+    private val packageName = packageInfoPresenter.packageName
+    private val userHandle = UserHandle.of(packageInfoPresenter.userId)
+    private var broadcastReceiverIsCreated = false
+
+    @Composable
+    fun getActionButton(app: ApplicationInfo): ActionButton {
+        if (!broadcastReceiverIsCreated) {
+            val intentFilter = IntentFilter(INTENT_ACTION)
+            DisposableBroadcastReceiverAsUser(intentFilter, userHandle) { intent ->
+                if (app.packageName == intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)) {
+                    onReceive(intent, app)
+                }
+            }
+            broadcastReceiverIsCreated = true
+        }
+        return ActionButton(
+            text = context.getString(R.string.archive),
+            imageVector = Icons.Outlined.CloudUpload,
+            enabled = remember(app) {
+                flow {
+                    emit(
+                        app.isActionButtonEnabled() && appButtonRepository.isAllowUninstallOrArchive(
+                            context,
+                            app
+                        )
+                    )
+                }.flowOn(Dispatchers.Default)
+            }.collectAsStateWithLifecycle(false).value
+        ) { onArchiveClicked(app) }
+    }
+
+    private fun ApplicationInfo.isActionButtonEnabled(): Boolean {
+        return !isArchived
+            && userPackageManager.isAppArchivable(packageName)
+            // We apply the same device policy for both the uninstallation and archive
+            // button.
+            && !appButtonRepository.isUninstallBlockedByAdmin(this)
+    }
+
+    private fun onArchiveClicked(app: ApplicationInfo) {
+        val intent = Intent(INTENT_ACTION)
+        intent.setPackage(context.packageName)
+        val pendingIntent = PendingIntent.getBroadcastAsUser(
+            context, 0, intent,
+            // FLAG_MUTABLE is required by PackageInstaller#requestArchive
+            PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE,
+            userHandle
+        )
+        try {
+            packageInstaller.requestArchive(app.packageName, pendingIntent.intentSender, 0)
+        } catch (e: Exception) {
+            Log.e(LOG_TAG, "Request archive failed", e)
+            Toast.makeText(
+                context,
+                context.getString(R.string.archiving_failed),
+                Toast.LENGTH_SHORT
+            ).show()
+        }
+    }
+
+    private fun onReceive(intent: Intent, app: ApplicationInfo) {
+        when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Int.MIN_VALUE)) {
+            PackageInstaller.STATUS_SUCCESS -> {
+                val appLabel = userPackageManager.getApplicationLabel(app)
+                Toast.makeText(
+                    context,
+                    context.getString(R.string.archiving_succeeded, appLabel),
+                    Toast.LENGTH_SHORT
+                ).show()
+            }
+
+            else -> {
+                Log.e(LOG_TAG, "Request archiving failed for $packageName with code $status")
+                Toast.makeText(
+                    context,
+                    context.getString(R.string.archiving_failed),
+                    Toast.LENGTH_SHORT
+                ).show()
+            }
+        }
+    }
+}
diff --git a/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt b/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt
index 2383ddb..f01c31c 100644
--- a/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppButtonRepository.kt
@@ -19,6 +19,7 @@
 import android.app.ActivityManager
 import android.content.ComponentName
 import android.content.Context
+import android.content.om.OverlayManager
 import android.content.pm.ApplicationInfo
 import android.content.pm.PackageManager
 import android.content.pm.ResolveInfo
@@ -26,7 +27,9 @@
 import com.android.settingslib.RestrictedLockUtilsInternal
 import com.android.settingslib.Utils
 import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
+import com.android.settingslib.spaprivileged.model.app.hasFlag
 import com.android.settingslib.spaprivileged.model.app.isDisallowControl
+import com.android.settingslib.spaprivileged.model.app.userHandle
 import com.android.settingslib.spaprivileged.model.app.userId
 
 class AppButtonRepository(private val context: Context) {
@@ -77,6 +80,55 @@
         false
     }
 
+    /** Gets whether a package can be uninstalled or archived. */
+    fun isAllowUninstallOrArchive(
+        context: Context, app: ApplicationInfo
+    ): Boolean {
+        val overlayManager = checkNotNull(context.getSystemService(OverlayManager::class.java))
+        when {
+            !app.hasFlag(ApplicationInfo.FLAG_INSTALLED) && !app.isArchived -> return false
+
+            com.android.settings.Utils.isProfileOrDeviceOwner(
+                context.devicePolicyManager, app.packageName, app.userId
+            ) -> return false
+
+            isDisallowControl(app) -> return false
+
+            uninstallDisallowedDueToHomeApp(app.packageName) -> return false
+
+            // Resource overlays can be uninstalled iff they are public (installed on /data) and
+            // disabled. ("Enabled" means they are in use by resource management.)
+            app.isEnabledResourceOverlay(overlayManager) -> return false
+
+            else -> return true
+        }
+    }
+
+    /**
+     * Checks whether the given package cannot be uninstalled due to home app restrictions.
+     *
+     * Home launcher apps need special handling, we can't allow uninstallation of the only home
+     * app, and we don't want to allow uninstallation of an explicitly preferred one -- the user
+     * can go to Home settings and pick a different one, after which we'll permit uninstallation
+     * of the now-not-default one.
+     */
+    private fun uninstallDisallowedDueToHomeApp(packageName: String): Boolean {
+        val homePackageInfo = getHomePackageInfo()
+        return when {
+            packageName !in homePackageInfo.homePackages -> false
+
+            // Disallow uninstall when this is the only home app.
+            homePackageInfo.homePackages.size == 1 -> true
+
+            // Disallow if this is the explicit default home app.
+            else -> packageName == homePackageInfo.currentDefaultHome?.packageName
+        }
+    }
+
+    private fun ApplicationInfo.isEnabledResourceOverlay(overlayManager: OverlayManager): Boolean =
+        isResourceOverlay &&
+            overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true
+
     data class HomePackages(
         val homePackages: Set<String>,
         val currentDefaultHome: ComponentName?,
diff --git a/src/com/android/settings/spa/app/appinfo/AppButtons.kt b/src/com/android/settings/spa/app/appinfo/AppButtons.kt
index 307ff11..f6fafd7 100644
--- a/src/com/android/settings/spa/app/appinfo/AppButtons.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppButtons.kt
@@ -30,7 +30,10 @@
 /**
  * @param featureFlags can be overridden in tests
  */
-fun AppButtons(packageInfoPresenter: PackageInfoPresenter, featureFlags: FeatureFlags = FeatureFlagsImpl()) {
+fun AppButtons(
+    packageInfoPresenter: PackageInfoPresenter,
+    featureFlags: FeatureFlags = FeatureFlagsImpl()
+) {
     if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return
     val presenter = remember { AppButtonsPresenter(packageInfoPresenter, featureFlags) }
     ActionButtons(actionButtons = presenter.getActionButtons())
@@ -49,6 +52,7 @@
     private val appUninstallButton = AppUninstallButton(packageInfoPresenter)
     private val appClearButton = AppClearButton(packageInfoPresenter)
     private val appForceStopButton = AppForceStopButton(packageInfoPresenter)
+    private val appArchiveButton = AppArchiveButton(packageInfoPresenter)
 
     @Composable
     fun getActionButtons() =
@@ -58,7 +62,11 @@
 
     @Composable
     private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull(
-        if (featureFlags.archiving()) null else appLaunchButton.getActionButton(app),
+        if (featureFlags.archiving()) {
+            appArchiveButton.getActionButton(app)
+        } else {
+            appLaunchButton.getActionButton(app)
+        },
         appInstallButton.getActionButton(app),
         appDisableButton.getActionButton(app),
         appUninstallButton.getActionButton(app),
diff --git a/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt b/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
index 5f6f097..ce72840 100644
--- a/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppUninstallButton.kt
@@ -18,7 +18,6 @@
 
 import android.app.settings.SettingsEnums
 import android.content.Intent
-import android.content.om.OverlayManager
 import android.content.pm.ApplicationInfo
 import android.os.UserHandle
 import android.os.UserManager
@@ -28,11 +27,8 @@
 import androidx.compose.ui.res.vectorResource
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.settings.R
-import com.android.settings.Utils
 import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd
 import com.android.settingslib.spa.widget.button.ActionButton
-import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager
-import com.android.settingslib.spaprivileged.model.app.hasFlag
 import com.android.settingslib.spaprivileged.model.app.isActiveAdmin
 import com.android.settingslib.spaprivileged.model.app.userHandle
 import kotlinx.coroutines.Dispatchers
@@ -42,7 +38,6 @@
 class AppUninstallButton(private val packageInfoPresenter: PackageInfoPresenter) {
     private val context = packageInfoPresenter.context
     private val appButtonRepository = AppButtonRepository(context)
-    private val overlayManager = context.getSystemService(OverlayManager::class.java)!!
     private val userManager = context.getSystemService(UserManager::class.java)!!
 
     @Composable
@@ -51,49 +46,6 @@
         return uninstallButton(app)
     }
 
-    /** Gets whether a package can be uninstalled. */
-    private fun isUninstallButtonEnabled(app: ApplicationInfo): Boolean = when {
-        !app.hasFlag(ApplicationInfo.FLAG_INSTALLED) -> false
-
-        Utils.isProfileOrDeviceOwner(
-            context.devicePolicyManager, app.packageName, packageInfoPresenter.userId) -> false
-
-        appButtonRepository.isDisallowControl(app) -> false
-
-        uninstallDisallowedDueToHomeApp(app.packageName) -> false
-
-        // Resource overlays can be uninstalled iff they are public (installed on /data) and
-        // disabled. ("Enabled" means they are in use by resource management.)
-        app.isEnabledResourceOverlay() -> false
-
-        else -> true
-    }
-
-    /**
-     * Checks whether the given package cannot be uninstalled due to home app restrictions.
-     *
-     * Home launcher apps need special handling, we can't allow uninstallation of the only home
-     * app, and we don't want to allow uninstallation of an explicitly preferred one -- the user
-     * can go to Home settings and pick a different one, after which we'll permit uninstallation
-     * of the now-not-default one.
-     */
-    private fun uninstallDisallowedDueToHomeApp(packageName: String): Boolean {
-        val homePackageInfo = appButtonRepository.getHomePackageInfo()
-        return when {
-            packageName !in homePackageInfo.homePackages -> false
-
-            // Disallow uninstall when this is the only home app.
-            homePackageInfo.homePackages.size == 1 -> true
-
-            // Disallow if this is the explicit default home app.
-            else -> packageName == homePackageInfo.currentDefaultHome?.packageName
-        }
-    }
-
-    private fun ApplicationInfo.isEnabledResourceOverlay(): Boolean =
-        isResourceOverlay &&
-            overlayManager.getOverlayInfo(packageName, userHandle)?.isEnabled == true
-
     @Composable
     private fun uninstallButton(app: ApplicationInfo) = ActionButton(
         text = if (isCloneApp(app)) context.getString(R.string.delete) else
@@ -101,7 +53,7 @@
         imageVector = ImageVector.vectorResource(R.drawable.ic_settings_delete),
         enabled = remember(app) {
             flow {
-                emit(isUninstallButtonEnabled(app))
+                emit(appButtonRepository.isAllowUninstallOrArchive(context, app))
             }.flowOn(Dispatchers.Default)
         }.collectAsStateWithLifecycle(false).value,
     ) { onUninstallClicked(app) }
diff --git a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
index a6bd8f0..8c802d1 100644
--- a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
+++ b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
@@ -87,19 +87,9 @@
     ).filter(::isInterestedAppChange).filter(::isForThisApp)
 
     @VisibleForTesting
-    fun isInterestedAppChange(intent: Intent) = when {
-        intent.action != Intent.ACTION_PACKAGE_REMOVED -> true
-
-        // filter out the fully removed case, in which the page will be closed, so no need to
-        // refresh
-        intent.getBooleanExtra(Intent.EXTRA_DATA_REMOVED, false) -> false
-
-        // filter out the updates are uninstalled (system app), which will followed by a replacing
-        // broadcast, we can refresh at that time
-        intent.getBooleanExtra(Intent.EXTRA_REPLACING, false) -> false
-
-        else -> true // App archived
-    }
+    fun isInterestedAppChange(intent: Intent) =
+        intent.action != Intent.ACTION_PACKAGE_REMOVED ||
+            intent.getBooleanExtra(Intent.EXTRA_ARCHIVAL, false)
 
     val flow: StateFlow<PackageInfo?> = merge(flowOf(null), appChangeFlow)
         .map { getPackageInfo() }
diff --git a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
index c990927..3e48aa5 100644
--- a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
+++ b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
@@ -18,6 +18,7 @@
 
 import android.Manifest
 import android.app.AlarmManager
+import android.app.AppOpsManager
 import android.app.compat.CompatChanges
 import android.app.settings.SettingsEnums
 import android.content.Context
@@ -56,6 +57,7 @@
     override val pageTitleResId = R.string.alarms_and_reminders_title
     override val switchTitleResId = R.string.alarms_and_reminders_switch_title
     override val footerResId = R.string.alarms_and_reminders_footer_title
+    override val enhancedConfirmationKey: String = AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM
 
     override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
         userIdFlow.map { userId ->
diff --git a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
index 7f63e38..dc98330 100644
--- a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
+++ b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
@@ -18,6 +18,7 @@
 
 import android.Manifest
 import android.app.AppGlobals
+import android.app.AppOpsManager
 import android.app.AppOpsManager.MODE_DEFAULT
 import android.app.AppOpsManager.OP_REQUEST_INSTALL_PACKAGES
 import android.content.Context
@@ -55,6 +56,7 @@
             UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
             UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY,
         )
+    override val enhancedConfirmationKey: String = AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES
 
     override fun transformItem(app: ApplicationInfo) =
         InstallUnknownAppsRecord(
diff --git a/src/com/android/settings/spa/app/specialaccess/MediaRoutingControl.kt b/src/com/android/settings/spa/app/specialaccess/MediaRoutingControl.kt
new file mode 100644
index 0000000..91c4928
--- /dev/null
+++ b/src/com/android/settings/spa/app/specialaccess/MediaRoutingControl.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.spa.app.specialaccess
+
+import android.Manifest
+import android.app.AppOpsManager
+import android.app.role.RoleManager
+import android.app.settings.SettingsEnums
+import android.companion.AssociationRequest
+import android.content.Context
+import com.android.settings.R
+import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
+import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel
+import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
+import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider
+
+object MediaRoutingControlAppListProvider : TogglePermissionAppListProvider {
+    override val permissionType = "MediaRoutingControl"
+    override fun createModel(context: Context) = MediaRoutingControlAppsListModel(context)
+}
+
+class MediaRoutingControlAppsListModel(context: Context) : AppOpPermissionListModel(context) {
+    override val pageTitleResId = R.string.media_routing_control_title
+    override val switchTitleResId = R.string.allow_media_routing_control
+    override val footerResId = R.string.allow_media_routing_description
+    override val appOp = AppOpsManager.OP_MEDIA_ROUTING_CONTROL
+    override val permission = Manifest.permission.MEDIA_ROUTING_CONTROL
+    override val setModeByUid = true
+    private val roleManager = context.getSystemService(RoleManager::class.java)
+
+    override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
+        super.setAllowed(record, newAllowed)
+        logPermissionToggleAction(newAllowed)
+    }
+
+    override fun isChangeable(record: AppOpPermissionRecord): Boolean {
+        return super.isChangeable(record) && (this.roleManager
+                ?.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)
+                ?.contains(record.app.packageName) == true)
+    }
+
+    private fun logPermissionToggleAction(newAllowed: Boolean) {
+        featureFactory.metricsFeatureProvider.action(
+                context,
+                SettingsEnums.MEDIA_ROUTING_CONTROL,
+                if (newAllowed)
+                    VALUE_LOGGING_ALLOWED
+                else
+                    VALUE_LOGGING_DENIED
+        )
+    }
+
+    companion object {
+        const val VALUE_LOGGING_ALLOWED = 1
+        const val VALUE_LOGGING_DENIED = 0
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
index cd615919..fe8f103 100644
--- a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
+++ b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
@@ -16,6 +16,7 @@
 
 package com.android.settings.spa.app.specialaccess
 
+import android.app.AppOpsManager
 import android.app.AppOpsManager.OP_PICTURE_IN_PICTURE
 import android.content.Context
 import android.content.pm.ActivityInfo
@@ -53,6 +54,7 @@
     override val pageTitleResId = R.string.picture_in_picture_title
     override val switchTitleResId = R.string.picture_in_picture_app_detail_switch
     override val footerResId = R.string.picture_in_picture_app_detail_summary
+    override val enhancedConfirmationKey: String = AppOpsManager.OPSTR_PICTURE_IN_PICTURE
 
     private val packageManager = context.packageManager
 
diff --git a/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt b/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
index fb05a38..0285b74 100644
--- a/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
+++ b/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
@@ -61,6 +61,7 @@
                 AllFilesAccessAppListProvider,
                 DisplayOverOtherAppsAppListProvider,
                 MediaManagementAppsAppListProvider,
+                MediaRoutingControlAppListProvider,
                 ModifySystemSettingsAppListProvider,
                 UseFullScreenIntentAppListProvider,
                 PictureInPictureListProvider,
diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceControllerTest.java
new file mode 100644
index 0000000..20a1d04
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceControllerTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.applications.specialaccess;
+
+import static com.android.settingslib.spa.framework.util.SpaIntentKt.KEY_DESTINATION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.preference.Preference;
+
+import com.android.media.flags.Flags;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.spa.SpaActivity;
+import com.android.settings.spa.app.specialaccess.MediaRoutingControlAppListProvider;
+
+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.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class MediaRoutingControlPreferenceControllerTest {
+
+    private static final String PREFERENCE_KEY = "test_preference_key";
+    private static final String DIFFERENT_PREFERENCE_KEY = "other_preference_key";
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private PackageManager mPackageManager;
+
+    private MediaRoutingControlPreferenceController mTestController;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        mTestController = new MediaRoutingControlPreferenceController(
+                mContext, PREFERENCE_KEY);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL)
+    public void getAvailabilityStatus_withFlagEnabled_shouldReturnTrue() {
+        assertThat(mTestController.getAvailabilityStatus())
+                .isEqualTo(BasePreferenceController.AVAILABLE);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL)
+    public void getAvailabilityStatus_withFlagDisabled_shouldReturnFalse() {
+        assertThat(mTestController.getAvailabilityStatus())
+                .isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE);
+    }
+
+    @Test
+    public void handlePreferenceTreeClick_withDifferentPreference_shouldReturnFalse() {
+        Preference preference = mock(Preference.class);
+        when(preference.getKey()).thenReturn(DIFFERENT_PREFERENCE_KEY);
+
+        assertThat(mTestController.handlePreferenceTreeClick(preference)).isFalse();
+    }
+
+    @Test
+    public void handlePreferenceTreeClick_withMediaRoutingPreference_shouldReturnTrue() {
+        Preference preference = mock(Preference.class);
+        when(preference.getKey()).thenReturn(PREFERENCE_KEY);
+
+        assertThat(mTestController.handlePreferenceTreeClick(preference)).isTrue();
+    }
+
+    @Test
+    public void handlePreferenceTreeClick_withDifferentPreference_shouldNotStartSpaActivity() {
+        Preference preference = mock(Preference.class);
+        when(preference.getKey()).thenReturn(DIFFERENT_PREFERENCE_KEY);
+
+        mTestController.handlePreferenceTreeClick(preference);
+
+        verify(mContext, never()).startActivity(any(Intent.class));
+    }
+
+    @Test
+    public void handlePreferenceTreeClick_withMediaRoutingPreference_shouldStartSpaActivity() {
+        Preference preference = mock(Preference.class);
+        when(preference.getKey()).thenReturn(PREFERENCE_KEY);
+
+        mTestController.handlePreferenceTreeClick(preference);
+
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext).startActivity(intentCaptor.capture());
+        final Intent intent = intentCaptor.getValue();
+        assertThat(intent.getComponent().getClassName()).isEqualTo(SpaActivity.class.getName());
+        assertThat(intent.getStringExtra(KEY_DESTINATION)).isEqualTo(
+                MediaRoutingControlAppListProvider.INSTANCE.getAppListRoute());
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
index 5e90e55..ff851eb 100644
--- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
@@ -262,6 +262,13 @@
         mFragment.onResume();
     }
 
+    @Test
+    public void testFragmentVisibleWhenNoHardwareDetected() {
+        doReturn(false).when(mFingerprintManager).isHardwareDetected();
+        setUpFragment(false);
+        assertThat(mFragment.isVisible()).isTrue();
+    }
+
     private void setSensor(@FingerprintSensorProperties.SensorType int sensorType) {
         final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
         props.add(new FingerprintSensorPropertiesInternal(
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..86c724c
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+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 com.android.settings.R;
+import com.android.settings.flags.Flags;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+
+import org.junit.After;
+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 CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    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() {
+        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);
+    }
+
+    @After
+    public void tearDown() {
+        ShadowAlertDialogCompat.reset();
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_flagOff_dialogNotExist() {
+        mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNull();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_flagOn_noConnectedDevice() {
+        mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+        ImageView guidance = rootView.findViewById(R.id.share_audio_guidance);
+        Button shareBtn = rootView.findViewById(R.id.share_btn);
+        assertThat(dialog.isShowing()).isTrue();
+        assertThat(subtitle1.getVisibility()).isEqualTo(View.GONE);
+        assertThat(guidance.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(shareBtn.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_noConnectedDevice_dialogDismiss() {
+        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
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_flagOn_singleConnectedDevice() {
+        ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+        list.add(TEST_DEVICE_ITEM1);
+        mFragment.show(mParent, list, (item) -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+        ImageView guidance = rootView.findViewById(R.id.share_audio_guidance);
+        Button shareBtn = rootView.findViewById(R.id.share_btn);
+        assertThat(dialog.isShowing()).isTrue();
+        assertThat(subtitle1.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(TEST_DEVICE_NAME1).isEqualTo(subtitle1.getText());
+        assertThat(guidance.getVisibility()).isEqualTo(View.GONE);
+        assertThat(shareBtn.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_singleConnectedDevice_dialogDismiss() {
+        mFragment.show(mParent, new ArrayList<>(), (item) -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        rootView.findViewById(R.id.cancel_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_singleConnectedDevice_shareClicked() {
+        AtomicBoolean isShareBtnClicked = new AtomicBoolean(false);
+        mFragment.show(
+                mParent,
+                new ArrayList<>(),
+                (item) -> {
+                    isShareBtnClicked.set(true);
+                });
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        rootView.findViewById(R.id.share_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+        assertThat(isShareBtnClicked.get()).isTrue();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_flagOn_multipleConnectedDevice() {
+        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();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+        ImageView guidance = rootView.findViewById(R.id.share_audio_guidance);
+        Button shareBtn = rootView.findViewById(R.id.share_btn);
+        assertThat(dialog.isShowing()).isTrue();
+        assertThat(subtitle1.getVisibility()).isEqualTo(View.GONE);
+        assertThat(guidance.getVisibility()).isEqualTo(View.GONE);
+        assertThat(shareBtn.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_multipleConnectedDevice_dialogDismiss() {
+        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();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        rootView.findViewById(R.id.cancel_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+    }
+}
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..976e164
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDisconnectDialogFragmentTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.View;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.internal.widget.RecyclerView;
+import com.android.settings.R;
+import com.android.settings.flags.Flags;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+
+import org.junit.After;
+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 AudioSharingDisconnectDialogFragmentTest {
+
+    @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    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= */ true);
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM2 =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME2, /* groupId= */ 2, /* isActive= */ false);
+
+    private Fragment mParent;
+    private AudioSharingDisconnectDialogFragment mFragment;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+    @Before
+    public void setUp() {
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mFragment = new AudioSharingDisconnectDialogFragment();
+        mParent = new Fragment();
+        FragmentController.setupFragment(
+                mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+        ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+        list.add(TEST_DEVICE_ITEM1);
+        list.add(TEST_DEVICE_ITEM2);
+        mFragment.show(mParent, list, TEST_DEVICE_NAME3, (item) -> {});
+        shadowMainLooper().idle();
+    }
+
+    @After
+    public void tearDown() {
+        ShadowAlertDialogCompat.reset();
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_flagOff_dialogNotExist() {
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_flagOn_dialogShowBtnForTwoDevices() {
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        RecyclerView view = rootView.findViewById(R.id.device_btn_list);
+        assertThat(view.getAdapter().getItemCount()).isEqualTo(2);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_clickCancel_dialogDismiss() {
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        rootView.findViewById(R.id.cancel_btn).performClick();
+        assertThat(dialog.isShowing()).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..5eb0e8a
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingJoinDialogFragmentTest.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 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.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.View;
+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.flags.Flags;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+
+import org.junit.After;
+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 AudioSharingJoinDialogFragmentTest {
+
+    @Rule public final MockitoRule mocks = MockitoJUnit.rule();
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    private static final String TEST_DEVICE_NAME1 = "test1";
+    private static final String TEST_DEVICE_NAME2 = "test2";
+    private static final AudioSharingDeviceItem TEST_DEVICE_ITEM =
+            new AudioSharingDeviceItem(TEST_DEVICE_NAME1, /* groupId= */ 1, /* isActive= */ true);
+
+    private Fragment mParent;
+    private AudioSharingJoinDialogFragment mFragment;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+    @Before
+    public void setUp() {
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mFragment = new AudioSharingJoinDialogFragment();
+        mParent = new Fragment();
+        FragmentController.setupFragment(
+                mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+    }
+
+    @After
+    public void tearDown() {
+        ShadowAlertDialogCompat.reset();
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_flagOff_dialogNotExist() {
+        mFragment.show(mParent, new ArrayList<>(), TEST_DEVICE_NAME2, () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNull();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_flagOn_dialogShowTextForSingleDevice() {
+        mFragment.show(mParent, new ArrayList<>(), TEST_DEVICE_NAME2, () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+        assertThat(subtitle1.getText()).isEqualTo(TEST_DEVICE_NAME2);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_flagOn_dialogShowTextForTwoDevice() {
+        ArrayList<AudioSharingDeviceItem> list = new ArrayList<>();
+        list.add(TEST_DEVICE_ITEM);
+        mFragment.show(mParent, list, TEST_DEVICE_NAME2, () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        TextView subtitle1 = rootView.findViewById(R.id.share_audio_subtitle1);
+        assertThat(subtitle1.getText()).isEqualTo(TEST_DEVICE_NAME1 + " and " + TEST_DEVICE_NAME2);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_clickCancel_dialogDismiss() {
+        mFragment.show(mParent, new ArrayList<>(), TEST_DEVICE_NAME2, () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        rootView.findViewById(R.id.cancel_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_clickShare_callbackTriggered() {
+        AtomicBoolean isShareBtnClicked = new AtomicBoolean(false);
+        mFragment.show(
+                mParent, new ArrayList<>(), TEST_DEVICE_NAME2, () -> isShareBtnClicked.set(true));
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog);
+        View rootView = shadowDialog.getView();
+        rootView.findViewById(R.id.share_btn).performClick();
+        assertThat(dialog.isShowing()).isFalse();
+        assertThat(isShareBtnClicked.get()).isTrue();
+    }
+}
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..1de7b2d
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingStopDialogFragmentTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.settings.flags.Flags;
+import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+
+import org.junit.After;
+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.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 CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    private static final String TEST_DEVICE_NAME = "test";
+
+    private Fragment mParent;
+    private AudioSharingStopDialogFragment mFragment;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+
+    @Before
+    public void setUp() {
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mFragment = new AudioSharingStopDialogFragment();
+        mParent = new Fragment();
+        FragmentController.setupFragment(
+                mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
+    }
+
+    @After
+    public void tearDown() {
+        ShadowAlertDialogCompat.reset();
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_flagOff_dialogNotExist() {
+        mFragment.show(mParent, TEST_DEVICE_NAME, () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        assertThat(dialog).isNull();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_clickCancel_dialogDismiss() {
+        mFragment.show(mParent, TEST_DEVICE_NAME, () -> {});
+        shadowMainLooper().idle();
+        AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
+        dialog.findViewById(android.R.id.button2).performClick();
+        shadowMainLooper().idle();
+        assertThat(dialog.isShowing()).isFalse();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING)
+    public void onCreateDialog_clickShare_callbackTriggered() {
+        AtomicBoolean isStopBtnClicked = new AtomicBoolean(false);
+        mFragment.show(mParent, TEST_DEVICE_NAME, () -> 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/dashboard/profileselector/ProfileSelectFragmentTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
index 302c8f3..3df6449 100644
--- a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
@@ -17,6 +17,9 @@
 package com.android.settings.dashboard.profileselector;
 
 import static android.content.Intent.EXTRA_USER_ID;
+import static android.os.UserManager.USER_TYPE_FULL_SYSTEM;
+import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
+import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
 
 import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.EXTRA_PROFILE;
 import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.PERSONAL_TAB;
@@ -30,9 +33,11 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.UserInfo;
 import android.os.Bundle;
 import android.os.Flags;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.ArraySet;
 
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
@@ -51,7 +56,9 @@
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
+import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 @RunWith(RobolectricTestRunner.class)
@@ -60,6 +67,9 @@
         com.android.settings.testutils.shadow.ShadowFragment.class,
 })
 public class ProfileSelectFragmentTest {
+    private static final String PRIMARY_USER_NAME = "primary";
+    private static final String MANAGED_USER_NAME = "managed";
+    private static final String PRIVATE_USER_NAME = "private";
 
     private Context mContext;
     private TestProfileSelectFragment mFragment;
@@ -151,6 +161,8 @@
     @Test
     public void testGetFragments_whenOnlyPersonal_returnsOneFragment() {
         mSetFlagsRule.disableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
                 null /* bundle */,
@@ -162,6 +174,10 @@
 
     @Test
     public void testGetFragments_whenPrivateDisabled_returnsOneFragment() {
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
+        mUserManager.addProfile(
+                new UserInfo(11, PRIVATE_USER_NAME, null, 0, USER_TYPE_PROFILE_PRIVATE));
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
                 null /* bundle */,
@@ -173,12 +189,6 @@
                     public boolean isPrivateSpaceLocked(Context context) {
                         return true;
                     }
-                },
-                new ProfileSelectFragment.ManagedProfileInfoProvider() {
-                    @Override
-                    public boolean isManagedProfilePresent(Context context) {
-                        return false;
-                    }
                 });
         assertThat(fragments).hasLength(1);
     }
@@ -186,6 +196,10 @@
     @Test
     public void testGetFragments_whenPrivateEnabled_returnsTwoFragments() {
         mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
+        mUserManager.addProfile(
+                new UserInfo(11, PRIVATE_USER_NAME, null, 0, USER_TYPE_PROFILE_PRIVATE));
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
                 null /* bundle */,
@@ -197,12 +211,6 @@
                     public boolean isPrivateSpaceLocked(Context context) {
                         return false;
                     }
-                },
-                new ProfileSelectFragment.ManagedProfileInfoProvider() {
-                    @Override
-                    public boolean isManagedProfilePresent(Context context) {
-                        return false;
-                    }
                 });
         assertThat(fragments).hasLength(2);
     }
@@ -210,6 +218,10 @@
     @Test
     public void testGetFragments_whenManagedProfile_returnsTwoFragments() {
         mSetFlagsRule.disableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
+        mUserManager.addProfile(
+                new UserInfo(10, MANAGED_USER_NAME, null, 0, USER_TYPE_PROFILE_MANAGED));
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
                 null /* bundle */,
@@ -221,12 +233,6 @@
                     public boolean isPrivateSpaceLocked(Context context) {
                         return false;
                     }
-                },
-                new ProfileSelectFragment.ManagedProfileInfoProvider() {
-                    @Override
-                    public boolean isManagedProfilePresent(Context context) {
-                        return true;
-                    }
                 });
         assertThat(fragments).hasLength(2);
     }
@@ -234,6 +240,12 @@
     @Test
     public void testGetFragments_whenAllProfiles_returnsThreeFragments() {
         mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
+        mUserManager.addProfile(
+                new UserInfo(10, MANAGED_USER_NAME, null, 0, USER_TYPE_PROFILE_MANAGED));
+        mUserManager.addProfile(
+                new UserInfo(11, PRIVATE_USER_NAME, null, 0, USER_TYPE_PROFILE_PRIVATE));
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
                 null /* bundle */,
@@ -245,12 +257,6 @@
                     public boolean isPrivateSpaceLocked(Context context) {
                         return false;
                     }
-                },
-                new ProfileSelectFragment.ManagedProfileInfoProvider() {
-                    @Override
-                    public boolean isManagedProfilePresent(Context context) {
-                        return true;
-                    }
                 });
         assertThat(fragments).hasLength(3);
     }
@@ -258,6 +264,12 @@
     @Test
     public void testGetFragments_whenAvailableBundle_returnsFragmentsWithCorrectBundles() {
         mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
+        mUserManager.addProfile(
+                new UserInfo(10, MANAGED_USER_NAME, null, 0, USER_TYPE_PROFILE_MANAGED));
+        mUserManager.addProfile(
+                new UserInfo(11, PRIVATE_USER_NAME, null, 0, USER_TYPE_PROFILE_PRIVATE));
         Bundle bundle = new Bundle();
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
@@ -270,20 +282,21 @@
                     public boolean isPrivateSpaceLocked(Context context) {
                         return false;
                     }
-                },
-                new ProfileSelectFragment.ManagedProfileInfoProvider() {
-                    @Override
-                    public boolean isManagedProfilePresent(Context context) {
-                        return true;
-                    }
                 });
         assertThat(fragments).hasLength(3);
-        assertThat(fragments[0].getArguments().getInt(EXTRA_PROFILE))
-                .isEqualTo(ProfileSelectFragment.ProfileType.PERSONAL);
-        assertThat(fragments[1].getArguments().getInt(EXTRA_PROFILE))
-                .isEqualTo(ProfileSelectFragment.ProfileType.WORK);
-        assertThat(fragments[2].getArguments().getInt(EXTRA_PROFILE))
-                .isEqualTo(ProfileSelectFragment.ProfileType.PRIVATE);
+
+        List<Integer> foundProfileTypesList = new ArrayList<>();
+        for (Fragment fragment : fragments) {
+            foundProfileTypesList.add(fragment.getArguments().getInt(EXTRA_PROFILE));
+        }
+
+        assertThat(foundProfileTypesList).hasSize(3);
+
+        Set<Integer> foundProfileTypes = new ArraySet<>(foundProfileTypesList);
+        assertThat(foundProfileTypes).containsExactly(
+                ProfileSelectFragment.ProfileType.PERSONAL,
+                ProfileSelectFragment.ProfileType.WORK,
+                ProfileSelectFragment.ProfileType.PRIVATE);
     }
 
     public static class TestProfileSelectFragment extends ProfileSelectFragment {
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceControllerTest.java
index ff8ea62..7c1650d2 100644
--- a/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceControllerTest.java
@@ -34,6 +34,7 @@
 import com.android.settings.testutils.FakeFeatureFactory;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
@@ -75,6 +76,7 @@
         assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
     }
 
+    @Ignore("b/315267179")
     @Test
     public void getSummary_available_returnExpectedDate() {
         when(mFactory.batterySettingsFeatureProvider.isFirstUseDateAvailable(eq(mContext),
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceControllerTest.java
index 608ce00..e50aa1c 100644
--- a/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceControllerTest.java
@@ -34,6 +34,7 @@
 import com.android.settings.testutils.FakeFeatureFactory;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
@@ -76,6 +77,7 @@
         assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
     }
 
+    @Ignore("b/315267179")
     @Test
     public void getSummary_available_returnExpectedDate() {
         when(mFactory.batterySettingsFeatureProvider.isManufactureDateAvailable(eq(mContext),
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java
index 91d8c7d..bdf81e4 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java
@@ -40,6 +40,7 @@
 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -155,6 +156,7 @@
                         "ScreenTimeoutAnomaly");
     }
 
+    @Ignore("b/313582999")
     @Test
     public void onClick_mainBtnOfAppsAnomaly_selectHighlightSlot() {
         final PowerAnomalyEvent appsAnomaly = BatteryTestUtils.createAppAnomalyEvent();
@@ -176,6 +178,7 @@
                 .action(mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_ACCEPT, "AppAnomaly");
     }
 
+    @Ignore("b/313582999")
     @Test
     public void onClick_dismissBtnOfAppsAnomaly_keepHighlightSlotIndex() {
         final PowerAnomalyEvent appsAnomaly = BatteryTestUtils.createAppAnomalyEvent();
diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/BluetoothUpdateWorkerTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/BluetoothUpdateWorkerTest.java
index e34737b..f306693 100644
--- a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/BluetoothUpdateWorkerTest.java
+++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/BluetoothUpdateWorkerTest.java
@@ -29,6 +29,7 @@
 import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
@@ -60,6 +61,7 @@
         verify(mResolver).notifyChange(URI, null);
     }
 
+    @Ignore("b/315399487")
     @Test
     public void onActiveDeviceChanged_shouldNotifyChange() {
         mBluetoothUpdateWorker.onActiveDeviceChanged(null, 0);
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt
new file mode 100644
index 0000000..cc5e365
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppArchiveButtonTest.kt
@@ -0,0 +1,135 @@
+/*
+ * 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.spa.app.appinfo
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.CloudUpload
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settingslib.spa.widget.button.ActionButton
+import 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.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class AppArchiveButtonTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {}
+
+    private val packageInfoPresenter = mock<PackageInfoPresenter>()
+
+    private val userPackageManager = mock<PackageManager>()
+
+    private val packageInstaller = mock<PackageInstaller>()
+
+    private lateinit var appArchiveButton: AppArchiveButton
+
+    @Before
+    fun setUp() {
+        whenever(packageInfoPresenter.context).thenReturn(context)
+        whenever(packageInfoPresenter.userPackageManager).thenReturn(userPackageManager)
+        whenever(userPackageManager.packageInstaller).thenReturn(packageInstaller)
+        whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
+        appArchiveButton = AppArchiveButton(packageInfoPresenter)
+    }
+
+    @Test
+    fun appArchiveButton_whenIsArchived_isDisabled() {
+        val app = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            isArchived = true
+        }
+        whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
+
+        val actionButton = setContent(app)
+
+        assertThat(actionButton.enabled).isFalse()
+    }
+
+    @Test
+    fun appArchiveButton_whenIsNotAppArchivable_isDisabled() {
+        val app = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            isArchived = false
+        }
+        whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(false)
+
+        val actionButton = setContent(app)
+
+        assertThat(actionButton.enabled).isFalse()
+    }
+
+    @Test
+    fun appArchiveButton_displaysRightTextAndIcon() {
+        val app = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            isArchived = false
+        }
+        whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
+
+        val actionButton = setContent(app)
+
+        assertThat(actionButton.text).isEqualTo(context.getString(R.string.archive))
+        assertThat(actionButton.imageVector).isEqualTo(Icons.Outlined.CloudUpload)
+    }
+
+    @Test
+    fun appArchiveButton_clicked() {
+        val app = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            isArchived = false
+        }
+        whenever(userPackageManager.isAppArchivable(app.packageName)).thenReturn(true)
+
+        val actionButton = setContent(app)
+        actionButton.onClick()
+
+        verify(packageInstaller).requestArchive(
+            eq(PACKAGE_NAME),
+            any(),
+            eq(0)
+        )
+    }
+
+    private fun setContent(app: ApplicationInfo): ActionButton {
+        lateinit var actionButton: ActionButton
+        composeTestRule.setContent {
+            actionButton = appArchiveButton.getActionButton(app)
+        }
+        return actionButton
+    }
+
+    private companion object {
+        const val PACKAGE_NAME = "package.name"
+    }
+}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
index e2f55ef..50094f2 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
@@ -22,8 +22,10 @@
 import android.content.pm.FakeFeatureFlagsImpl
 import android.content.pm.Flags
 import android.content.pm.PackageInfo
+import android.content.pm.PackageInstaller
 import android.content.pm.PackageManager
 import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled
 import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
@@ -62,6 +64,9 @@
     @Mock
     private lateinit var packageManager: PackageManager
 
+    @Mock
+    private lateinit var packageInstaller: PackageInstaller
+
     private val featureFlags = FakeFeatureFlagsImpl()
 
     @Before
@@ -74,6 +79,7 @@
         whenever(packageInfoPresenter.context).thenReturn(context)
         whenever(packageInfoPresenter.packageName).thenReturn(PACKAGE_NAME)
         whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager)
+        whenever(packageManager.packageInstaller).thenReturn(packageInstaller)
         whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO)
         whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false)
         featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
@@ -118,8 +124,24 @@
         composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsNotDisplayed()
     }
 
-    private fun setContent() {
-        whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(PACKAGE_INFO))
+    @Test
+    fun uninstallButton_enabled_whenAppIsArchived() {
+        whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent())
+        featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
+        val packageInfo = PackageInfo().apply {
+            applicationInfo = ApplicationInfo().apply {
+                packageName = PACKAGE_NAME
+                isArchived = true
+            }
+            packageName = PACKAGE_NAME
+        }
+        setContent(packageInfo)
+
+        composeTestRule.onNodeWithText(context.getString(R.string.uninstall_text)).assertIsEnabled()
+    }
+
+    private fun setContent(packageInfo: PackageInfo = PACKAGE_INFO) {
+        whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(packageInfo))
         composeTestRule.setContent {
             AppButtons(packageInfoPresenter, featureFlags)
         }
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt
index d1c318a..31722c9 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInstallerInfoPreferenceTest.kt
@@ -21,14 +21,11 @@
 import android.content.pm.ApplicationInfo
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsEnabled
 import androidx.compose.ui.test.assertIsNotDisplayed
-import androidx.compose.ui.test.assertIsNotEnabled
 import androidx.compose.ui.test.hasText
 import androidx.compose.ui.test.isEnabled
+import androidx.compose.ui.test.isNotEnabled
 import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.test.performClick
 import androidx.test.core.app.ApplicationProvider
@@ -122,9 +119,8 @@
             .thenReturn(null)
 
         setContent()
-        waitUntilDisplayed()
 
-        composeTestRule.onNode(preferenceNode).assertIsNotEnabled()
+        composeTestRule.waitUntilExists(preferenceNode.and(isNotEnabled()))
     }
 
     @Test
@@ -143,17 +139,16 @@
     @Test
     fun whenNotInstantApp() {
         setContent()
-        waitUntilDisplayed()
 
-        composeTestRule.onNodeWithText("App installed from installer label")
-            .assertIsDisplayed()
-            .assertIsEnabled()
+        composeTestRule.waitUntilExists(hasText("App installed from installer label"))
+        composeTestRule.waitUntilExists(preferenceNode.and(isEnabled()))
     }
 
     @Test
     fun whenClick_startActivity() {
         setContent()
-        waitUntilDisplayed()
+        composeTestRule.delay()
+
         composeTestRule.onRoot().performClick()
         composeTestRule.delay()
 
@@ -168,14 +163,10 @@
         }
     }
 
-    private fun waitUntilDisplayed() {
-        composeTestRule.waitUntilExists(preferenceNode)
-    }
-
     private val preferenceNode = hasText(context.getString(R.string.app_install_details_title))
 
     private companion object {
-        const val PACKAGE_NAME = "packageName"
+        const val PACKAGE_NAME = "package.name"
         const val INSTALLER_PACKAGE_NAME = "installer"
         const val INSTALLER_PACKAGE_LABEL = "installer label"
         val STORE_LINK = Intent("store/link")
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt
index d81bb1a..5dd66e8 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt
@@ -105,6 +105,7 @@
     fun isInterestedAppChange_archived_interested() {
         val intent = Intent(Intent.ACTION_PACKAGE_REMOVED).apply {
             data = Uri.parse("package:$PACKAGE_NAME")
+            putExtra(Intent.EXTRA_ARCHIVAL, true)
         }
 
         val isInterestedAppChange = packageInfoPresenter.isInterestedAppChange(intent)
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt
new file mode 100644
index 0000000..5f0f2c6
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt
@@ -0,0 +1,217 @@
+/*
+ * 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.spa.app.specialaccess
+
+import android.Manifest
+import android.app.AppOpsManager
+import android.app.role.RoleManager
+import android.app.settings.SettingsEnums
+import android.companion.AssociationRequest
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settings.testutils.FakeFeatureFactory
+import com.android.settingslib.spaprivileged.model.app.IAppOpsController
+import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
+import 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.Mock
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class MediaRoutingControlTest {
+    @get:Rule
+    val mockito: MockitoRule = MockitoJUnit.rule()
+
+    @Spy
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    private lateinit var listModel: MediaRoutingControlAppsListModel
+
+    @Mock
+    private lateinit var mockRoleManager: RoleManager
+
+    private val fakeFeatureFactory = FakeFeatureFactory()
+    private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider
+
+    @Before
+    fun setUp() {
+        whenever(context.getSystemService(RoleManager::class.java))
+                .thenReturn(mockRoleManager)
+        listModel = MediaRoutingControlAppsListModel(context)
+    }
+
+    @Test
+    fun modelResourceIdAndProperties() {
+        assertThat(listModel.pageTitleResId).isEqualTo(R.string.media_routing_control_title)
+        assertThat(listModel.switchTitleResId).isEqualTo(R.string.allow_media_routing_control)
+        assertThat(listModel.footerResId).isEqualTo(R.string.allow_media_routing_description)
+        assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_MEDIA_ROUTING_CONTROL)
+        assertThat(listModel.permission).isEqualTo(Manifest.permission.MEDIA_ROUTING_CONTROL)
+        assertThat(listModel.setModeByUid).isTrue()
+    }
+
+    @Test
+    fun setAllowed_callWithNewStatusAsTrue_shouldChangeAppControllerModeToAllowed() {
+        val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController = fakeAppOpController,
+                )
+
+        listModel.setAllowed(permissionRequestedRecord, true)
+
+        assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ALLOWED)
+    }
+
+    @Test
+    fun setAllowed_callWithNewStatusAsTrue_shouldLogPermissionToggleActionAsAllowed() {
+        val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController = fakeAppOpController,
+                )
+
+        listModel.setAllowed(permissionRequestedRecord, true)
+
+        verify(metricsFeatureProvider)
+                .action(context, SettingsEnums.MEDIA_ROUTING_CONTROL, VALUE_LOGGING_ALLOWED)
+    }
+
+    @Test
+    fun setAllowed_callWithNewStatusAsFalse_shouldChangeAppControllerModeToErrored() {
+        val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController = fakeAppOpController,
+                )
+
+        listModel.setAllowed(permissionRequestedRecord, false)
+
+        assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ERRORED)
+    }
+
+    @Test
+    fun setAllowed_callWithNewStatusAsFalse_shouldLogPermissionToggleActionAsDenied() {
+        val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController = fakeAppOpController,
+                )
+
+        listModel.setAllowed(permissionRequestedRecord, false)
+
+        verify(metricsFeatureProvider)
+                .action(context, SettingsEnums.MEDIA_ROUTING_CONTROL, VALUE_LOGGING_DENIED)
+    }
+
+    @Test
+    fun isChangeable_permissionRequestedByAppAndWatchCompanionRoleAssigned_shouldReturnTrue() {
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController =
+                            FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+                )
+        whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH))
+                .thenReturn(listOf(PACKAGE_NAME))
+
+        val isSpecialAccessChangeable = listModel.isChangeable(permissionRequestedRecord)
+
+        assertThat(isSpecialAccessChangeable).isTrue()
+    }
+
+    @Test
+    fun isChangeable_permissionNotRequestedByAppButWatchCompanionRoleAssigned_shouldReturnFalse() {
+        val permissionNotRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = false,
+                        hasRequestBroaderPermission = false,
+                        appOpsController =
+                            FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+                )
+        whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH))
+                .thenReturn(listOf(PACKAGE_NAME))
+
+        val isSpecialAccessChangeable = listModel.isChangeable(permissionNotRequestedRecord)
+
+        assertThat(isSpecialAccessChangeable).isFalse()
+    }
+
+    @Test
+    fun isChangeable_permissionRequestedByAppButWatchCompanionRoleNotAssigned_shouldReturnFalse() {
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController =
+                            FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+                )
+        whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH))
+                .thenReturn(listOf("other.package.name"))
+
+        val isSpecialAccessChangeable = listModel.isChangeable(permissionRequestedRecord)
+
+        assertThat(isSpecialAccessChangeable).isFalse()
+    }
+
+    private class FakeAppOpsController(fakeMode: Int) : IAppOpsController {
+
+        override val mode = MutableLiveData(fakeMode)
+
+        override fun setAllowed(allowed: Boolean) {
+            if (allowed)
+                mode.postValue(AppOpsManager.MODE_ALLOWED)
+            else
+                mode.postValue(AppOpsManager.MODE_ERRORED)
+        }
+
+        override fun getMode(): Int = mode.value!!
+    }
+
+    companion object {
+        const val PACKAGE_NAME = "test.package.name"
+        const val VALUE_LOGGING_ALLOWED = 1
+        const val VALUE_LOGGING_DENIED = 0
+    }
+}
\ No newline at end of file