Merge "Add device to to cached device manager if it's not present" into main
diff --git a/Android.bp b/Android.bp
index cb898be..0c6d8d1 100644
--- a/Android.bp
+++ b/Android.bp
@@ -125,6 +125,9 @@
         "telephony-common",
         "ims-common",
     ],
+    flags_packages: [
+        "android.app.flags-aconfig",
+    ],
 }
 
 platform_compat_config {
@@ -155,6 +158,9 @@
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
+    flags_packages: [
+        "android.app.flags-aconfig",
+    ],
 }
 
 android_library_import {
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ca96328..8cfd9b5 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1287,20 +1287,63 @@
         </activity>
 
         <activity
-            android:name="Settings$ZenModeSettingsActivity"
+            android:name="Settings$ModesSettingsActivity"
             android:label="@string/zen_mode_settings_title"
             android:icon="@drawable/ic_homepage_notification"
             android:exported="true">
-            <intent-filter android:priority="1">
+            <intent-filter android:priority="1"
+                           android:featureFlag="android.app.modes_ui">
                 <action android:name="android.settings.ZEN_MODE_SETTINGS" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
-            <intent-filter android:priority="1">
+            <intent-filter android:priority="1"
+                           android:featureFlag="android.app.modes_ui">
                 <action android:name="android.settings.ZEN_MODE_PRIORITY_SETTINGS" />
                 <category android:name="android.intent.category.BROWSABLE" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
-            <intent-filter android:priority="41">
+            <intent-filter android:priority="41"
+                           android:featureFlag="android.app.modes_ui">
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.android.settings.SHORTCUT" />
+            </intent-filter>
+            <intent-filter android:priority="10"
+                           android:featureFlag="android.app.modes_ui">
+                <action android:name="android.settings.ZEN_MODE_AUTOMATION_SETTINGS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter android:priority="10"
+                           android:featureFlag="android.app.modes_ui">
+                <action android:name="android.settings.ACTION_CONDITION_PROVIDER_SETTINGS" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+                       android:value="com.android.settings.notification.modes.ZenModesListFragment"/>
+            <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
+                       android:value="@string/menu_key_notifications"/>
+            <meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
+                       android:value="true" />
+        </activity>
+
+        <activity
+            android:name="Settings$ZenModeSettingsActivity"
+            android:label="@string/zen_mode_settings_title"
+            android:icon="@drawable/ic_homepage_notification"
+            android:exported="true">
+            <intent-filter android:priority="1"
+                           android:featureFlag="!android.app.modes_ui">
+                <action android:name="android.settings.ZEN_MODE_SETTINGS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter android:priority="1"
+                           android:featureFlag="!android.app.modes_ui">
+                <action android:name="android.settings.ZEN_MODE_PRIORITY_SETTINGS" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter android:priority="41"
+                           android:featureFlag="!android.app.modes_ui">
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="com.android.settings.SHORTCUT" />
             </intent-filter>
@@ -1313,6 +1356,20 @@
         </activity>
 
         <activity
+            android:name="Settings$ModeSettingsActivity"
+            android:exported="true">
+            <intent-filter android:priority="1"
+                           android:featureFlag="android.app.modes_ui">
+                <action android:name="android.settings.AUTOMATIC_ZEN_RULE_SETTINGS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+                       android:value="com.android.settings.notification.modes.ZenModeFragment"/>
+            <meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
+                       android:value="true" />
+        </activity>
+
+        <activity
             android:name=".notification.zen.ZenSuggestionActivity"
             android:label="@string/zen_mode_settings_title"
             android:icon="@drawable/ic_suggestion_dnd"
@@ -1351,11 +1408,13 @@
             android:label="@string/zen_mode_automation_settings_title"
             android:icon="@drawable/ic_notifications"
             android:exported="true">
-            <intent-filter android:priority="1">
+            <intent-filter android:priority="10"
+                           android:featureFlag="!android.app.modes_ui">
                 <action android:name="android.settings.ZEN_MODE_AUTOMATION_SETTINGS" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
-            <intent-filter android:priority="1">
+            <intent-filter android:priority="10"
+                           android:featureFlag="!android.app.modes_ui">
                 <action android:name="android.settings.ACTION_CONDITION_PROVIDER_SETTINGS" />
                 <category android:name="android.intent.category.BROWSABLE" />
                 <category android:name="android.intent.category.DEFAULT" />
@@ -2832,17 +2891,20 @@
         <!-- Note this must not be exported since it returns the password in the intent -->
         <activity android:name=".password.ConfirmLockPattern$InternalActivity"
             android:exported="false"
+            android:enableOnBackInvokedCallback="false"
             android:theme="@style/GlifTheme.Light"/>
 
         <!-- Note this must not be exported since it returns the password in the intent -->
         <activity android:name=".password.ConfirmLockPassword$InternalActivity"
             android:exported="false"
             android:windowSoftInputMode="adjustResize"
+            android:enableOnBackInvokedCallback="false"
             android:theme="@style/GlifTheme.Light"/>
 
         <activity android:name=".password.SetupChooseLockGeneric"
             android:theme="@style/GlifTheme.Light"
             android:exported="true"
+            android:enableOnBackInvokedCallback="false"
             android:label="@string/lock_settings_picker_title">
             <intent-filter android:priority="1">
                 <action android:name="com.android.settings.SETUP_LOCK_SCREEN" />
@@ -2852,16 +2914,19 @@
 
         <activity android:name=".password.SetupChooseLockGeneric$InternalActivity"
             android:exported="false"
+            android:enableOnBackInvokedCallback="false"
             android:excludeFromRecents="true" />
 
         <activity android:name=".password.ChooseLockGeneric"
             android:label="@string/lockpassword_choose_lock_generic_header"
             android:excludeFromRecents="true"
+            android:enableOnBackInvokedCallback="false"
             android:exported="false" />
 
         <activity android:name=".password.SetNewPasswordActivity"
             android:theme="@android:style/Theme.NoDisplay"
             android:exported="true"
+            android:enableOnBackInvokedCallback="false"
             android:excludeFromRecents="true" >
             <intent-filter android:priority="1">
                 <action android:name="android.app.action.SET_NEW_PASSWORD" />
@@ -2907,24 +2972,29 @@
         <activity android:name=".password.ChooseLockGeneric$InternalActivity"
             android:exported="false"
             android:label="@string/lockpassword_choose_lock_generic_header"
+            android:enableOnBackInvokedCallback="false"
             android:excludeFromRecents="true" />
 
         <activity android:name=".password.SetupChooseLockPattern"
             android:exported="false"
+            android:enableOnBackInvokedCallback="false"
             android:theme="@style/GlifTheme.Light" />
 
         <activity android:name=".password.ChooseLockPattern"
             android:exported="false"
+            android:enableOnBackInvokedCallback="false"
             android:theme="@style/GlifTheme.Light" />
 
         <activity android:name=".password.SetupChooseLockPassword"
             android:exported="false"
             android:theme="@style/GlifTheme.Light"
+            android:enableOnBackInvokedCallback="false"
             android:windowSoftInputMode="stateVisible|adjustResize" />
 
         <activity android:name=".password.ChooseLockPassword"
             android:exported="false"
             android:theme="@style/GlifTheme.Light"
+            android:enableOnBackInvokedCallback="false"
             android:windowSoftInputMode="stateVisible|adjustResize"/>
 
         <activity
diff --git a/aconfig/settings_connecteddevice_flag_declarations.aconfig b/aconfig/settings_connecteddevice_flag_declarations.aconfig
index 7942ccd..693e398 100644
--- a/aconfig/settings_connecteddevice_flag_declarations.aconfig
+++ b/aconfig/settings_connecteddevice_flag_declarations.aconfig
@@ -9,13 +9,26 @@
 }
 
 flag {
+  name: "rotation_connected_display_setting"
+  namespace: "display_manager"
+  description: "Allow changing rotation of the connected display."
+  bug: "294015706"
+}
+
+flag {
+  name: "resolution_and_enable_connected_display_setting"
+  namespace: "display_manager"
+  description: "Allow enabling/disabling and changing resolution of the connected display."
+  bug: "253296253"
+}
+
+flag {
   name: "enable_auth_challenge_for_usb_preferences"
   namespace: "safety_center"
   description: "Gates whether to require an auth challenge for changing USB preferences"
   bug: "317367746"
 }
 
-
 flag {
     name: "enable_bonded_bluetooth_device_searchable"
     namespace: "pixel_cross_device_control"
@@ -24,4 +37,4 @@
     metadata {
         purpose: PURPOSE_BUGFIX
     }
-}
\ No newline at end of file
+}
diff --git a/res/drawable/external_display_mirror_landscape.xml b/res/drawable/external_display_mirror_landscape.xml
new file mode 100644
index 0000000..4272ddb
--- /dev/null
+++ b/res/drawable/external_display_mirror_landscape.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="232.02106dp"
+    android:viewportHeight="214"
+    android:viewportWidth="380"
+    android:width="412dp">
+    <path
+        android:pathData="M16,0L364,0A16,16 0,0 1,380 16L380,198A16,16 0,0 1,364 214L16,214A16,16 0,0 1,0 198L0,16A16,16 0,0 1,16 0z"
+        android:fillColor="#00000000"/>
+    <path
+        android:pathData="M150.5,38L327.5,38A5.5,5.5 0,0 1,333 43.5L333,138.5A5.5,5.5 0,0 1,327.5 144L150.5,144A5.5,5.5 0,0 1,145 138.5L145,43.5A5.5,5.5 0,0 1,150.5 38z"
+        android:fillColor="#80868B"/>
+    <path
+        android:pathData="M150.58,39L327.42,39A4.58,4.58 0,0 1,332 43.58L332,138.42A4.58,4.58 0,0 1,327.42 143L150.58,143A4.58,4.58 0,0 1,146 138.42L146,43.58A4.58,4.58 0,0 1,150.58 39z"
+        android:fillColor="#000000"/>
+    <path
+        android:pathData="M254.25,144H223.75L221.52,173.34C221.48,173.82 221.08,174.18 220.6,174.18H211.37C211.25,174.18 211.12,174.21 211.01,174.26C210.11,174.65 210.39,176 211.37,176H266.63C267.61,176 267.89,174.65 266.99,174.26C266.88,174.21 266.75,174.18 266.63,174.18H257.4C256.92,174.18 256.52,173.82 256.48,173.34L254.25,144Z"
+        android:fillColor="#5F6368"/>
+    <path
+        android:pathData="M330,53L330,129A3,3 0,0 1,327 132L151,132A3,3 0,0 1,148 129L148,53A3,3 0,0 1,151 50L327,50A3,3 0,0 1,330 53z"
+        android:strokeWidth="2"
+        android:fillColor="#00000000"
+        android:strokeColor="#E0E994"/>
+    <path
+        android:pathData="M113,91.08V86.55C113,86.25 112.88,85.96 112.67,85.75C112.45,85.54 112.17,85.42 111.86,85.41V61.64C111.84,60.15 111.24,58.72 110.17,57.66C109.1,56.61 107.66,56.01 106.16,56H53.71C52.2,56.01 50.75,56.61 49.68,57.67C48.62,58.73 48.01,60.17 48,61.66V170.34C48.01,171.83 48.62,173.27 49.68,174.33C50.75,175.39 52.2,175.99 53.71,176H106.16C107.67,175.99 109.11,175.39 110.18,174.33C111.25,173.27 111.85,171.83 111.86,170.34V114.86C112.16,114.86 112.45,114.74 112.67,114.52C112.88,114.31 113,114.03 113,113.73V102.4C113,102.1 112.88,101.82 112.67,101.6C112.45,101.39 112.17,101.27 111.86,101.27V92.21C112.16,92.21 112.45,92.09 112.67,91.88C112.88,91.67 113,91.38 113,91.08ZM110.72,170.34C110.72,171.54 110.24,172.69 109.38,173.54C108.53,174.39 107.37,174.87 106.16,174.87H53.71C52.5,174.87 51.34,174.39 50.48,173.54C49.62,172.69 49.14,171.54 49.14,170.34V61.64C49.14,60.44 49.62,59.29 50.48,58.44C51.34,57.59 52.5,57.11 53.71,57.11H106.16C107.37,57.11 108.53,57.59 109.38,58.44C110.24,59.29 110.72,60.44 110.72,61.64V170.34Z"
+        android:fillColor="#80868B"/>
+    <path
+        android:pathData="M54,59L106,59A3,3 0,0 1,109 62L109,170A3,3 0,0 1,106 173L54,173A3,3 0,0 1,51 170L51,62A3,3 0,0 1,54 59z"
+        android:strokeColor="#E0E994"
+        android:strokeWidth="2"
+        android:fillColor="#000000"/>
+    <path
+        android:pathData="M80,184.72V189.7C80,193.73 83.27,197 87.3,197H164.7C168.73,197 172,193.73 172,189.7V144"
+        android:strokeColor="#5F6368"
+        android:strokeWidth="0.684"
+        android:fillColor="#00000000"/>
+    <path
+        android:pathData="M77,176H83V184.09C83,184.59 82.59,185 82.09,185H77.91C77.41,185 77,184.59 77,184.09V176Z"
+        android:fillColor="#5F6368"/>
+</vector>
diff --git a/res/drawable/external_display_mirror_portrait.xml b/res/drawable/external_display_mirror_portrait.xml
new file mode 100644
index 0000000..0fe7f93
--- /dev/null
+++ b/res/drawable/external_display_mirror_portrait.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="232.02106dp"
+    android:viewportHeight="214"
+    android:viewportWidth="380"
+    android:width="412dp" >
+    <path
+        android:pathData="M16,0L364,0A16,16 0,0 1,380 16L380,198A16,16 0,0 1,364 214L16,214A16,16 0,0 1,0 198L0,16A16,16 0,0 1,16 0z"
+        android:fillColor="#00000000"/>
+    <path
+        android:pathData="M150.5,38L327.5,38A5.5,5.5 0,0 1,333 43.5L333,138.5A5.5,5.5 0,0 1,327.5 144L150.5,144A5.5,5.5 0,0 1,145 138.5L145,43.5A5.5,5.5 0,0 1,150.5 38z"
+        android:fillColor="#80868B"/>
+    <path
+        android:pathData="M150.58,39L327.42,39A4.58,4.58 0,0 1,332 43.58L332,138.42A4.58,4.58 0,0 1,327.42 143L150.58,143A4.58,4.58 0,0 1,146 138.42L146,43.58A4.58,4.58 0,0 1,150.58 39z"
+        android:fillColor="#000000"/>
+    <path
+        android:pathData="M254.25,144H223.75L221.52,173.34C221.48,173.82 221.08,174.18 220.6,174.18H211.37C211.25,174.18 211.12,174.21 211.01,174.26C210.11,174.65 210.39,176 211.37,176H266.63C267.61,176 267.89,174.65 266.99,174.26C266.88,174.21 266.75,174.18 266.63,174.18H257.4C256.92,174.18 256.52,173.82 256.48,173.34L254.25,144Z"
+        android:fillColor="#5F6368"/>
+    <path
+        android:pathData="M216,41L262,41A3,3 0,0 1,265 44L265,138A3,3 0,0 1,262 141L216,141A3,3 0,0 1,213 138L213,44A3,3 0,0 1,216 41z"
+        android:strokeWidth="2"
+        android:fillColor="#00000000"
+        android:strokeColor="#E0E994"/>
+    <path
+        android:pathData="M113,91.08V86.55C113,86.25 112.88,85.96 112.67,85.75C112.45,85.54 112.17,85.42 111.86,85.41V61.64C111.84,60.15 111.24,58.72 110.17,57.66C109.1,56.61 107.66,56.01 106.16,56H53.71C52.2,56.01 50.75,56.61 49.68,57.67C48.62,58.73 48.01,60.17 48,61.66V170.34C48.01,171.83 48.62,173.27 49.68,174.33C50.75,175.39 52.2,175.99 53.71,176H106.16C107.67,175.99 109.11,175.39 110.18,174.33C111.25,173.27 111.85,171.83 111.86,170.34V114.86C112.16,114.86 112.45,114.74 112.67,114.52C112.88,114.31 113,114.03 113,113.73V102.4C113,102.1 112.88,101.82 112.67,101.6C112.45,101.39 112.17,101.27 111.86,101.27V92.21C112.16,92.21 112.45,92.09 112.67,91.88C112.88,91.67 113,91.38 113,91.08ZM110.72,170.34C110.72,171.54 110.24,172.69 109.38,173.54C108.53,174.39 107.37,174.87 106.16,174.87H53.71C52.5,174.87 51.34,174.39 50.48,173.54C49.62,172.69 49.14,171.54 49.14,170.34V61.64C49.14,60.44 49.62,59.29 50.48,58.44C51.34,57.59 52.5,57.11 53.71,57.11H106.16C107.37,57.11 108.53,57.59 109.38,58.44C110.24,59.29 110.72,60.44 110.72,61.64V170.34Z"
+        android:fillColor="#80868B"/>
+    <path
+        android:pathData="M54,59L106,59A3,3 0,0 1,109 62L109,170A3,3 0,0 1,106 173L54,173A3,3 0,0 1,51 170L51,62A3,3 0,0 1,54 59z"
+        android:strokeColor="#E0E994"
+        android:strokeWidth="2"
+        android:fillColor="#000000"/>
+    <path
+        android:pathData="M80,184.72V189.7C80,193.73 83.27,197 87.3,197H164.7C168.73,197 172,193.73 172,189.7V144"
+        android:strokeColor="#5F6368"
+        android:strokeWidth="0.684"
+        android:fillColor="#00000000"/>
+    <path
+        android:pathData="M77,176H83V184.09C83,184.59 82.59,185 82.09,185H77.91C77.41,185 77,184.59 77,184.09V176Z"
+        android:fillColor="#5F6368"/>
+</vector>
diff --git a/res/drawable/ic_do_not_disturb_on_24dp.xml b/res/drawable/ic_do_not_disturb_on_24dp.xml
deleted file mode 100644
index cace8d4..0000000
--- a/res/drawable/ic_do_not_disturb_on_24dp.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-    Copyright (C) 2018 The Android Open Source Project
-
-    Licensed under the Apache License, Version 2.0 (the "License");
-    you may not use this file except in compliance with the License.
-    You may obtain a copy of the License at
-
-         http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-    See the License for the specific language governing permissions and
-    limitations under the License.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0"
-        android:tint="?android:attr/colorControlNormal">
-    <path
-        android:fillColor="#FFFFFFFF"
-        android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8c0,-4.41 3.59,-8 8,-8c4.41,0 8,3.59 8,8C20,16.41 16.41,20 12,20z"/>
-    <path
-        android:fillColor="#FFFFFFFF"
-        android:pathData="M7,11h10v2h-10z"/>
-</vector>
diff --git a/res/drawable/ic_external_display_32dp.xml b/res/drawable/ic_external_display_32dp.xml
new file mode 100644
index 0000000..3e18282
--- /dev/null
+++ b/res/drawable/ic_external_display_32dp.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="32dp"
+    android:height="32dp"
+    android:viewportWidth="32"
+    android:viewportHeight="32">
+  <path
+      android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
+      android:fillColor="#FAFBD8"/>
+  <group>
+    <clip-path
+        android:pathData="M5.333,5.332h21.333v21.333h-21.333z"/>
+    <path
+        android:pathData="M12.689,23.288V21.643H14.333V19.976H9C8.555,19.976 8.17,19.813 7.844,19.488C7.518,19.162 7.355,18.769 7.355,18.31V9.665C7.355,9.206 7.518,8.814 7.844,8.488C8.17,8.162 8.555,7.999 9,7.999H23C23.444,7.999 23.829,8.162 24.155,8.488C24.481,8.814 24.644,9.206 24.644,9.665V18.31C24.644,18.769 24.481,19.162 24.155,19.488C23.829,19.813 23.444,19.976 23,19.976H17.666V21.643H19.311V23.288H12.689ZM9,18.31H23V9.665H9V18.31ZM9,18.31V9.665V18.31Z"
+        android:fillColor="#8E964B"/>
+  </group>
+</vector>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index d972e13..d346474 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -179,6 +179,9 @@
     <dimen name="pointer_fill_style_circle_padding">8dp</dimen>
     <dimen name="pointer_fill_style_shape_default_stroke">1dp</dimen>
     <dimen name="pointer_fill_style_shape_hovered_stroke">3dp</dimen>
+    <dimen name="pointer_scale_padding">8dp</dimen>
+    <item name="pointer_scale_size_start" format="float" type="dimen">1.0</item>
+    <item name="pointer_scale_size_end" format="float" type="dimen">2.5</item>
 
     <!-- RemoteAuth-->
     <dimen name="remoteauth_fragment_padding_horizontal">40dp</dimen>
diff --git a/res/values/integers.xml b/res/values/integers.xml
index f62ccae..5427cdd 100644
--- a/res/values/integers.xml
+++ b/res/values/integers.xml
@@ -36,4 +36,8 @@
     <integer name="enrollment_progress_minimum_time_display">0</integer>
     <!-- The time (in millis) to wait to collect messages in fingerprint enrollment before displaying it. -->
     <integer name="enrollment_collect_time">0</integer>
+
+    <!--  PointerIcon Settings  -->
+    <integer name="pointer_scale_seek_bar_start">0</integer>
+    <integer name="pointer_scale_seek_bar_end">3</integer>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 075056d..4f295b2 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1893,6 +1893,37 @@
     <!-- Nfc developer settings: The confirm button of the popup dialog. [CHAR_LIMIT=60] -->
     <string name="nfc_reboot_dialog_confirm">Restart</string>
 
+    <!-- External Display settings. The keywords for searching. [CHAR LIMIT=40] -->
+    <string name="keywords_external_display">mirror, external display, connected display, usb display, resolution, rotation</string>
+    <!-- External Display settings. When external display is enabled. [CHAR LIMIT=40] -->
+    <string name="external_display_on">On</string>
+    <!-- External Display settings. When external display is disabled. [CHAR LIMIT=40] -->
+    <string name="external_display_off">Off</string>
+    <!-- External Display settings. The title of the screen. [CHAR LIMIT=40] -->
+    <string name="external_display_settings_title">External Display</string>
+    <!-- External Display use. The title of the use preference. [CHAR LIMIT=40] -->
+    <string name="external_display_use_title">Use external display</string>
+    <!-- External Display resolution settings. The title of the screen. [CHAR LIMIT=40] -->
+    <string name="external_display_resolution_settings_title">Display resolution</string>
+    <!-- External Display settings. Text that appears when scanning for devices is finished and no nearby device was found [CHAR LIMIT=40]-->
+    <string name="external_display_not_found">External display is disconnected</string>
+    <!-- External Display settings. Rotation of the external display -->
+    <string name="external_display_rotation">Rotation</string>
+    <!-- External Display settings. Standard rotation of the external display -->
+    <string name="external_display_standard_rotation">Standard</string>
+    <!-- External Display settings. 90 rotation of the external display -->
+    <string name="external_display_rotation_90">90°</string>
+    <!-- External Display settings. 180 rotation of the external display -->
+    <string name="external_display_rotation_180">180°</string>
+    <!-- External Display settings. 180 rotation of the external display -->
+    <string name="external_display_rotation_270">270°</string>
+    <!-- External Display settings. Footer title -->
+    <string name="external_display_change_resolution_footer_title">Changing rotation or resolution may stop any apps that are currently running</string>
+    <!-- External Display settings. No Displays footer title -->
+    <string name="external_display_not_found_footer_title">Your device must be connected to an external display to mirror your screen</string>
+    <!-- External Display settings. More resolution options -->
+    <string name="external_display_more_options_title">More options</string>
+
     <!-- Wifi Display settings. The title of the screen. [CHAR LIMIT=40] -->
     <string name="wifi_display_settings_title">Cast</string>
     <!-- Wifi Display settings. The keywords of the setting. [CHAR LIMIT=NONE] -->
@@ -4571,6 +4602,12 @@
 
     <!-- On Languages & input settings screen, setting summary.  Setting for mouse pointer speed. [CHAR LIMIT=35] -->
     <string name="pointer_speed">Pointer speed</string>
+    <!-- Setting for mouse pointer scale. [CHAR LIMIT=35] -->
+    <string name="pointer_scale">Pointer scale</string>
+    <!-- Content description for decreasing pointer scale. [CHAR LIMIT=35] -->
+    <string name="pointer_scale_decrease_content_description">Decrease pointer scale</string>
+    <!-- Setting for mouse pointer scale. [CHAR LIMIT=35] -->
+    <string name="pointer_scale_increase_content_description">Increase pointer scale</string>
 
     <!-- On Languages & input settings screen, heading. Inside the "Languages & input settings" screen, this is the header for settings that relate to game controller devices. [CHAR LIMIT=40] -->
     <string name="game_controller_settings_category">Game Controller</string>
@@ -7268,6 +7305,8 @@
     <string name="help_url_install_certificate" translatable="false"></string>
     <!-- Help URL, Tap & pay [DO NOT TRANSLATE] -->
     <string name="help_url_nfc_payment" translatable="false"></string>
+    <!-- Help URL, External display [DO NOT TRANSLATE] -->
+    <string name="help_url_external_display" translatable="false"></string>
     <!-- Help URL, Remote display [DO NOT TRANSLATE] -->
     <string name="help_url_remote_display" translatable="false"></string>
     <!-- Help URL, Face [DO NOT TRANSLATE] -->
@@ -7930,16 +7969,28 @@
     <!-- Zen Modes: Option to add an automatic schedule for a mode. [CHAR_LIMIT=40] -->
     <string name="zen_mode_select_schedule">Select activation type</string>
 
-    <!-- Zen Modes: Option to choose a time-based schedule for a mode. [CHAR_LIMIT=40] -->
+    <!-- Priority Modes: Option to choose a time-based schedule for a mode. [CHAR_LIMIT=40] -->
     <string name="zen_mode_select_schedule_time">Time</string>
-    <!-- Zen Modes: Example text for the option to choose a time-based schedule for a mode. [CHAR_LIMIT=60] -->
+    <!-- Priority Modes: Example text for the option to choose a time-based schedule for a mode. [CHAR_LIMIT=60] -->
     <string name="zen_mode_select_schedule_time_example">Ex. \"9:30 – 5:00 PM\"</string>
 
-    <!-- Zen Modes: Option to choose a calendar-events-based schedule for a mode. [CHAR_LIMIT=40] -->
+    <!-- Priority Modes: Option to choose a calendar-events-based schedule for a mode. [CHAR_LIMIT=40] -->
     <string name="zen_mode_select_schedule_calendar">Calendar</string>
-    <!-- Zen Modes: Example text for the option to choose a calendar-events-based schedule for a mode. [CHAR_LIMIT=60] -->
+    <!-- Priority Modes: Example text for the option to choose a calendar-events-based schedule for a mode. [CHAR_LIMIT=60] -->
     <string name="zen_mode_select_schedule_calendar_example">Ex. \"Personal calendar\"</string>
 
+    <!-- Priority Modes: Short text that indicates that a mode is currently on (active). [CHAR_LIMIT=10] -->
+    <string name="zen_mode_active_text">ON</string>
+
+    <!-- Priority Modes: Format string for the "current state + trigger description summary for rules in the list. [CHAR_LIMIT=10] -->
+    <string name="zen_mode_format_status_and_trigger" translatable="false"><xliff:g id="current_status" example="ON">%1$s</xliff:g> • <xliff:g id="trigger_description" example="Mon-Fri, 23:00-7:00">%2$s</xliff:g></string>
+
+    <!-- Priority Modes: Call to action for a mode that is disabled and needs to be configured. [CHAR_LIMIT=40] -->
+    <string name="zen_mode_disabled_tap_to_set_up">Tap to set up</string>
+
+    <!-- Priority Modes: Indicates that a mode is disabled by the user. [CHAR_LIMIT=40] -->
+    <string name="zen_mode_disabled_by_user">Paused</string>
+
     <!-- Subtitle for the Do not Disturb slice. [CHAR LIMIT=50]-->
     <string name="zen_mode_slice_subtitle">Limit interruptions</string>
 
@@ -7955,9 +8006,15 @@
     <!--  Do not disturb: Title for dialog that allows users to delete DND rules/schedules[CHAR LIMIT=40] -->
     <string name="zen_mode_delete_automatic_rules">Delete schedules</string>
 
-    <!--  Do not disturb: Delete text button presented in a dialog to confirm the user would like to delete the selected DND rules. [CHAR LIMIT=30] -->
+    <!-- Do not disturb: Delete text button presented in a dialog to confirm the user would like to delete the selected DND rules. [CHAR LIMIT=30] -->
     <string name="zen_mode_schedule_delete">Delete</string>
 
+    <!-- Do not disturb: Menu option for deleting a mode on its configuration page [CHAR LIMIT=40] -->
+    <string name="zen_mode_menu_delete_mode">Delete mode</string>
+
+    <!-- Do not disturb: Confirmation dialog asking the user whether they would like to delete the named mode [CHAR LIMIT: 40] -->
+    <string name="zen_mode_delete_mode_confirmation">Delete \"<xliff:g id="mode" example="My Schedule">%1$s</xliff:g>\" mode?</string>
+
     <!--  Do not disturb: Edit label for button that allows user to edit the dnd schedule name. [CHAR LIMIT=30] -->
     <string name="zen_mode_rule_name_edit">Edit</string>
 
@@ -12643,7 +12700,7 @@
     <string name="default_print_service_main_switch_title">Use print service</string>
 
     <!-- Title for multiple users main switch. [CHAR LIMIT=50] -->
-    <string name="multiple_users_main_switch_title">Allow multiple users</string>
+    <string name="multiple_users_main_switch_title">Allow user switch</string>
     <!-- Search keywords for the "Allow Multiple Users" section in Multiple Users Screen. [CHAR LIMIT=NONE] -->
     <string name="multiple_users_main_switch_keywords">allow, multiple, user, permit, many</string>
     <!-- Search keywords for the Users Screen. [CHAR LIMIT=NONE] -->
diff --git a/res/xml/external_display_resolution_settings.xml b/res/xml/external_display_resolution_settings.xml
new file mode 100644
index 0000000..6ac6b1a
--- /dev/null
+++ b/res/xml/external_display_resolution_settings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:title="@string/external_display_resolution_settings_title">
+</PreferenceScreen>
diff --git a/res/xml/external_display_settings.xml b/res/xml/external_display_settings.xml
new file mode 100644
index 0000000..0047211
--- /dev/null
+++ b/res/xml/external_display_settings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    settings:keywords="@string/keywords_external_display"
+    android:title="@string/external_display_settings_title">
+</PreferenceScreen>
diff --git a/res/xml/trackpad_settings.xml b/res/xml/trackpad_settings.xml
index 1eb16b7..04422dd 100644
--- a/res/xml/trackpad_settings.xml
+++ b/res/xml/trackpad_settings.xml
@@ -66,9 +66,19 @@
         android:key="pointer_fill_style"
         android:title="@string/pointer_fill_style"
         android:order="50"
-        android:dialogTitle="@string/pointer_fill_style"
         settings:controller="com.android.settings.inputmethod.PointerFillStylePreferenceController"/>
 
+    <com.android.settings.widget.LabeledSeekBarPreference
+        android:key="pointer_scale"
+        android:title="@string/pointer_scale"
+        android:order="70"
+        android:max="@integer/pointer_scale_seek_bar_end"
+        settings:iconStart="@drawable/ic_remove_24dp"
+        settings:iconStartContentDescription="@string/pointer_scale_decrease_content_description"
+        settings:iconEnd="@drawable/ic_add_24dp"
+        settings:iconEndContentDescription="@string/pointer_scale_increase_content_description"
+        settings:controller="com.android.settings.inputmethod.PointerScaleSeekBarController" />
+
     <com.android.settingslib.widget.ButtonPreference
         android:key="trackpad_touch_gesture"
         android:title="@string/trackpad_touch_gesture"
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index 3367bf1..e3bb1a1 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -18,6 +18,8 @@
 
 import static android.provider.Settings.ACTION_PRIVACY_SETTINGS;
 
+import android.annotation.FlaggedApi;
+import android.app.Flags;
 import android.content.ActivityNotFoundException;
 import android.content.Intent;
 import android.os.Bundle;
@@ -317,11 +319,13 @@
     public static class PrintSettingsActivity extends SettingsActivity { /* empty */ }
     public static class PrintJobSettingsActivity extends SettingsActivity { /* empty */ }
     public static class ZenModeSettingsActivity extends SettingsActivity { /* empty */ }
-    public static class ZenModeBehaviorSettingsActivity extends SettingsActivity { /* empty */ }
-    public static class ZenModeBlockedEffectsSettingsActivity extends SettingsActivity { /* empty */ }
     public static class ZenModeAutomationSettingsActivity extends SettingsActivity { /* empty */ }
     public static class ZenModeScheduleRuleSettingsActivity extends SettingsActivity { /* empty */ }
     public static class ZenModeEventRuleSettingsActivity extends SettingsActivity { /* empty */ }
+    @FlaggedApi(Flags.FLAG_MODES_UI)
+    public static class ModeSettingsActivity extends SettingsActivity { /* empty */ }
+    @FlaggedApi(Flags.FLAG_MODES_UI)
+    public static class ModesSettingsActivity extends SettingsActivity { /* empty */ }
     public static class SoundSettingsActivity extends SettingsActivity { /* empty */ }
     public static class ConfigureNotificationSettingsActivity extends SettingsActivity { /* empty */ }
     public static class ConversationListSettingsActivity extends SettingsActivity { /* empty */ }
diff --git a/src/com/android/settings/SettingsPreferenceFragmentBase.java b/src/com/android/settings/SettingsPreferenceFragmentBase.java
new file mode 100644
index 0000000..dd2e287
--- /dev/null
+++ b/src/com/android/settings/SettingsPreferenceFragmentBase.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.search.Indexable;
+
+/**
+ * Base class for fragment suitable for unit testing.
+ */
+public abstract class SettingsPreferenceFragmentBase extends SettingsPreferenceFragment
+        implements Indexable {
+    @Override
+    @SuppressWarnings({"RequiresNullabilityAnnotation"})
+    public void onCreate(final Bundle icicle) {
+        super.onCreate(icicle);
+        onCreateCallback(icicle);
+    }
+
+    @Override
+    @SuppressWarnings({"RequiresNullabilityAnnotation"})
+    public void onActivityCreated(final Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        onActivityCreatedCallback(savedInstanceState);
+    }
+
+    @Override
+    public void onSaveInstanceState(@NonNull final Bundle outState) {
+        super.onSaveInstanceState(outState);
+        onSaveInstanceStateCallback(outState);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        onStartCallback();
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        onStopCallback();
+    }
+
+    protected Activity getCurrentActivity() {
+        return getActivity();
+    }
+
+    /**
+     * Callback called from {@link #onCreate}
+     */
+    public abstract void onCreateCallback(@Nullable Bundle icicle);
+
+    /**
+     * Callback called from {@link #onActivityCreated}
+     */
+    public abstract void onActivityCreatedCallback(@Nullable Bundle savedInstanceState);
+
+    /**
+     * Callback called from {@link #onStart}
+     */
+    public abstract void onStartCallback();
+
+    /**
+     * Callback called from {@link #onStop}
+     */
+    public abstract void onStopCallback();
+
+    /**
+     * Callback called from {@link #onSaveInstanceState}
+     */
+    public void onSaveInstanceStateCallback(@NonNull final Bundle outState) {
+        // Do nothing.
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java
index 56a3005..2548b95 100644
--- a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java
+++ b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java
@@ -15,6 +15,8 @@
  */
 package com.android.settings.connecteddevice;
 
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isExternalDisplaySettingsPageEnabled;
+
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.hardware.input.InputManager;
@@ -22,6 +24,8 @@
 import android.util.Log;
 import android.view.InputDevice;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceGroup;
@@ -31,12 +35,15 @@
 import com.android.settings.bluetooth.BluetoothDeviceUpdater;
 import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater;
 import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.display.ExternalDisplayUpdater;
 import com.android.settings.connecteddevice.dock.DockUpdater;
 import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater;
 import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater;
 import com.android.settings.core.BasePreferenceController;
 import com.android.settings.core.PreferenceControllerMixin;
 import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.flags.FeatureFlags;
+import com.android.settings.flags.FeatureFlagsImpl;
 import com.android.settings.flags.Flags;
 import com.android.settings.overlay.DockUpdaterFeatureProvider;
 import com.android.settings.overlay.FeatureFactory;
@@ -64,6 +71,8 @@
 
     @VisibleForTesting
     PreferenceGroup mPreferenceGroup;
+    @Nullable
+    private ExternalDisplayUpdater mExternalDisplayUpdater;
     private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
     private ConnectedUsbDeviceUpdater mConnectedUsbDeviceUpdater;
     private DockUpdater mConnectedDockUpdater;
@@ -71,6 +80,8 @@
     private final PackageManager mPackageManager;
     private final InputManager mInputManager;
     private final LocalBluetoothManager mLocalBluetoothManager;
+    @NonNull
+    private final FeatureFlags mFeatureFlags = new FeatureFlagsImpl();
 
     public ConnectedDeviceGroupController(Context context) {
         super(context, KEY);
@@ -81,6 +92,10 @@
 
     @Override
     public void onStart() {
+        if (mExternalDisplayUpdater != null) {
+            mExternalDisplayUpdater.registerCallback();
+        }
+
         if (mBluetoothDeviceUpdater != null) {
             mBluetoothDeviceUpdater.registerCallback();
             mBluetoothDeviceUpdater.refreshPreference();
@@ -101,6 +116,10 @@
 
     @Override
     public void onStop() {
+        if (mExternalDisplayUpdater != null) {
+            mExternalDisplayUpdater.unregisterCallback();
+        }
+
         if (mBluetoothDeviceUpdater != null) {
             mBluetoothDeviceUpdater.unregisterCallback();
         }
@@ -127,6 +146,10 @@
 
         if (isAvailable()) {
             final Context context = screen.getContext();
+            if (mExternalDisplayUpdater != null) {
+                mExternalDisplayUpdater.initPreference(context);
+            }
+
             if (mBluetoothDeviceUpdater != null) {
                 mBluetoothDeviceUpdater.setPrefContext(context);
                 mBluetoothDeviceUpdater.forceUpdate();
@@ -150,7 +173,8 @@
 
     @Override
     public int getAvailabilityStatus() {
-        return (hasBluetoothFeature()
+        return (hasExternalDisplayFeature()
+                || hasBluetoothFeature()
                 || hasUsbFeature()
                 || hasUsiStylusFeature()
                 || mConnectedDockUpdater != null)
@@ -180,11 +204,13 @@
     }
 
     @VisibleForTesting
-    void init(BluetoothDeviceUpdater bluetoothDeviceUpdater,
+    void init(@Nullable ExternalDisplayUpdater externalDisplayUpdater,
+            BluetoothDeviceUpdater bluetoothDeviceUpdater,
             ConnectedUsbDeviceUpdater connectedUsbDeviceUpdater,
             DockUpdater connectedDockUpdater,
             StylusDeviceUpdater connectedStylusDeviceUpdater) {
 
+        mExternalDisplayUpdater = externalDisplayUpdater;
         mBluetoothDeviceUpdater = bluetoothDeviceUpdater;
         mConnectedUsbDeviceUpdater = connectedUsbDeviceUpdater;
         mConnectedDockUpdater = connectedDockUpdater;
@@ -197,7 +223,10 @@
                 FeatureFactory.getFeatureFactory().getDockUpdaterFeatureProvider();
         final DockUpdater connectedDockUpdater =
                 dockUpdaterFeatureProvider.getConnectedDockUpdater(context, this);
-        init(hasBluetoothFeature()
+        init(hasExternalDisplayFeature()
+                        ? new ExternalDisplayUpdater(this, fragment.getMetricsCategory())
+                        : null,
+                hasBluetoothFeature()
                         ? new ConnectedBluetoothDeviceUpdater(context, this,
                         fragment.getMetricsCategory())
                         : null,
@@ -210,6 +239,19 @@
                         : null);
     }
 
+    /**
+     * @return trunk stable feature flags.
+     */
+    @VisibleForTesting
+    @NonNull
+    public FeatureFlags getFeatureFlags() {
+        return mFeatureFlags;
+    }
+
+    private boolean hasExternalDisplayFeature() {
+        return isExternalDisplaySettingsPageEnabled(getFeatureFlags());
+    }
+
     private boolean hasBluetoothFeature() {
         return mPackageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
     }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
index 0bb6b60..bfccdc4 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreference.java
@@ -95,14 +95,14 @@
     }
 
     private void configureInvisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
-        divider.setVisibility(View.INVISIBLE);
-        shareButton.setVisibility(View.INVISIBLE);
+        divider.setVisibility(View.GONE);
+        shareButton.setVisibility(View.GONE);
         shareButton.setOnClickListener(null);
     }
 
     private void launchAudioSharingQrCodeFragment() {
         new SubSettingLauncher(getContext())
-                .setTitleText(getContext().getString(R.string.audio_streams_qr_code_page_title))
+                .setTitleRes(R.string.audio_streams_qr_code_page_title)
                 .setDestination(AudioStreamsQrCodeFragment.class.getName())
                 .setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
                 .launch();
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
index 24b8f20..894ba48 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceController.java
@@ -26,6 +26,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
 import androidx.preference.Preference;
@@ -56,7 +57,8 @@
     private static final boolean DEBUG = BluetoothUtils.D;
     private static final String PREF_KEY = "audio_sharing_stream_name";
 
-    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
+    @VisibleForTesting
+    final BluetoothLeBroadcast.Callback mBroadcastCallback =
             new BluetoothLeBroadcast.Callback() {
                 @Override
                 public void onBroadcastMetadataChanged(
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
index 258cf3b..14930e1 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceController.java
@@ -19,12 +19,19 @@
 import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.isBroadcasting;
 
 import android.app.settings.SettingsEnums;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
 import androidx.preference.PreferenceScreen;
 
 import com.android.settings.R;
@@ -41,15 +48,19 @@
 
 public class AudioSharingPasswordPreferenceController extends BasePreferenceController
         implements ValidatedEditTextPreference.Validator,
-                AudioSharingPasswordPreference.OnDialogEventListener {
-
+                AudioSharingPasswordPreference.OnDialogEventListener,
+                DefaultLifecycleObserver {
     private static final String TAG = "AudioSharingPasswordPreferenceController";
     private static final String PREF_KEY = "audio_sharing_stream_password";
     private static final String SHARED_PREF_NAME = "audio_sharing_settings";
     private static final String SHARED_PREF_KEY = "default_password";
+    @Nullable private final ContentResolver mContentResolver;
+    @Nullable private final SharedPreferences mSharedPref;
     @Nullable private final LocalBluetoothManager mBtManager;
     @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
     @Nullable private AudioSharingPasswordPreference mPreference;
+    private final ContentObserver mSettingsObserver;
+    private final SharedPreferences.OnSharedPreferenceChangeListener mSharedPrefChangeListener;
     private final AudioSharingPasswordValidator mAudioSharingPasswordValidator;
     private final MetricsFeatureProvider mMetricsFeatureProvider;
 
@@ -61,10 +72,45 @@
                         ? mBtManager.getProfileManager().getLeAudioBroadcastProfile()
                         : null;
         mAudioSharingPasswordValidator = new AudioSharingPasswordValidator();
+        mContentResolver = context.getContentResolver();
+        mSettingsObserver = new PasswordSettingsObserver();
+        mSharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
+        mSharedPrefChangeListener = new PasswordSharedPrefChangeListener();
         mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
     }
 
     @Override
+    public void onStart(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Feature is not available.");
+            return;
+        }
+        if (mContentResolver != null) {
+            mContentResolver.registerContentObserver(
+                    Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE),
+                    false,
+                    mSettingsObserver);
+        }
+        if (mSharedPref != null) {
+            mSharedPref.registerOnSharedPreferenceChangeListener(mSharedPrefChangeListener);
+        }
+    }
+
+    @Override
+    public void onStop(@NonNull LifecycleOwner owner) {
+        if (!isAvailable()) {
+            Log.d(TAG, "Feature is not available.");
+            return;
+        }
+        if (mContentResolver != null) {
+            mContentResolver.unregisterContentObserver(mSettingsObserver);
+        }
+        if (mSharedPref != null) {
+            mSharedPref.unregisterOnSharedPreferenceChangeListener(mSharedPrefChangeListener);
+        }
+    }
+
+    @Override
     public int getAvailabilityStatus() {
         return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
     }
@@ -125,7 +171,6 @@
                             persistDefaultPassword(mContext, password);
                             mBroadcast.setBroadcastCode(
                                     isPublicBroadcast ? new byte[0] : password.getBytes());
-                            updatePreference();
                             mMetricsFeatureProvider.action(
                                     mContext,
                                     SettingsEnums.ACTION_AUDIO_STREAM_PASSWORD_UPDATED,
@@ -164,32 +209,52 @@
                         });
     }
 
-    private static void persistDefaultPassword(Context context, String defaultPassword) {
+    private class PasswordSettingsObserver extends ContentObserver {
+        PasswordSettingsObserver() {
+            super(new Handler(Looper.getMainLooper()));
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            Log.d(TAG, "onChange, broadcast password has been changed");
+            updatePreference();
+        }
+    }
+
+    private class PasswordSharedPrefChangeListener
+            implements SharedPreferences.OnSharedPreferenceChangeListener {
+        @Override
+        public void onSharedPreferenceChanged(
+                SharedPreferences sharedPreferences, @Nullable String key) {
+            if (!SHARED_PREF_KEY.equals(key)) {
+                return;
+            }
+            Log.d(TAG, "onSharedPreferenceChanged, default password has been changed");
+            updatePreference();
+        }
+    }
+
+    private void persistDefaultPassword(Context context, String defaultPassword) {
         if (getDefaultPassword(context).equals(defaultPassword)) {
             return;
         }
-
-        SharedPreferences sharedPref =
-                context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
-        if (sharedPref == null) {
+        if (mSharedPref == null) {
             Log.w(TAG, "persistDefaultPassword(): sharedPref is empty!");
             return;
         }
 
-        SharedPreferences.Editor editor = sharedPref.edit();
+        SharedPreferences.Editor editor = mSharedPref.edit();
         editor.putString(SHARED_PREF_KEY, defaultPassword);
         editor.apply();
     }
 
-    private static String getDefaultPassword(Context context) {
-        SharedPreferences sharedPref =
-                context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
-        if (sharedPref == null) {
+    private String getDefaultPassword(Context context) {
+        if (mSharedPref == null) {
             Log.w(TAG, "getDefaultPassword(): sharedPref is empty!");
             return "";
         }
 
-        String value = sharedPref.getString(SHARED_PREF_KEY, "");
+        String value = mSharedPref.getString(SHARED_PREF_KEY, "");
         if (value != null && value.isEmpty()) {
             Log.w(TAG, "getDefaultPassword(): default password is empty!");
         }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseState.java
index 24a28dd..7be01a2 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseState.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseState.java
@@ -36,7 +36,8 @@
 
     @Nullable private static AddSourceWaitForResponseState sInstance = null;
 
-    private AddSourceWaitForResponseState() {}
+    @VisibleForTesting
+    AddSourceWaitForResponseState() {}
 
     static AddSourceWaitForResponseState getInstance() {
         if (sInstance == null) {
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java
index b0c5b6b..4bb8475 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandler.java
@@ -35,10 +35,10 @@
     private static final boolean DEBUG = BluetoothUtils.D;
     @VisibleForTesting static final int EMPTY_STRING_RES = 0;
 
-    final AudioStreamsRepository mAudioStreamsRepository = AudioStreamsRepository.getInstance();
     final Handler mHandler = new Handler(Looper.getMainLooper());
     final MetricsFeatureProvider mMetricsFeatureProvider =
             FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
+    AudioStreamsRepository mAudioStreamsRepository = AudioStreamsRepository.getInstance();
 
     AudioStreamStateHandler() {}
 
@@ -112,4 +112,9 @@
     AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
         return AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN;
     }
+
+    @VisibleForTesting
+    void setAudioStreamsRepositoryForTesting(AudioStreamsRepository repository) {
+        mAudioStreamsRepository = repository;
+    }
 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdater.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdater.java
index ab22b07..47ee440 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdater.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdater.java
@@ -16,24 +16,22 @@
 
 package com.android.settings.connecteddevice.audiosharing.audiostreams;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 import android.text.TextUtils;
-import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.settings.R;
 import com.android.settings.bluetooth.Utils;
 import com.android.settingslib.bluetooth.BluetoothCallback;
-import com.android.settingslib.bluetooth.BluetoothUtils;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.utils.ThreadUtils;
 
 public class AudioStreamsActiveDeviceSummaryUpdater implements BluetoothCallback {
-    private static final String TAG = "AudioStreamsActiveDeviceSummaryUpdater";
-    private static final boolean DEBUG = BluetoothUtils.D;
     private final LocalBluetoothManager mBluetoothManager;
     private Context mContext;
     @Nullable private String mSummary;
@@ -47,17 +45,20 @@
     }
 
     @Override
-    public void onActiveDeviceChanged(
-            @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
-        if (DEBUG) {
-            Log.d(
-                    TAG,
-                    "onActiveDeviceChanged() with activeDevice : "
-                            + (activeDevice == null ? "null" : activeDevice.getAddress())
-                            + " on profile : "
-                            + bluetoothProfile);
+    public void onBluetoothStateChanged(@AdapterState int bluetoothState) {
+        if (bluetoothState == BluetoothAdapter.STATE_OFF) {
+            notifyChangeIfNeeded();
         }
-        if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
+    }
+
+    @Override
+    public void onProfileConnectionStateChanged(
+            @NonNull CachedBluetoothDevice cachedDevice,
+            @ConnectionState int state,
+            int bluetoothProfile) {
+        if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
+                && (state == BluetoothAdapter.STATE_CONNECTED
+                        || state == BluetoothAdapter.STATE_DISCONNECTED)) {
             notifyChangeIfNeeded();
         }
     }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsCategoryController.java
index 3174ace..0107c6e 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsCategoryController.java
@@ -16,12 +16,12 @@
 
 package com.android.settings.connecteddevice.audiosharing.audiostreams;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.lifecycle.LifecycleOwner;
 
 import com.android.settings.bluetooth.Utils;
@@ -44,9 +44,13 @@
     private final BluetoothCallback mBluetoothCallback =
             new BluetoothCallback() {
                 @Override
-                public void onActiveDeviceChanged(
-                        @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
-                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
+                public void onProfileConnectionStateChanged(
+                        @NonNull CachedBluetoothDevice cachedDevice,
+                        @ConnectionState int state,
+                        int bluetoothProfile) {
+                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
+                            && (state == BluetoothAdapter.STATE_CONNECTED
+                                    || state == BluetoothAdapter.STATE_DISCONNECTED)) {
                         updateVisibility();
                     }
                 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
index 775186a..6e335a0 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java
@@ -71,7 +71,8 @@
      *
      * @param source The LE broadcast metadata representing the audio source.
      */
-    void addSource(BluetoothLeBroadcastMetadata source) {
+    @VisibleForTesting
+    public void addSource(BluetoothLeBroadcastMetadata source) {
         if (mLeBroadcastAssistant == null) {
             Log.w(TAG, "addSource(): LeBroadcastAssistant is null!");
             return;
@@ -97,7 +98,8 @@
     }
 
     /** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */
-    void removeSource(int broadcastId) {
+    @VisibleForTesting
+    public void removeSource(int broadcastId) {
         if (mLeBroadcastAssistant == null) {
             Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
             return;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
index cb3a0da..3370d8d 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallback.java
@@ -19,7 +19,6 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothLeBroadcastMetadata;
 import android.bluetooth.BluetoothLeBroadcastReceiveState;
-import android.util.Log;
 
 public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback {
     private static final String TAG = "AudioStreamsProgressCategoryCallback";
@@ -53,10 +52,6 @@
     @Override
     public void onSearchStarted(int reason) {
         super.onSearchStarted(reason);
-        if (mCategoryController == null) {
-            Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
-            return;
-        }
         mCategoryController.setScanning(true);
     }
 
@@ -69,10 +64,6 @@
     @Override
     public void onSearchStopped(int reason) {
         super.onSearchStopped(reason);
-        if (mCategoryController == null) {
-            Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
-            return;
-        }
         mCategoryController.setScanning(false);
     }
 
@@ -86,10 +77,6 @@
     @Override
     public void onSourceFound(BluetoothLeBroadcastMetadata source) {
         super.onSourceFound(source);
-        if (mCategoryController == null) {
-            Log.w(TAG, "onSourceFound() : mCategoryController is null!");
-            return;
-        }
         mCategoryController.handleSourceFound(source);
     }
 
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
index 890879e..9bbf135 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryController.java
@@ -20,6 +20,7 @@
 
 import android.app.AlertDialog;
 import android.app.settings.SettingsEnums;
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothLeBroadcastMetadata;
 import android.bluetooth.BluetoothLeBroadcastReceiveState;
 import android.bluetooth.BluetoothProfile;
@@ -27,10 +28,12 @@
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
 import androidx.lifecycle.DefaultLifecycleObserver;
 import androidx.lifecycle.LifecycleOwner;
 import androidx.preference.PreferenceScreen;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.settings.R;
 import com.android.settings.bluetooth.Utils;
 import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment;
@@ -55,13 +58,35 @@
         implements DefaultLifecycleObserver {
     private static final String TAG = "AudioStreamsProgressCategoryController";
     private static final boolean DEBUG = BluetoothUtils.D;
-    private static final int UNSET_BROADCAST_ID = -1;
-    private final BluetoothCallback mBluetoothCallback =
+    @VisibleForTesting static final int UNSET_BROADCAST_ID = -1;
+
+    @VisibleForTesting
+    final BluetoothCallback mBluetoothCallback =
             new BluetoothCallback() {
                 @Override
-                public void onActiveDeviceChanged(
-                        @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
-                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
+                public void onBluetoothStateChanged(@AdapterState int bluetoothState) {
+                    Log.d(TAG, "onBluetoothStateChanged() with bluetoothState : " + bluetoothState);
+                    if (bluetoothState == BluetoothAdapter.STATE_OFF) {
+                        mExecutor.execute(() -> init());
+                    }
+                }
+
+                @Override
+                public void onProfileConnectionStateChanged(
+                        @NonNull CachedBluetoothDevice cachedDevice,
+                        @ConnectionState int state,
+                        int bluetoothProfile) {
+                    Log.d(
+                            TAG,
+                            "onProfileConnectionStateChanged() with cachedDevice : "
+                                    + cachedDevice.getAddress()
+                                    + " with state : "
+                                    + state
+                                    + " on profile : "
+                                    + bluetoothProfile);
+                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
+                            && (state == BluetoothAdapter.STATE_CONNECTED
+                                    || state == BluetoothAdapter.STATE_DISCONNECTED)) {
                         mExecutor.execute(() -> init());
                     }
                 }
@@ -92,7 +117,7 @@
         SOURCE_ADDED,
     }
 
-    private final Executor mExecutor;
+    @VisibleForTesting Executor mExecutor;
     private final AudioStreamsProgressCategoryCallback mBroadcastAssistantCallback;
     private final AudioStreamsHelper mAudioStreamsHelper;
     private final MediaControlHelper mMediaControlHelper;
@@ -103,7 +128,7 @@
     private @Nullable BluetoothLeBroadcastMetadata mSourceFromQrCode;
     private SourceOriginForLogging mSourceFromQrCodeOriginForLogging;
     @Nullable private AudioStreamsProgressCategoryPreference mCategoryPreference;
-    @Nullable private AudioStreamsDashboardFragment mFragment;
+    @Nullable private Fragment mFragment;
 
     public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
         super(context, preferenceKey);
@@ -142,12 +167,12 @@
         mExecutor.execute(this::stopScanning);
     }
 
-    void setFragment(AudioStreamsDashboardFragment fragment) {
+    void setFragment(Fragment fragment) {
         mFragment = fragment;
     }
 
     @Nullable
-    AudioStreamsDashboardFragment getFragment() {
+    Fragment getFragment() {
         return mFragment;
     }
 
@@ -546,7 +571,8 @@
         return preference;
     }
 
-    private void moveToState(AudioStreamPreference preference, AudioStreamState state) {
+    @VisibleForTesting
+    void moveToState(AudioStreamPreference preference, AudioStreamState state) {
         AudioStreamStateHandler stateHandler =
                 switch (state) {
                     case SYNCED -> SyncedState.getInstance();
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeController.java
index 5f50be7..d0d82fb 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.BluetoothAdapter;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 import android.util.Log;
@@ -47,9 +48,13 @@
     final BluetoothCallback mBluetoothCallback =
             new BluetoothCallback() {
                 @Override
-                public void onActiveDeviceChanged(
-                        @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
-                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
+                public void onProfileConnectionStateChanged(
+                        @NonNull CachedBluetoothDevice cachedDevice,
+                        @ConnectionState int state,
+                        int bluetoothProfile) {
+                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
+                            && (state == BluetoothAdapter.STATE_CONNECTED
+                                    || state == BluetoothAdapter.STATE_DISCONNECTED)) {
                         updateVisibility();
                     }
                 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedState.java
index ee84429..88393ab 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedState.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedState.java
@@ -25,6 +25,7 @@
 
 import com.android.settings.R;
 import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.dashboard.DashboardFragment;
 
 class SourceAddedState extends AudioStreamStateHandler {
     @VisibleForTesting
@@ -32,7 +33,8 @@
 
     @Nullable private static SourceAddedState sInstance = null;
 
-    private SourceAddedState() {}
+    @VisibleForTesting
+    SourceAddedState() {}
 
     static SourceAddedState getInstance() {
         if (sInstance == null) {
@@ -80,13 +82,13 @@
                     AudioStreamDetailsFragment.BROADCAST_ID_ARG, p.getAudioStreamBroadcastId());
 
             new SubSettingLauncher(p.getContext())
-                    .setTitleText(
-                            p.getContext().getString(R.string.audio_streams_detail_page_title))
+                    .setTitleRes(R.string.audio_streams_detail_page_title)
                     .setDestination(AudioStreamDetailsFragment.class.getName())
                     .setSourceMetricsCategory(
-                            controller.getFragment() == null
+                            !(controller.getFragment() instanceof DashboardFragment)
                                     ? SettingsEnums.PAGE_UNKNOWN
-                                    : controller.getFragment().getMetricsCategory())
+                                    : ((DashboardFragment) controller.getFragment())
+                                            .getMetricsCategory())
                     .setArguments(broadcast)
                     .launch();
             return true;
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncState.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncState.java
index 55f61fd..9689b26 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncState.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncState.java
@@ -39,7 +39,8 @@
 
     @Nullable private static WaitForSyncState sInstance = null;
 
-    private WaitForSyncState() {}
+    @VisibleForTesting
+    WaitForSyncState() {}
 
     static WaitForSyncState getInstance() {
         if (sInstance == null) {
@@ -114,7 +115,8 @@
                 SettingsEnums.DIALOG_AUDIO_STREAM_MAIN_WAIT_FOR_SYNC_TIMEOUT);
     }
 
-    private void launchQrCodeScanFragment(Context context, Fragment fragment) {
+    @VisibleForTesting
+    void launchQrCodeScanFragment(Context context, Fragment fragment) {
         new SubSettingLauncher(context)
                 .setTitleRes(R.string.audio_streams_main_page_scan_qr_code_title)
                 .setDestination(AudioStreamsQrCodeScanFragment.class.getName())
diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java b/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java
new file mode 100644
index 0000000..09f8e92
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragment.java
@@ -0,0 +1,544 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.display;
+
+
+import static android.view.Display.INVALID_DISPLAY;
+
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_HELP_URL;
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DISPLAY_ID_ARG;
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE;
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed;
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isUseDisplaySettingEnabled;
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isResolutionSettingEnabled;
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isRotationSettingEnabled;
+
+import android.app.Activity;
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Display;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settings.SettingsPreferenceFragmentBase;
+import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener;
+import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.search.BaseSearchIndexProvider;
+import com.android.settingslib.search.Indexable;
+import com.android.settingslib.search.SearchIndexable;
+import com.android.settingslib.widget.FooterPreference;
+import com.android.settingslib.widget.IllustrationPreference;
+import com.android.settingslib.widget.MainSwitchPreference;
+import com.android.settingslib.widget.TwoTargetPreference;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The Settings screen for External Displays configuration and connection management.
+ */
+@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
+public class ExternalDisplayPreferenceFragment extends SettingsPreferenceFragmentBase
+        implements Indexable {
+    static final int EXTERNAL_DISPLAY_SETTINGS_RESOURCE = R.xml.external_display_settings;
+    public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
+            new BaseSearchIndexProvider(EXTERNAL_DISPLAY_SETTINGS_RESOURCE);
+    static final String DISPLAYS_LIST_PREFERENCE_KEY = "displays_list_preference";
+    static final String EXTERNAL_DISPLAY_USE_PREFERENCE_KEY = "external_display_use_preference";
+    static final String EXTERNAL_DISPLAY_ROTATION_KEY = "external_display_rotation";
+    static final String EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY = "external_display_resolution";
+    static final int EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE =
+            R.string.external_display_change_resolution_footer_title;
+    static final int EXTERNAL_DISPLAY_LANDSCAPE_DRAWABLE =
+            R.drawable.external_display_mirror_landscape;
+    static final int EXTERANAL_DISPLAY_TITLE_RESOURCE =
+            R.string.external_display_settings_title;
+    static final int EXTERNAL_DISPLAY_USE_TITLE_RESOURCE =
+            R.string.external_display_use_title;
+    static final int EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE =
+            R.string.external_display_not_found_footer_title;
+    static final int EXTERNAL_DISPLAY_PORTRAIT_DRAWABLE =
+            R.drawable.external_display_mirror_portrait;
+    static final int EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE =
+            R.string.external_display_rotation;
+    static final int EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE =
+            R.string.external_display_resolution_settings_title;
+    @VisibleForTesting
+    static final String PREVIOUSLY_SHOWN_LIST_KEY = "mPreviouslyShownListOfDisplays";
+    private boolean mStarted;
+    @Nullable
+    private MainSwitchPreference mUseDisplayPref;
+    @Nullable
+    private IllustrationPreference mImagePreference;
+    @Nullable
+    private Preference mResolutionPreference;
+    @Nullable
+    private ListPreference mRotationPref;
+    @Nullable
+    private FooterPreference mFooterPreference;
+    @Nullable
+    private PreferenceCategory mDisplaysPreference;
+    @Nullable
+    private Injector mInjector;
+    @Nullable
+    private String[] mRotationEntries;
+    @Nullable
+    private String[] mRotationEntriesValues;
+    @NonNull
+    private final Runnable mUpdateRunnable = this::update;
+    private final DisplayListener mListener = new DisplayListener() {
+        @Override
+        public void update(int displayId) {
+            scheduleUpdate();
+        }
+    };
+    private boolean mPreviouslyShownListOfDisplays;
+
+    public ExternalDisplayPreferenceFragment() {}
+
+    @VisibleForTesting
+    ExternalDisplayPreferenceFragment(@NonNull Injector injector) {
+        mInjector = injector;
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY;
+    }
+
+    @Override
+    public int getHelpResource() {
+        return EXTERNAL_DISPLAY_HELP_URL;
+    }
+
+    @Override
+    public void onSaveInstanceStateCallback(@NonNull Bundle outState) {
+        outState.putSerializable(PREVIOUSLY_SHOWN_LIST_KEY,
+                (Serializable) mPreviouslyShownListOfDisplays);
+    }
+
+    @Override
+    public void onCreateCallback(@Nullable Bundle icicle) {
+        if (mInjector == null) {
+            mInjector = new Injector(getPrefContext());
+        }
+        addPreferencesFromResource(EXTERNAL_DISPLAY_SETTINGS_RESOURCE);
+    }
+
+    @Override
+    public void onActivityCreatedCallback(@Nullable Bundle savedInstanceState) {
+        restoreState(savedInstanceState);
+        View view = getView();
+        TextView emptyView = null;
+        if (view != null) {
+            emptyView = (TextView) view.findViewById(android.R.id.empty);
+        }
+        if (emptyView != null) {
+            emptyView.setText(EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE);
+            setEmptyView(emptyView);
+        }
+    }
+
+    @Override
+    public void onStartCallback() {
+        mStarted = true;
+        if (mInjector == null) {
+            return;
+        }
+        mInjector.registerDisplayListener(mListener);
+        scheduleUpdate();
+    }
+
+    @Override
+    public void onStopCallback() {
+        mStarted = false;
+        if (mInjector == null) {
+            return;
+        }
+        mInjector.unregisterDisplayListener(mListener);
+        unscheduleUpdate();
+    }
+
+    /**
+     * @return id of the preference.
+     */
+    @Override
+    protected int getPreferenceScreenResId() {
+        return EXTERNAL_DISPLAY_SETTINGS_RESOURCE;
+    }
+
+    @VisibleForTesting
+    protected void launchResolutionSelector(@NonNull final Context context, final int displayId) {
+        final Bundle args = new Bundle();
+        args.putInt(DISPLAY_ID_ARG, displayId);
+        new SubSettingLauncher(context)
+                .setDestination(ResolutionPreferenceFragment.class.getName())
+                .setArguments(args)
+                .setSourceMetricsCategory(getMetricsCategory()).launch();
+    }
+
+    @VisibleForTesting
+    protected void launchDisplaySettings(final int displayId) {
+        final Bundle args = new Bundle();
+        var context = getPrefContext();
+        args.putInt(DISPLAY_ID_ARG, displayId);
+        new SubSettingLauncher(context)
+                .setDestination(this.getClass().getName())
+                .setArguments(args)
+                .setSourceMetricsCategory(getMetricsCategory()).launch();
+    }
+
+    /**
+     * Returns the preference for the footer.
+     */
+    @NonNull
+    @VisibleForTesting
+    FooterPreference getFooterPreference(@NonNull Context context) {
+        if (mFooterPreference == null) {
+            mFooterPreference = new FooterPreference(context);
+            mFooterPreference.setPersistent(false);
+        }
+        return mFooterPreference;
+    }
+
+    @NonNull
+    @VisibleForTesting
+    ListPreference getRotationPreference(@NonNull Context context) {
+        if (mRotationPref == null) {
+            mRotationPref = new ListPreference(context);
+            mRotationPref.setPersistent(false);
+        }
+        return mRotationPref;
+    }
+
+    @NonNull
+    @VisibleForTesting
+    Preference getResolutionPreference(@NonNull Context context) {
+        if (mResolutionPreference == null) {
+            mResolutionPreference = new Preference(context);
+            mResolutionPreference.setPersistent(false);
+        }
+        return mResolutionPreference;
+    }
+
+    @NonNull
+    @VisibleForTesting
+    MainSwitchPreference getUseDisplayPreference(@NonNull Context context) {
+        if (mUseDisplayPref == null) {
+            mUseDisplayPref = new MainSwitchPreference(context);
+            mUseDisplayPref.setPersistent(false);
+        }
+        return mUseDisplayPref;
+    }
+
+    @NonNull
+    @VisibleForTesting
+    IllustrationPreference getIllustrationPreference(@NonNull Context context) {
+        if (mImagePreference == null) {
+            mImagePreference = new IllustrationPreference(context);
+            mImagePreference.setPersistent(false);
+        }
+        return mImagePreference;
+    }
+
+    /**
+     * @return return display id argument of this settings page.
+     */
+    @VisibleForTesting
+    protected int getDisplayIdArg() {
+        var args = getArguments();
+        return args != null ? args.getInt(DISPLAY_ID_ARG, INVALID_DISPLAY) : INVALID_DISPLAY;
+    }
+
+    @NonNull
+    private PreferenceCategory getDisplaysListPreference(@NonNull Context context) {
+        if (mDisplaysPreference == null) {
+            mDisplaysPreference = new PreferenceCategory(context);
+            mDisplaysPreference.setPersistent(false);
+        }
+        return mDisplaysPreference;
+    }
+
+    private void restoreState(@Nullable Bundle savedInstanceState) {
+        if (savedInstanceState == null) {
+            return;
+        }
+        mPreviouslyShownListOfDisplays = Boolean.TRUE.equals(savedInstanceState.getSerializable(
+                PREVIOUSLY_SHOWN_LIST_KEY, Boolean.class));
+    }
+
+    private void update() {
+        final var screen = getPreferenceScreen();
+        if (screen == null || mInjector == null || mInjector.getContext() == null) {
+            return;
+        }
+        screen.removeAll();
+        updateScreenForDisplayId(getDisplayIdArg(), screen, mInjector.getContext());
+    }
+
+    private void updateScreenForDisplayId(final int displayId,
+            @NonNull final PreferenceScreen screen, @NonNull Context context) {
+        final var displaysToShow = getDisplaysToShow(displayId);
+        if (displaysToShow.isEmpty() && displayId == INVALID_DISPLAY) {
+            showTextWhenNoDisplaysToShow(screen, context);
+        } else if (displaysToShow.size() == 1
+                && ((displayId == INVALID_DISPLAY && !mPreviouslyShownListOfDisplays)
+                        || displaysToShow.get(0).getDisplayId() == displayId)) {
+            showDisplaySettings(displaysToShow.get(0), screen, context);
+        } else if (displayId == INVALID_DISPLAY) {
+            // If ever shown a list of displays - keep showing it for consistency after
+            // disconnecting one of the displays, and only one display is left.
+            mPreviouslyShownListOfDisplays = true;
+            showDisplaysList(displaysToShow, screen, context);
+        }
+        updateSettingsTitle(displaysToShow, displayId);
+    }
+
+    private void updateSettingsTitle(@NonNull final List<Display> displaysToShow, int displayId) {
+        final Activity activity = getCurrentActivity();
+        if (activity == null) {
+            return;
+        }
+        if (displaysToShow.size() == 1 && displaysToShow.get(0).getDisplayId() == displayId) {
+            var displayName = displaysToShow.get(0).getName();
+            if (!displayName.isEmpty()) {
+                activity.setTitle(displayName.substring(0, Math.min(displayName.length(), 40)));
+                return;
+            }
+        }
+        activity.setTitle(EXTERANAL_DISPLAY_TITLE_RESOURCE);
+    }
+
+    private void showTextWhenNoDisplaysToShow(@NonNull final PreferenceScreen screen,
+            @NonNull Context context) {
+        if (isUseDisplaySettingEnabled(mInjector)) {
+            screen.addPreference(updateUseDisplayPreferenceNoDisplaysFound(context));
+        }
+        screen.addPreference(updateFooterPreference(context,
+                EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE));
+    }
+
+    private void showDisplaySettings(@NonNull Display display, @NonNull PreferenceScreen screen,
+            @NonNull Context context) {
+        final var isEnabled = mInjector != null && mInjector.isDisplayEnabled(display);
+        if (isUseDisplaySettingEnabled(mInjector)) {
+            screen.addPreference(updateUseDisplayPreference(context, display, isEnabled));
+        }
+        if (!isEnabled) {
+            // Skip all other settings
+            return;
+        }
+        final var displayRotation = getDisplayRotation(display.getDisplayId());
+        screen.addPreference(updateIllustrationImage(context, displayRotation));
+        screen.addPreference(updateResolutionPreference(context, display));
+        screen.addPreference(updateRotationPreference(context, display, displayRotation));
+        if (isResolutionSettingEnabled(mInjector)) {
+            screen.addPreference(updateFooterPreference(context,
+                    EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE));
+        }
+    }
+
+    private void showDisplaysList(@NonNull List<Display> displaysToShow,
+            @NonNull PreferenceScreen screen, @NonNull Context context) {
+        var pref = getDisplaysListPreference(context);
+        pref.setKey(DISPLAYS_LIST_PREFERENCE_KEY);
+        pref.removeAll();
+        if (!displaysToShow.isEmpty()) {
+            screen.addPreference(pref);
+        }
+        for (var display : displaysToShow) {
+            pref.addPreference(new DisplayPreference(context, display));
+        }
+    }
+
+    private List<Display> getDisplaysToShow(int displayIdToShow) {
+        if (mInjector == null) {
+            return List.of();
+        }
+        if (displayIdToShow != INVALID_DISPLAY) {
+            var display = mInjector.getDisplay(displayIdToShow);
+            if (display != null && isDisplayAllowed(display, mInjector)) {
+                return List.of(display);
+            }
+        }
+        var displaysToShow = new ArrayList<Display>();
+        for (var display : mInjector.getAllDisplays()) {
+            if (display != null && isDisplayAllowed(display, mInjector)) {
+                displaysToShow.add(display);
+            }
+        }
+        return displaysToShow;
+    }
+
+    private Preference updateUseDisplayPreferenceNoDisplaysFound(@NonNull Context context) {
+        final var pref = getUseDisplayPreference(context);
+        pref.setKey(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY);
+        pref.setTitle(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE);
+        pref.setChecked(false);
+        pref.setEnabled(false);
+        pref.setOnPreferenceChangeListener(null);
+        return pref;
+    }
+
+    private Preference updateUseDisplayPreference(@NonNull final Context context,
+            @NonNull final Display display, boolean isEnabled) {
+        final var pref = getUseDisplayPreference(context);
+        pref.setKey(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY);
+        pref.setTitle(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE);
+        pref.setChecked(isEnabled);
+        pref.setEnabled(true);
+        pref.setOnPreferenceChangeListener((p, newValue) -> {
+            writePreferenceClickMetric(p);
+            final boolean result;
+            if (mInjector == null) {
+                return false;
+            }
+            if ((Boolean) newValue) {
+                result = mInjector.enableConnectedDisplay(display.getDisplayId());
+            } else {
+                result = mInjector.disableConnectedDisplay(display.getDisplayId());
+            }
+            if (result) {
+                pref.setChecked((Boolean) newValue);
+            }
+            return result;
+        });
+        return pref;
+    }
+
+    private Preference updateIllustrationImage(@NonNull final Context context,
+            final int displayRotation) {
+        var pref = getIllustrationPreference(context);
+        if (displayRotation % 2 == 0) {
+            pref.setLottieAnimationResId(EXTERNAL_DISPLAY_PORTRAIT_DRAWABLE);
+        } else {
+            pref.setLottieAnimationResId(EXTERNAL_DISPLAY_LANDSCAPE_DRAWABLE);
+        }
+        return pref;
+    }
+
+    private Preference updateFooterPreference(@NonNull final Context context, final int title) {
+        var pref = getFooterPreference(context);
+        pref.setTitle(title);
+        return pref;
+    }
+
+    private Preference updateRotationPreference(@NonNull final Context context,
+            @NonNull final Display display, final int displayRotation) {
+        var pref = getRotationPreference(context);
+        pref.setKey(EXTERNAL_DISPLAY_ROTATION_KEY);
+        pref.setTitle(EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE);
+        if (mRotationEntries == null || mRotationEntriesValues == null) {
+            mRotationEntries = new String[] {
+                    context.getString(R.string.external_display_standard_rotation),
+                    context.getString(R.string.external_display_rotation_90),
+                    context.getString(R.string.external_display_rotation_180),
+                    context.getString(R.string.external_display_rotation_270)};
+            mRotationEntriesValues = new String[] {"0", "1", "2", "3"};
+        }
+        pref.setEntries(mRotationEntries);
+        pref.setEntryValues(mRotationEntriesValues);
+        pref.setValueIndex(displayRotation);
+        pref.setSummary(mRotationEntries[displayRotation]);
+        pref.setOnPreferenceChangeListener((p, newValue) -> {
+            writePreferenceClickMetric(p);
+            var rotation = Integer.parseInt((String) newValue);
+            var displayId = display.getDisplayId();
+            if (mInjector == null || !mInjector.freezeDisplayRotation(displayId, rotation)) {
+                return false;
+            }
+            pref.setValueIndex(rotation);
+            return true;
+        });
+        pref.setEnabled(isRotationSettingEnabled(mInjector));
+        return pref;
+    }
+
+    private Preference updateResolutionPreference(@NonNull final Context context,
+            @NonNull final Display display) {
+        var pref = getResolutionPreference(context);
+        pref.setKey(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY);
+        pref.setTitle(EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE);
+        pref.setSummary(display.getMode().getPhysicalWidth() + " x "
+                + display.getMode().getPhysicalHeight());
+        pref.setOnPreferenceClickListener((Preference p) -> {
+            writePreferenceClickMetric(p);
+            launchResolutionSelector(context, display.getDisplayId());
+            return true;
+        });
+        pref.setEnabled(isResolutionSettingEnabled(mInjector));
+        return pref;
+    }
+
+    private int getDisplayRotation(int displayId) {
+        if (mInjector == null) {
+            return 0;
+        }
+        return Math.min(3, Math.max(0, mInjector.getDisplayUserRotation(displayId)));
+    }
+
+    private void scheduleUpdate() {
+        if (mInjector == null || !mStarted) {
+            return;
+        }
+        unscheduleUpdate();
+        mInjector.getHandler().post(mUpdateRunnable);
+    }
+
+    private void unscheduleUpdate() {
+        if (mInjector == null || !mStarted) {
+            return;
+        }
+        mInjector.getHandler().removeCallbacks(mUpdateRunnable);
+    }
+
+    @VisibleForTesting
+    class DisplayPreference extends TwoTargetPreference
+            implements Preference.OnPreferenceClickListener {
+        private final int mDisplayId;
+
+        DisplayPreference(@NonNull final Context context, @NonNull final Display display) {
+            super(context);
+            mDisplayId = display.getDisplayId();
+            setPersistent(false);
+            setKey("display_id_" + mDisplayId);
+            setTitle(display.getName());
+            setSummary(display.getMode().getPhysicalWidth() + " x "
+                               + display.getMode().getPhysicalHeight());
+            setOnPreferenceClickListener(this);
+        }
+
+        @Override
+        public boolean onPreferenceClick(@NonNull Preference preference) {
+            launchDisplaySettings(mDisplayId);
+            writePreferenceClickMetric(preference);
+            return true;
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java b/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java
new file mode 100644
index 0000000..89d464c
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/display/ExternalDisplaySettingsConfiguration.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.connecteddevice.display;
+
+import static android.content.Context.DISPLAY_SERVICE;
+import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED;
+import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_ADDED;
+import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED;
+import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED;
+import static android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_REMOVED;
+import static android.view.Display.INVALID_DISPLAY;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManagerGlobal;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.view.Display;
+import android.view.Display.Mode;
+import android.view.IWindowManager;
+import android.view.WindowManagerGlobal;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.flags.FeatureFlags;
+import com.android.settings.flags.FeatureFlagsImpl;
+
+public class ExternalDisplaySettingsConfiguration {
+    static final String VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY =
+            "persist.demo.userrotation.package_name";
+    static final String DISPLAY_ID_ARG = "display_id";
+    static final int EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE = R.string.external_display_not_found;
+    static final int EXTERNAL_DISPLAY_HELP_URL = R.string.help_url_external_display;
+
+    public static class SystemServicesProvider {
+        @Nullable
+        private IWindowManager mWindowManager;
+        @Nullable
+        private DisplayManager mDisplayManager;
+        @Nullable
+        protected Context mContext;
+        /**
+         * @param name of a system property.
+         * @return the value of the system property.
+         */
+        @NonNull
+        public String getSystemProperty(@NonNull String name) {
+            return SystemProperties.get(name);
+        }
+
+        /**
+         * @return return public Display manager.
+         */
+        @Nullable
+        public DisplayManager getDisplayManager() {
+            if (mDisplayManager == null && getContext() != null) {
+                mDisplayManager = (DisplayManager) getContext().getSystemService(DISPLAY_SERVICE);
+            }
+            return mDisplayManager;
+        }
+
+        /**
+         * @return internal IWindowManager
+         */
+        @Nullable
+        public IWindowManager getWindowManager() {
+            if (mWindowManager == null) {
+                mWindowManager = WindowManagerGlobal.getWindowManagerService();
+            }
+            return mWindowManager;
+        }
+
+        /**
+         * @return context.
+         */
+        @Nullable
+        public Context getContext() {
+            return mContext;
+        }
+    }
+
+    public static class Injector extends SystemServicesProvider {
+        @NonNull
+        private final FeatureFlags mFlags;
+        @NonNull
+        private final Handler mHandler;
+
+        Injector(@Nullable Context context) {
+            this(context, new FeatureFlagsImpl(), new Handler(Looper.getMainLooper()));
+        }
+
+        Injector(@Nullable Context context, @NonNull FeatureFlags flags, @NonNull Handler handler) {
+            mContext = context;
+            mFlags = flags;
+            mHandler = handler;
+        }
+
+        /**
+         * @return all displays including disabled.
+         */
+        @NonNull
+        public Display[] getAllDisplays() {
+            var dm = getDisplayManager();
+            if (dm == null) {
+                return new Display[0];
+            }
+            return dm.getDisplays(DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED);
+        }
+
+        /**
+         * @return enabled displays only.
+         */
+        @NonNull
+        public Display[] getEnabledDisplays() {
+            var dm = getDisplayManager();
+            if (dm == null) {
+                return new Display[0];
+            }
+            return dm.getDisplays();
+        }
+
+        /**
+         * @return true if the display is enabled
+         */
+        public boolean isDisplayEnabled(@NonNull Display display) {
+            for (var enabledDisplay : getEnabledDisplays()) {
+                if (enabledDisplay.getDisplayId() == display.getDisplayId()) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Register display listener.
+         */
+        public void registerDisplayListener(@NonNull DisplayManager.DisplayListener listener) {
+            var dm = getDisplayManager();
+            if (dm == null) {
+                return;
+            }
+            dm.registerDisplayListener(listener, mHandler, EVENT_FLAG_DISPLAY_ADDED
+                    | EVENT_FLAG_DISPLAY_CHANGED | EVENT_FLAG_DISPLAY_REMOVED
+                    | EVENT_FLAG_DISPLAY_CONNECTION_CHANGED);
+        }
+
+        /**
+         * Unregister display listener.
+         */
+        public void unregisterDisplayListener(@NonNull DisplayManager.DisplayListener listener) {
+            var dm = getDisplayManager();
+            if (dm == null) {
+                return;
+            }
+            dm.unregisterDisplayListener(listener);
+        }
+
+        /**
+         * @return feature flags.
+         */
+        @NonNull
+        public FeatureFlags getFlags() {
+            return mFlags;
+        }
+
+        /**
+         * Enable connected display.
+         */
+        public boolean enableConnectedDisplay(int displayId) {
+            var dm = getDisplayManager();
+            if (dm == null) {
+                return false;
+            }
+            dm.enableConnectedDisplay(displayId);
+            return true;
+        }
+
+        /**
+         * Disable connected display.
+         */
+        public boolean disableConnectedDisplay(int displayId) {
+            var dm = getDisplayManager();
+            if (dm == null) {
+                return false;
+            }
+            dm.disableConnectedDisplay(displayId);
+            return true;
+        }
+
+        /**
+         * @param displayId which must be returned
+         * @return display object for the displayId
+         */
+        @Nullable
+        public Display getDisplay(int displayId) {
+            if (displayId == INVALID_DISPLAY) {
+                return null;
+            }
+            var dm = getDisplayManager();
+            if (dm == null) {
+                return null;
+            }
+            return dm.getDisplay(displayId);
+        }
+
+        /**
+         * @return handler
+         */
+        @NonNull
+        public Handler getHandler() {
+            return mHandler;
+        }
+
+        /**
+         * Get display rotation
+         * @param displayId display identifier
+         * @return rotation
+         */
+        public int getDisplayUserRotation(int displayId) {
+            var wm = getWindowManager();
+            if (wm == null) {
+                return 0;
+            }
+            try {
+                return wm.getDisplayUserRotation(displayId);
+            } catch (RemoteException e) {
+                return 0;
+            }
+        }
+
+        /**
+         * Freeze rotation of the display in the specified rotation.
+         * @param displayId display identifier
+         * @param rotation [0, 1, 2, 3]
+         * @return true if successful
+         */
+        public boolean freezeDisplayRotation(int displayId, int rotation) {
+            var wm = getWindowManager();
+            if (wm == null) {
+                return false;
+            }
+            try {
+                wm.freezeDisplayRotation(displayId, rotation,
+                        "ExternalDisplayPreferenceFragment");
+                return true;
+            } catch (RemoteException e) {
+                return false;
+            }
+        }
+
+        /**
+         * Enforce display mode on the given display.
+         */
+        public void setUserPreferredDisplayMode(int displayId, @NonNull Mode mode) {
+            DisplayManagerGlobal.getInstance().setUserPreferredDisplayMode(displayId, mode);
+        }
+    }
+
+    public abstract static class DisplayListener implements DisplayManager.DisplayListener {
+        @Override
+        public void onDisplayAdded(int displayId) {
+            update(displayId);
+        }
+
+        @Override
+        public void onDisplayRemoved(int displayId) {
+            update(displayId);
+        }
+
+        @Override
+        public void onDisplayChanged(int displayId) {
+            update(displayId);
+        }
+
+        @Override
+        public void onDisplayConnected(int displayId) {
+            update(displayId);
+        }
+
+        @Override
+        public void onDisplayDisconnected(int displayId) {
+            update(displayId);
+        }
+
+        /**
+         * Called from other listener methods to trigger update of the settings page.
+         */
+        public abstract void update(int displayId);
+    }
+
+    /**
+     * @return whether the settings page is enabled or not.
+     */
+    public static boolean isExternalDisplaySettingsPageEnabled(@NonNull FeatureFlags flags) {
+        return flags.rotationConnectedDisplaySetting()
+                || flags.resolutionAndEnableConnectedDisplaySetting();
+    }
+
+    static boolean isDisplayAllowed(@NonNull Display display,
+            @NonNull SystemServicesProvider props) {
+        return display.getType() == Display.TYPE_EXTERNAL
+                || display.getType() == Display.TYPE_OVERLAY
+                || isVirtualDisplayAllowed(display, props);
+    }
+
+    static boolean isVirtualDisplayAllowed(@NonNull Display display,
+            @NonNull SystemServicesProvider properties) {
+        var sysProp = properties.getSystemProperty(VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY);
+        return !sysProp.isEmpty() && display.getType() == Display.TYPE_VIRTUAL
+                       && sysProp.equals(display.getOwnerPackageName());
+    }
+
+    static boolean isUseDisplaySettingEnabled(@Nullable Injector injector) {
+        return injector != null && injector.getFlags().resolutionAndEnableConnectedDisplaySetting();
+    }
+
+    static boolean isResolutionSettingEnabled(@Nullable Injector injector) {
+        return injector != null && injector.getFlags().resolutionAndEnableConnectedDisplaySetting();
+    }
+
+    static boolean isRotationSettingEnabled(@Nullable Injector injector) {
+        return injector != null && injector.getFlags().rotationConnectedDisplaySetting();
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdater.java b/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdater.java
new file mode 100644
index 0000000..64dd7bb
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdater.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.display;
+
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener;
+import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.RestrictedLockUtilsInternal;
+import com.android.settingslib.RestrictedPreference;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+
+public class ExternalDisplayUpdater {
+
+    private static final String PREF_KEY = "external_display_settings";
+    private final int mMetricsCategory;
+    @NonNull
+    private final MetricsFeatureProvider mMetricsFeatureProvider;
+    @NonNull
+    private final Runnable mUpdateRunnable = this::update;
+    @NonNull
+    private final DevicePreferenceCallback mDevicePreferenceCallback;
+    @Nullable
+    private RestrictedPreference mPreference;
+    @Nullable
+    private Injector mInjector;
+    private final DisplayListener mListener =  new DisplayListener() {
+        @Override
+        public void update(int displayId) {
+            scheduleUpdate();
+        }
+    };
+
+    public ExternalDisplayUpdater(@NonNull DevicePreferenceCallback callback, int metricsCategory) {
+        mDevicePreferenceCallback = callback;
+        mMetricsCategory = metricsCategory;
+        mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
+    }
+
+    /**
+     * Set the context to generate the {@link Preference}, so it could get the correct theme.
+     */
+    public void initPreference(@NonNull Context context) {
+        initPreference(context, new Injector(context));
+    }
+
+    @VisibleForTesting
+    void initPreference(@NonNull Context context, Injector injector) {
+        mInjector = injector;
+        mPreference = new RestrictedPreference(context, null /* AttributeSet */);
+        mPreference.setTitle(R.string.external_display_settings_title);
+        mPreference.setSummary(getSummary());
+        mPreference.setIcon(getDrawable(context));
+        mPreference.setKey(PREF_KEY);
+        mPreference.setDisabledByAdmin(checkIfUsbDataSignalingIsDisabled(context));
+        mPreference.setOnPreferenceClickListener((Preference p) -> {
+            mMetricsFeatureProvider.logClickedPreference(p, mMetricsCategory);
+            // New version - uses a separate screen.
+            new SubSettingLauncher(context)
+                    .setDestination(ExternalDisplayPreferenceFragment.class.getName())
+                    .setTitleRes(R.string.external_display_settings_title)
+                    .setSourceMetricsCategory(mMetricsCategory)
+                    .launch();
+            return true;
+        });
+
+        scheduleUpdate();
+    }
+
+    /**
+     * Unregister the display listener.
+     */
+    public void unregisterCallback() {
+        if (mInjector != null) {
+            mInjector.unregisterDisplayListener(mListener);
+        }
+    }
+
+    /**
+     * Register the display listener.
+     */
+    public void registerCallback() {
+        if (mInjector != null) {
+            mInjector.registerDisplayListener(mListener);
+        }
+    }
+
+    @VisibleForTesting
+    @Nullable
+    protected RestrictedLockUtils.EnforcedAdmin checkIfUsbDataSignalingIsDisabled(Context context) {
+        return RestrictedLockUtilsInternal.checkIfUsbDataSignalingIsDisabled(context,
+                    UserHandle.myUserId());
+    }
+
+    @VisibleForTesting
+    @Nullable
+    protected Drawable getDrawable(Context context) {
+        return context.getDrawable(R.drawable.ic_external_display_32dp);
+    }
+
+    @Nullable
+    protected CharSequence getSummary() {
+        if (mInjector == null) {
+            return null;
+        }
+        var context = mInjector.getContext();
+        if (context == null) {
+            return null;
+        }
+
+        for (var display : mInjector.getEnabledDisplays()) {
+            if (display != null && isDisplayAllowed(display, mInjector)) {
+                return context.getString(R.string.external_display_on);
+            }
+        }
+
+        for (var display : mInjector.getAllDisplays()) {
+            if (display != null && isDisplayAllowed(display, mInjector)) {
+                return context.getString(R.string.external_display_off);
+            }
+        }
+
+        return null;
+    }
+
+    private void scheduleUpdate() {
+        if (mInjector == null) {
+            return;
+        }
+        unscheduleUpdate();
+        mInjector.getHandler().post(mUpdateRunnable);
+    }
+
+    private void unscheduleUpdate() {
+        if (mInjector == null) {
+            return;
+        }
+        mInjector.getHandler().removeCallbacks(mUpdateRunnable);
+    }
+
+    private void update() {
+        var summary = getSummary();
+        if (mPreference == null) {
+            return;
+        }
+        mPreference.setSummary(summary);
+        if (summary != null) {
+            mDevicePreferenceCallback.onDeviceAdded(mPreference);
+        } else {
+            mDevicePreferenceCallback.onDeviceRemoved(mPreference);
+        }
+    }
+}
diff --git a/src/com/android/settings/connecteddevice/display/OWNERS b/src/com/android/settings/connecteddevice/display/OWNERS
new file mode 100644
index 0000000..78aecb9
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/display/OWNERS
@@ -0,0 +1,7 @@
+# Default reviewers for this and subdirectories.
+santoscordon@google.com
+petsjonkin@google.com
+flc@google.com
+wilczynskip@google.com
+brup@google.com
+olb@google.com
diff --git a/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragment.java b/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragment.java
new file mode 100644
index 0000000..10314cb
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragment.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.display;
+
+import static android.view.Display.INVALID_DISPLAY;
+
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DISPLAY_ID_ARG;
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_HELP_URL;
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE;
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.isDisplayAllowed;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.Pair;
+import android.view.Display;
+import android.view.Display.Mode;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceScreen;
+
+import com.android.internal.util.ToBooleanFunction;
+import com.android.settings.R;
+import com.android.settings.SettingsPreferenceFragmentBase;
+import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener;
+import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.Injector;
+import com.android.settingslib.widget.SelectorWithWidgetPreference;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+public class ResolutionPreferenceFragment extends SettingsPreferenceFragmentBase {
+    private static final String TAG = "ResolutionPreferenceFragment";
+    static final int DEFAULT_LOW_REFRESH_RATE = 60;
+    static final String MORE_OPTIONS_KEY = "more_options";
+    static final String TOP_OPTIONS_KEY = "top_options";
+    static final int MORE_OPTIONS_TITLE_RESOURCE =
+            R.string.external_display_more_options_title;
+    static final int EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE =
+            R.xml.external_display_resolution_settings;
+    @Nullable
+    private Injector mInjector;
+    @Nullable
+    private PreferenceCategory mTopOptionsPreference;
+    @Nullable
+    private PreferenceCategory mMoreOptionsPreference;
+    private boolean mStarted;
+    private final HashSet<String> mResolutionPreferences = new HashSet<>();
+    private int mExternalDisplayPeakWidth;
+    private int mExternalDisplayPeakHeight;
+    private int mExternalDisplayPeakRefreshRate;
+    private boolean mRefreshRateSynchronizationEnabled;
+    private boolean mMoreOptionsExpanded;
+    private final Runnable mUpdateRunnable = this::update;
+    private final DisplayListener mListener = new DisplayListener() {
+        @Override
+        public void update(int displayId) {
+            scheduleUpdate();
+        }
+    };
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.SETTINGS_CONNECTED_DEVICE_CATEGORY;
+    }
+
+    @Override
+    public int getHelpResource() {
+        return EXTERNAL_DISPLAY_HELP_URL;
+    }
+
+    @Override
+    public void onCreateCallback(@Nullable Bundle icicle) {
+        if (mInjector == null) {
+            mInjector = new Injector(getPrefContext());
+        }
+        addPreferencesFromResource(EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE);
+        updateDisplayModeLimits(mInjector.getContext());
+    }
+
+    @Override
+    public void onActivityCreatedCallback(@Nullable Bundle savedInstanceState) {
+        View view = getView();
+        TextView emptyView = null;
+        if (view != null) {
+            emptyView = (TextView) view.findViewById(android.R.id.empty);
+        }
+        if (emptyView != null) {
+            emptyView.setText(EXTERNAL_DISPLAY_NOT_FOUND_RESOURCE);
+            setEmptyView(emptyView);
+        }
+    }
+
+    @Override
+    public void onStartCallback() {
+        mStarted = true;
+        if (mInjector == null) {
+            return;
+        }
+        mInjector.registerDisplayListener(mListener);
+        scheduleUpdate();
+    }
+
+    @Override
+    public void onStopCallback() {
+        mStarted = false;
+        if (mInjector == null) {
+            return;
+        }
+        mInjector.unregisterDisplayListener(mListener);
+        unscheduleUpdate();
+    }
+
+    public ResolutionPreferenceFragment() {}
+
+    @VisibleForTesting
+    ResolutionPreferenceFragment(@NonNull Injector injector) {
+        mInjector = injector;
+    }
+
+    @VisibleForTesting
+    protected int getDisplayIdArg() {
+        var args = getArguments();
+        return args != null ? args.getInt(DISPLAY_ID_ARG, INVALID_DISPLAY) : INVALID_DISPLAY;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    protected Resources getResources(@NonNull Context context) {
+        return context.getResources();
+    }
+
+    private void update() {
+        final PreferenceScreen screen = getPreferenceScreen();
+        if (screen == null || mInjector == null) {
+            return;
+        }
+        var context = mInjector.getContext();
+        if (context == null) {
+            return;
+        }
+        var display = mInjector.getDisplay(getDisplayIdArg());
+        if (display == null || !isDisplayAllowed(display, mInjector)) {
+            screen.removeAll();
+            mTopOptionsPreference = null;
+            mMoreOptionsPreference = null;
+            return;
+        }
+        mResolutionPreferences.clear();
+        var remainingModes = addModePreferences(context,
+                getTopPreference(context, screen),
+                display.getSupportedModes(), this::isTopMode, display);
+        addRemainingPreferences(context,
+                getMorePreference(context, screen),
+                display, remainingModes.first, remainingModes.second);
+    }
+
+    private PreferenceCategory getTopPreference(@NonNull Context context,
+            @NonNull PreferenceScreen screen) {
+        if (mTopOptionsPreference == null) {
+            mTopOptionsPreference = new PreferenceCategory(context);
+            mTopOptionsPreference.setPersistent(false);
+            mTopOptionsPreference.setKey(TOP_OPTIONS_KEY);
+            screen.addPreference(mTopOptionsPreference);
+        } else {
+            mTopOptionsPreference.removeAll();
+        }
+        return mTopOptionsPreference;
+    }
+
+    private PreferenceCategory getMorePreference(@NonNull Context context,
+            @NonNull PreferenceScreen screen) {
+        if (mMoreOptionsPreference == null) {
+            mMoreOptionsPreference = new PreferenceCategory(context);
+            mMoreOptionsPreference.setPersistent(false);
+            mMoreOptionsPreference.setTitle(MORE_OPTIONS_TITLE_RESOURCE);
+            mMoreOptionsPreference.setOnExpandButtonClickListener(() -> {
+                mMoreOptionsExpanded = true;
+            });
+            mMoreOptionsPreference.setKey(MORE_OPTIONS_KEY);
+            screen.addPreference(mMoreOptionsPreference);
+        } else {
+            mMoreOptionsPreference.removeAll();
+        }
+        return mMoreOptionsPreference;
+    }
+
+    private void addRemainingPreferences(@NonNull Context context,
+            @NonNull PreferenceCategory group, @NonNull Display display,
+            boolean isSelectedModeFound, @NonNull Mode[] moreModes) {
+        if (moreModes.length == 0) {
+            return;
+        }
+        mMoreOptionsExpanded |= !isSelectedModeFound;
+        group.setInitialExpandedChildrenCount(mMoreOptionsExpanded ? Integer.MAX_VALUE : 0);
+        addModePreferences(context, group, moreModes, /*checkMode=*/ null, display);
+    }
+
+    private Pair<Boolean, Mode[]> addModePreferences(@NonNull Context context,
+            @NonNull PreferenceGroup group,
+            @NonNull Mode[] modes,
+            @Nullable ToBooleanFunction<Mode> checkMode,
+            @NonNull Display display) {
+        Display.Mode curMode = display.getMode();
+        var currentResolution = curMode.getPhysicalWidth() + "x" + curMode.getPhysicalHeight();
+        var rotatedResolution = curMode.getPhysicalHeight() + "x" + curMode.getPhysicalWidth();
+        var skippedModes = new ArrayList<Mode>();
+        var isAnyOfModesSelected = false;
+        for (var mode : modes) {
+            var modeStr = mode.getPhysicalWidth() + "x" + mode.getPhysicalHeight();
+            SelectorWithWidgetPreference pref = group.findPreference(modeStr);
+            if (pref != null) {
+                continue;
+            }
+            if (checkMode != null && !checkMode.apply(mode)) {
+                skippedModes.add(mode);
+                continue;
+            }
+            var isCurrentMode =
+                    currentResolution.equals(modeStr) || rotatedResolution.equals(modeStr);
+            if (!isCurrentMode && !isAllowedMode(mode)) {
+                continue;
+            }
+            if (mResolutionPreferences.contains(modeStr)) {
+                // Added to "Top modes" already.
+                continue;
+            }
+            mResolutionPreferences.add(modeStr);
+            pref = new SelectorWithWidgetPreference(context);
+            pref.setPersistent(false);
+            pref.setKey(modeStr);
+            pref.setTitle(mode.getPhysicalWidth() + " x " + mode.getPhysicalHeight());
+            pref.setSingleLineTitle(true);
+            pref.setOnClickListener(preference -> onDisplayModeClicked(preference, display));
+            pref.setChecked(isCurrentMode);
+            isAnyOfModesSelected |= isCurrentMode;
+            group.addPreference(pref);
+        }
+        return new Pair<>(isAnyOfModesSelected, skippedModes.toArray(Mode.EMPTY_ARRAY));
+    }
+
+    private boolean isTopMode(@NonNull Mode mode) {
+        return mTopOptionsPreference != null
+                && mTopOptionsPreference.getPreferenceCount() < 3;
+    }
+
+    private boolean isAllowedMode(@NonNull Mode mode) {
+        if (mRefreshRateSynchronizationEnabled
+                && (mode.getRefreshRate() < DEFAULT_LOW_REFRESH_RATE - 1
+                        || mode.getRefreshRate() > DEFAULT_LOW_REFRESH_RATE + 1)) {
+            Log.d(TAG, mode + " refresh rate is out of synchronization range");
+            return false;
+        }
+        if (mExternalDisplayPeakHeight > 0
+                && mode.getPhysicalHeight() > mExternalDisplayPeakHeight) {
+            Log.d(TAG, mode + " height is above the allowed limit");
+            return false;
+        }
+        if (mExternalDisplayPeakWidth > 0
+                && mode.getPhysicalWidth() > mExternalDisplayPeakWidth) {
+            Log.d(TAG, mode + " width is above the allowed limit");
+            return false;
+        }
+        if (mExternalDisplayPeakRefreshRate > 0
+                && mode.getRefreshRate() > mExternalDisplayPeakRefreshRate) {
+            Log.d(TAG, mode + " refresh rate is above the allowed limit");
+            return false;
+        }
+        return true;
+    }
+
+    private void scheduleUpdate() {
+        if (mInjector == null || !mStarted) {
+            return;
+        }
+        unscheduleUpdate();
+        mInjector.getHandler().post(mUpdateRunnable);
+    }
+
+    private void unscheduleUpdate() {
+        if (mInjector == null || !mStarted) {
+            return;
+        }
+        mInjector.getHandler().removeCallbacks(mUpdateRunnable);
+    }
+
+    private void onDisplayModeClicked(@NonNull SelectorWithWidgetPreference preference,
+            @NonNull Display display) {
+        if (mInjector == null) {
+            return;
+        }
+        String[] modeResolution = preference.getKey().split("x");
+        int width = Integer.parseInt(modeResolution[0]);
+        int height = Integer.parseInt(modeResolution[1]);
+        for (var mode : display.getSupportedModes()) {
+            if (mode.getPhysicalWidth() == width && mode.getPhysicalHeight() == height
+                        && isAllowedMode(mode)) {
+                mInjector.setUserPreferredDisplayMode(display.getDisplayId(), mode);
+                return;
+            }
+        }
+    }
+
+    private void updateDisplayModeLimits(@Nullable Context context) {
+        if (context == null) {
+            return;
+        }
+        mExternalDisplayPeakRefreshRate = getResources(context).getInteger(
+                    com.android.internal.R.integer.config_externalDisplayPeakRefreshRate);
+        mExternalDisplayPeakWidth = getResources(context).getInteger(
+                    com.android.internal.R.integer.config_externalDisplayPeakWidth);
+        mExternalDisplayPeakHeight = getResources(context).getInteger(
+                    com.android.internal.R.integer.config_externalDisplayPeakHeight);
+        mRefreshRateSynchronizationEnabled = getResources(context).getBoolean(
+                    com.android.internal.R.bool.config_refreshRateSynchronizationEnabled);
+        Log.d(TAG, "mExternalDisplayPeakRefreshRate=" + mExternalDisplayPeakRefreshRate);
+        Log.d(TAG, "mExternalDisplayPeakWidth=" + mExternalDisplayPeakWidth);
+        Log.d(TAG, "mExternalDisplayPeakHeight=" + mExternalDisplayPeakHeight);
+        Log.d(TAG, "mRefreshRateSynchronizationEnabled=" + mRefreshRateSynchronizationEnabled);
+    }
+}
diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java
index 1c14712..11c05f3 100644
--- a/src/com/android/settings/core/gateway/SettingsGateway.java
+++ b/src/com/android/settings/core/gateway/SettingsGateway.java
@@ -162,6 +162,8 @@
 import com.android.settings.notification.app.ChannelNotificationSettings;
 import com.android.settings.notification.app.ConversationListSettings;
 import com.android.settings.notification.history.NotificationStation;
+import com.android.settings.notification.modes.ZenModeFragment;
+import com.android.settings.notification.modes.ZenModesListFragment;
 import com.android.settings.notification.zen.ZenAccessSettings;
 import com.android.settings.notification.zen.ZenModeAutomationSettings;
 import com.android.settings.notification.zen.ZenModeBlockedEffectsSettings;
@@ -396,6 +398,8 @@
             CellularSecuritySettingsFragment.class.getName(),
             AccessibilityHearingAidsFragment.class.getName(),
             HearingDevicePairingFragment.class.getName(),
+            ZenModesListFragment.class.getName(),
+            ZenModeFragment.class.getName()
     };
 
     public static final String[] SETTINGS_FOR_RESTRICTED = {
diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java
index 7b5cd9f..b5d5099 100644
--- a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java
+++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java
@@ -99,7 +99,8 @@
         return mScreenOnTime;
     }
 
-    List<BatteryDiffEntry> getAppDiffEntryList() {
+    /** Gets the {@link BatteryDiffEntry} list for apps. */
+    public List<BatteryDiffEntry> getAppDiffEntryList() {
         return mAppEntries;
     }
 
@@ -298,8 +299,7 @@
      * Sets total consume power, and adjusts the percentages to ensure the total round percentage
      * could be 100%, and then sorts entries based on the sorting key.
      */
-    @VisibleForTesting
-    static void processAndSortEntries(final List<BatteryDiffEntry> batteryDiffEntries) {
+    public static void processAndSortEntries(final List<BatteryDiffEntry> batteryDiffEntries) {
         if (batteryDiffEntries.isEmpty()) {
             return;
         }
diff --git a/src/com/android/settings/homepage/contextualcards/conditional/DndConditionCardController.java b/src/com/android/settings/homepage/contextualcards/conditional/DndConditionCardController.java
index e69a336..6362068 100644
--- a/src/com/android/settings/homepage/contextualcards/conditional/DndConditionCardController.java
+++ b/src/com/android/settings/homepage/contextualcards/conditional/DndConditionCardController.java
@@ -105,7 +105,8 @@
                         + mAppContext.getText(R.string.condition_zen_title))
                 .setTitleText(mAppContext.getText(R.string.condition_zen_title).toString())
                 .setSummaryText(getSummary())
-                .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_do_not_disturb_on_24dp))
+                .setIconDrawable(mAppContext.getDrawable(
+                        com.android.settingslib.R.drawable.ic_do_not_disturb_on_24dp))
                 .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH)
                 .build();
     }
diff --git a/src/com/android/settings/inputmethod/PointerScaleSeekBarController.java b/src/com/android/settings/inputmethod/PointerScaleSeekBarController.java
new file mode 100644
index 0000000..06d5203
--- /dev/null
+++ b/src/com/android/settings/inputmethod/PointerScaleSeekBarController.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.inputmethod;
+
+import static android.view.PointerIcon.DEFAULT_POINTER_SCALE;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.widget.SeekBar;
+
+import androidx.annotation.NonNull;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.widget.LabeledSeekBarPreference;
+
+public class PointerScaleSeekBarController extends BasePreferenceController {
+
+    private final int mProgressMin;
+    private final int mProgressMax;
+    private final float mScaleMin;
+    private final float mScaleMax;
+
+    public PointerScaleSeekBarController(@NonNull Context context, @NonNull String key) {
+        super(context, key);
+
+        Resources res =  context.getResources();
+        mProgressMin = res.getInteger(R.integer.pointer_scale_seek_bar_start);
+        mProgressMax = res.getInteger(R.integer.pointer_scale_seek_bar_end);
+        mScaleMin = res.getFloat(R.dimen.pointer_scale_size_start);
+        mScaleMax = res.getFloat(R.dimen.pointer_scale_size_end);
+    }
+
+    @AvailabilityStatus
+    public int getAvailabilityStatus() {
+        return android.view.flags.Flags.enableVectorCursorA11ySettings() ? AVAILABLE
+                : CONDITIONALLY_UNAVAILABLE;
+    }
+
+    @Override
+    public void displayPreference(@NonNull PreferenceScreen screen) {
+        super.displayPreference(screen);
+
+        LabeledSeekBarPreference seekBarPreference = screen.findPreference(getPreferenceKey());
+        seekBarPreference.setMax(mProgressMax);
+        seekBarPreference.setContinuousUpdates(/* continuousUpdates= */ true);
+        seekBarPreference.setProgress(scaleToProgress(
+                Settings.System.getFloatForUser(mContext.getContentResolver(),
+                        Settings.System.POINTER_SCALE, DEFAULT_POINTER_SCALE,
+                        UserHandle.USER_CURRENT)));
+        seekBarPreference.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(@NonNull SeekBar seekBar, int progress,
+                    boolean fromUser) {
+                Settings.System.putFloatForUser(mContext.getContentResolver(),
+                        Settings.System.POINTER_SCALE, progressToScale(progress),
+                        UserHandle.USER_CURRENT);
+            }
+
+            @Override
+            public void onStartTrackingTouch(@NonNull SeekBar seekBar) {}
+
+            @Override
+            public void onStopTrackingTouch(@NonNull SeekBar seekBar) {}
+        });
+    }
+
+    private float progressToScale(int progress) {
+        return (((progress - mProgressMin) * (mScaleMax - mScaleMin)) / (mProgressMax
+                - mProgressMin)) + mScaleMin;
+    }
+
+    private int scaleToProgress(float scale) {
+        return (int) (
+                (((scale - mScaleMin) * (mProgressMax - mProgressMin)) / (mScaleMax - mScaleMin))
+                        + mProgressMin);
+    }
+}
diff --git a/src/com/android/settings/network/MobileNetworkRepository.java b/src/com/android/settings/network/MobileNetworkRepository.java
index 8ee5389..ebb341e 100644
--- a/src/com/android/settings/network/MobileNetworkRepository.java
+++ b/src/com/android/settings/network/MobileNetworkRepository.java
@@ -49,7 +49,6 @@
 import com.android.settingslib.mobile.dataservice.MobileNetworkInfoEntity;
 import com.android.settingslib.mobile.dataservice.SubscriptionInfoDao;
 import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity;
-import com.android.settingslib.mobile.dataservice.UiccInfoDao;
 import com.android.settingslib.mobile.dataservice.UiccInfoEntity;
 
 import java.util.ArrayList;
@@ -81,15 +80,11 @@
     private SubscriptionManager mSubscriptionManager;
     private MobileNetworkDatabase mMobileNetworkDatabase;
     private SubscriptionInfoDao mSubscriptionInfoDao;
-    private UiccInfoDao mUiccInfoDao;
     private MobileNetworkInfoDao mMobileNetworkInfoDao;
     private List<SubscriptionInfoEntity> mAvailableSubInfoEntityList = new ArrayList<>();
     private List<SubscriptionInfoEntity> mActiveSubInfoEntityList = new ArrayList<>();
-    private List<UiccInfoEntity> mUiccInfoEntityList = new ArrayList<>();
-    private List<MobileNetworkInfoEntity> mMobileNetworkInfoEntityList = new ArrayList<>();
     private Context mContext;
     private AirplaneModeObserver mAirplaneModeObserver;
-    private DataRoamingObserver mDataRoamingObserver;
     private MetricsFeatureProvider mMetricsFeatureProvider;
     private int mPhysicalSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
     private int mLogicalSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
@@ -124,10 +119,8 @@
         mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_MOBILE_NETWORK_DB_CREATED);
         mSubscriptionManager = context.getSystemService(SubscriptionManager.class);
         mSubscriptionInfoDao = mMobileNetworkDatabase.mSubscriptionInfoDao();
-        mUiccInfoDao = mMobileNetworkDatabase.mUiccInfoDao();
         mMobileNetworkInfoDao = mMobileNetworkDatabase.mMobileNetworkInfoDao();
         mAirplaneModeObserver = new AirplaneModeObserver(new Handler(Looper.getMainLooper()));
-        mDataRoamingObserver = new DataRoamingObserver(new Handler(Looper.getMainLooper()));
     }
 
     private class AirplaneModeObserver extends ContentObserver {
@@ -158,47 +151,6 @@
         }
     }
 
-    private class DataRoamingObserver extends ContentObserver {
-        private int mRegSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
-        private String mBaseField = Settings.Global.DATA_ROAMING;
-
-        DataRoamingObserver(Handler handler) {
-            super(handler);
-        }
-
-        public void register(Context context, int subId) {
-            mRegSubId = subId;
-            String lastField = mBaseField;
-            createTelephonyManagerBySubId(subId);
-            TelephonyManager tm = mTelephonyManagerMap.get(subId);
-            if (tm.getSimCount() != 1) {
-                lastField += subId;
-            }
-            context.getContentResolver().registerContentObserver(
-                    Settings.Global.getUriFor(lastField), false, this);
-        }
-
-        public void unRegister(Context context) {
-            context.getContentResolver().unregisterContentObserver(this);
-        }
-
-        @Override
-        public void onChange(boolean selfChange, Uri uri) {
-            TelephonyManager tm = mTelephonyManagerMap.get(mRegSubId);
-            if (tm == null) {
-                return;
-            }
-            sExecutor.execute(() -> {
-                Log.d(TAG, "DataRoamingObserver changed");
-                insertMobileNetworkInfo(mContext, mRegSubId, tm);
-            });
-            boolean isDataRoamingEnabled = tm.isDataRoamingEnabled();
-            for (MobileNetworkCallback callback : sCallbacks) {
-                callback.onDataRoamingChanged(mRegSubId, isDataRoamingEnabled);
-            }
-        }
-    }
-
     /**
      * Register all callbacks and listener.
      *
@@ -224,7 +176,6 @@
         observeAllMobileNetworkInfo(lifecycleOwner);
         if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
             createTelephonyManagerBySubId(subId);
-            mDataRoamingObserver.register(mContext, subId);
         }
         // When one client registers callback first time, convey the cached results to the client
         // so that the client is aware of the content therein.
@@ -288,7 +239,6 @@
         if (sCallbacks.isEmpty()) {
             mSubscriptionManager.removeOnSubscriptionsChangedListener(this);
             mAirplaneModeObserver.unRegister(mContext);
-            mDataRoamingObserver.unRegister(mContext);
 
             mTelephonyManagerMap.forEach((id, manager) -> {
                 TelephonyCallback callback = mTelephonyCallbackMap.get(id);
@@ -338,22 +288,6 @@
                 lifecycleOwner, this::onAllMobileNetworkInfoChanged);
     }
 
-    public List<SubscriptionInfoEntity> getAvailableSubInfoEntityList() {
-        return mAvailableSubInfoEntityList;
-    }
-
-    public List<SubscriptionInfoEntity> getActiveSubscriptionInfoList() {
-        return mActiveSubInfoEntityList;
-    }
-
-    public List<UiccInfoEntity> getUiccInfoEntityList() {
-        return mUiccInfoEntityList;
-    }
-
-    public List<MobileNetworkInfoEntity> getMobileNetworkInfoEntityList() {
-        return mMobileNetworkInfoEntityList;
-    }
-
     public SubscriptionInfoEntity getSubInfoById(String subId) {
         return mSubscriptionInfoDao.querySubInfoById(subId);
     }
@@ -464,7 +398,6 @@
     }
 
     private void onAllUiccInfoChanged(List<UiccInfoEntity> uiccInfoEntityList) {
-        mUiccInfoEntityList = new ArrayList<>(uiccInfoEntityList);
         for (MobileNetworkCallback callback : sCallbacks) {
             callback.onAllUiccInfoChanged(uiccInfoEntityList);
         }
@@ -474,7 +407,6 @@
 
     private void onAllMobileNetworkInfoChanged(
             List<MobileNetworkInfoEntity> mobileNetworkInfoEntityList) {
-        mMobileNetworkInfoEntityList = new ArrayList<>(mobileNetworkInfoEntityList);
         for (MobileNetworkCallback callback : sCallbacks) {
             callback.onAllMobileNetworkInfoChanged(mobileNetworkInfoEntityList);
         }
@@ -515,8 +447,6 @@
         mMobileNetworkDatabase.deleteSubInfoBySubId(subId);
         mMobileNetworkDatabase.deleteUiccInfoBySubId(subId);
         mMobileNetworkDatabase.deleteMobileNetworkInfoBySubId(subId);
-        mUiccInfoEntityList.removeIf(info -> info.subId.equals(subId));
-        mMobileNetworkInfoEntityList.removeIf(info -> info.subId.equals(subId));
         int id = Integer.parseInt(subId);
         removerRegisterBySubId(id);
         mSubscriptionInfoMap.remove(id);
@@ -613,10 +543,8 @@
     private MobileNetworkInfoEntity convertToMobileNetworkInfoEntity(Context context, int subId,
             TelephonyManager telephonyManager) {
         boolean isDataEnabled = false;
-        boolean isDataRoamingEnabled = false;
         if (telephonyManager != null) {
             isDataEnabled = telephonyManager.isDataEnabled();
-            isDataRoamingEnabled = telephonyManager.isDataRoamingEnabled();
         } else {
             Log.d(TAG, "TelephonyManager is null, subId = " + subId);
         }
@@ -632,7 +560,7 @@
                 MobileNetworkUtils.isTdscdmaSupported(context, subId),
                 MobileNetworkUtils.activeNetworkIsCellular(context),
                 SubscriptionUtil.showToggleForPhysicalSim(mSubscriptionManager),
-                isDataRoamingEnabled
+                /* deprecated isDataRoamingEnabled = */ false
         );
     }
 
@@ -741,7 +669,6 @@
     }
 
     private class PhoneCallStateTelephonyCallback extends TelephonyCallback implements
-            TelephonyCallback.CallStateListener,
             TelephonyCallback.UserMobileDataStateListener {
 
         private int mSubId;
@@ -751,13 +678,6 @@
         }
 
         @Override
-        public void onCallStateChanged(int state) {
-            for (MobileNetworkCallback callback : sCallbacks) {
-                callback.onCallStateChanged(state);
-            }
-        }
-
-        @Override
         public void onUserMobileDataStateChanged(boolean enabled) {
             Log.d(TAG, "onUserMobileDataStateChanged enabled " + enabled + " on SUB " + mSubId);
             sExecutor.execute(() -> {
@@ -787,15 +707,6 @@
 
         default void onAirplaneModeChanged(boolean enabled) {
         }
-
-        /**
-         * Notify clients data roaming changed of subscription.
-         */
-        default void onDataRoamingChanged(int subId, boolean enabled) {
-        }
-
-        default void onCallStateChanged(int state) {
-        }
     }
 
     public void dump(IndentingPrintWriter printwriter) {
@@ -803,8 +714,6 @@
         printwriter.increaseIndent();
         printwriter.println(" availableSubInfoEntityList= " + mAvailableSubInfoEntityList);
         printwriter.println(" activeSubInfoEntityList=" + mActiveSubInfoEntityList);
-        printwriter.println(" mobileNetworkInfoEntityList= " + mMobileNetworkInfoEntityList);
-        printwriter.println(" uiccInfoEntityList= " + mUiccInfoEntityList);
         printwriter.println(" CacheSubscriptionInfoEntityMap= " + sCacheSubscriptionInfoEntityMap);
         printwriter.println(" SubscriptionInfoMap= " + mSubscriptionInfoMap);
         printwriter.flush();
diff --git a/src/com/android/settings/network/apn/ApnPreference.java b/src/com/android/settings/network/apn/ApnPreference.java
index 879fcb6..55258c1 100644
--- a/src/com/android/settings/network/apn/ApnPreference.java
+++ b/src/com/android/settings/network/apn/ApnPreference.java
@@ -85,10 +85,11 @@
         final RelativeLayout textArea = (RelativeLayout) view.findViewById(R.id.text_layout);
         textArea.setOnClickListener(this);
 
+        final View radioButtonFrame = view.itemView.requireViewById(R.id.apn_radio_button_frame);
         final RadioButton rb = view.itemView.requireViewById(R.id.apn_radiobutton);
         mRadioButton = rb;
         if (mDefaultSelectable) {
-            view.itemView.requireViewById(R.id.apn_radio_button_frame).setOnClickListener((v) -> {
+            radioButtonFrame.setOnClickListener((v) -> {
                 rb.performClick();
             });
             rb.setOnCheckedChangeListener(this);
@@ -96,9 +97,9 @@
             mProtectFromCheckedChange = true;
             rb.setChecked(mIsChecked);
             mProtectFromCheckedChange = false;
-            rb.setVisibility(View.VISIBLE);
+            radioButtonFrame.setVisibility(View.VISIBLE);
         } else {
-            rb.setVisibility(View.GONE);
+            radioButtonFrame.setVisibility(View.GONE);
         }
     }
 
diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java
index 34d2fbd..d70ef25 100644
--- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java
+++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java
@@ -20,11 +20,14 @@
 
 import android.app.Activity;
 import android.app.settings.SettingsEnums;
+import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.Bundle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
@@ -106,6 +109,15 @@
     private SubscriptionInfoEntity mSubscriptionInfoEntity;
     private MobileNetworkInfoEntity mMobileNetworkInfoEntity;
 
+    private BroadcastReceiver mBrocastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
+                redrawPreferenceControllers();
+            }
+        }
+    };
+
     public MobileNetworkSettings() {
         super(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS);
     }
@@ -351,6 +363,10 @@
         mMobileNetworkRepository.updateEntity();
         // TODO: remove log after fixing b/182326102
         Log.d(LOG_TAG, "onResume() subId=" + mSubId);
+
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
+        getContext().registerReceiver(mBrocastReceiver, intentFilter, Context.RECEIVER_EXPORTED);
     }
 
     private void onSubscriptionDetailChanged() {
@@ -370,6 +386,7 @@
     @Override
     public void onPause() {
         mMobileNetworkRepository.removeRegister(this);
+        getContext().unregisterReceiver(mBrocastReceiver);
         super.onPause();
     }
 
diff --git a/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt b/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt
index a5d4ba8..b5cdeda 100644
--- a/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt
+++ b/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt
@@ -21,14 +21,16 @@
 import android.telephony.CarrierConfigManager
 import android.telephony.CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL
 import android.telephony.SubscriptionManager
-import android.telephony.TelephonyManager
 import android.telephony.ims.ImsMmTelManager.WiFiCallingMode
 import android.telephony.ims.feature.MmTelFeature
 import android.telephony.ims.stub.ImsRegistrationImplBase
+import androidx.lifecycle.LifecycleOwner
 import com.android.settings.network.telephony.ims.ImsMmTelRepository
 import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl
 import com.android.settings.network.telephony.ims.imsFeatureProvisionedFlow
 import com.android.settings.network.telephony.subscriptionsChangedFlow
+import com.android.settings.network.telephony.telephonyManager
+import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
@@ -38,13 +40,19 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.withContext
 
-class WifiCallingRepository(
+interface IWifiCallingRepository {
+    /** TODO: Move this to UI layer, when UI layer migrated to Kotlin. */
+    fun collectIsWifiCallingReadyFlow(lifecycleOwner: LifecycleOwner, action: (Boolean) -> Unit)
+}
+
+class WifiCallingRepository
+@JvmOverloads
+constructor(
     private val context: Context,
     private val subId: Int,
     private val imsMmTelRepository: ImsMmTelRepository = ImsMmTelRepositoryImpl(context, subId)
-) {
-    private val telephonyManager = context.getSystemService(TelephonyManager::class.java)!!
-        .createForSubscriptionId(subId)
+) : IWifiCallingRepository {
+    private val telephonyManager = context.telephonyManager(subId)
 
     private val carrierConfigManager = context.getSystemService(CarrierConfigManager::class.java)!!
 
@@ -59,6 +67,14 @@
             .getConfigForSubId(subId, KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL)
             .getBoolean(KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL)
 
+    /** TODO: Move this to UI layer, when UI layer migrated to Kotlin. */
+    override fun collectIsWifiCallingReadyFlow(
+        lifecycleOwner: LifecycleOwner,
+        action: (Boolean) -> Unit,
+    ) {
+        wifiCallingReadyFlow().collectLatestWithLifecycle(lifecycleOwner, action = action)
+    }
+
     @OptIn(ExperimentalCoroutinesApi::class)
     fun wifiCallingReadyFlow(): Flow<Boolean> {
         if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false)
diff --git a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
index aebc4eb..9f819d1 100644
--- a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
+++ b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
@@ -26,6 +26,8 @@
 import androidx.preference.Preference;
 
 import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 import com.google.common.base.Preconditions;
 
diff --git a/src/com/android/settings/notification/modes/IconLoader.java b/src/com/android/settings/notification/modes/IconLoader.java
deleted file mode 100644
index c590285..0000000
--- a/src/com/android/settings/notification/modes/IconLoader.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.notification.modes;
-
-import static com.google.common.util.concurrent.Futures.immediateFuture;
-
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.Nullable;
-import android.app.AutomaticZenRule;
-import android.content.Context;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.InsetDrawable;
-import android.service.notification.SystemZenRules;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.LruCache;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-import androidx.appcompat.content.res.AppCompatResources;
-
-import com.google.common.util.concurrent.FluentFuture;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-class IconLoader {
-
-    private static final String TAG = "ZenIconLoader";
-
-    private static final Drawable MISSING = new ColorDrawable();
-
-    @Nullable // Until first usage
-    private static IconLoader sInstance;
-
-    private final LruCache<String, Drawable> mCache;
-    private final ListeningExecutorService mBackgroundExecutor;
-
-    static IconLoader getInstance() {
-        if (sInstance == null) {
-            sInstance = new IconLoader();
-        }
-        return sInstance;
-    }
-
-    private IconLoader() {
-        this(Executors.newFixedThreadPool(4));
-    }
-
-    @VisibleForTesting
-    IconLoader(ExecutorService backgroundExecutor) {
-        mCache = new LruCache<>(50);
-        mBackgroundExecutor =
-                MoreExecutors.listeningDecorator(backgroundExecutor);
-    }
-
-    @NonNull
-    ListenableFuture<Drawable> getIcon(Context context, @NonNull AutomaticZenRule rule) {
-        if (rule.getIconResId() == 0) {
-            return Futures.immediateFuture(getFallbackIcon(context, rule.getType()));
-        }
-
-        return FluentFuture.from(loadIcon(context, rule.getPackageName(), rule.getIconResId()))
-                .transform(icon ->
-                        icon != null ? icon : getFallbackIcon(context, rule.getType()),
-                        MoreExecutors.directExecutor());
-    }
-
-    @NonNull
-    private ListenableFuture</* @Nullable */ Drawable> loadIcon(Context context, String pkg,
-            int iconResId) {
-        String cacheKey = pkg + ":" + iconResId;
-        synchronized (mCache) {
-            Drawable cachedValue = mCache.get(cacheKey);
-            if (cachedValue != null) {
-                return immediateFuture(cachedValue != MISSING ? cachedValue : null);
-            }
-        }
-
-        return FluentFuture.from(mBackgroundExecutor.submit(() -> {
-            if (TextUtils.isEmpty(pkg) || SystemZenRules.PACKAGE_ANDROID.equals(pkg)) {
-                return context.getDrawable(iconResId);
-            } else {
-                Context appContext = context.createPackageContext(pkg, 0);
-                Drawable appDrawable = AppCompatResources.getDrawable(appContext, iconResId);
-                return getMonochromeIconIfPresent(appDrawable);
-            }
-        })).catching(Exception.class, ex -> {
-            // If we cannot resolve the icon, then store MISSING in the cache below, so
-            // we don't try again.
-            Log.e(TAG, "Error while loading icon " + cacheKey, ex);
-            return null;
-        }, MoreExecutors.directExecutor()).transform(drawable -> {
-            synchronized (mCache) {
-                mCache.put(cacheKey, drawable != null ? drawable : MISSING);
-            }
-            return drawable;
-        }, MoreExecutors.directExecutor());
-    }
-
-    private static Drawable getFallbackIcon(Context context, int ruleType) {
-        int iconResIdFromType = switch (ruleType) {
-            case AutomaticZenRule.TYPE_UNKNOWN ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_unknown;
-            case AutomaticZenRule.TYPE_OTHER ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_other;
-            case AutomaticZenRule.TYPE_SCHEDULE_TIME ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_schedule_time;
-            case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar;
-            case AutomaticZenRule.TYPE_BEDTIME ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_bedtime;
-            case AutomaticZenRule.TYPE_DRIVING ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_driving;
-            case AutomaticZenRule.TYPE_IMMERSIVE ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_immersive;
-            case AutomaticZenRule.TYPE_THEATER ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_theater;
-            case AutomaticZenRule.TYPE_MANAGED ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_managed;
-            default ->
-                    com.android.internal.R.drawable.ic_zen_mode_type_unknown;
-        };
-        return requireNonNull(context.getDrawable(iconResIdFromType));
-    }
-
-    private static Drawable getMonochromeIconIfPresent(Drawable icon) {
-        // For created rules, the app should've provided a monochrome Drawable. However, implicit
-        // rules have the app's icon, which is not -- but might have a monochrome layer. Thus
-        // we choose it, if present.
-        if (icon instanceof AdaptiveIconDrawable adaptiveIcon) {
-            if (adaptiveIcon.getMonochrome() != null) {
-                // Wrap with negative inset => scale icon (inspired from BaseIconFactory)
-                return new InsetDrawable(adaptiveIcon.getMonochrome(),
-                        -2.0f * AdaptiveIconDrawable.getExtraInsetFraction());
-            }
-        }
-        return icon;
-    }
-}
diff --git a/src/com/android/settings/notification/modes/IconUtil.java b/src/com/android/settings/notification/modes/IconUtil.java
index c6ecaa0..1e653bf 100644
--- a/src/com/android/settings/notification/modes/IconUtil.java
+++ b/src/com/android/settings/notification/modes/IconUtil.java
@@ -24,6 +24,7 @@
 import android.graphics.drawable.ShapeDrawable;
 import android.graphics.drawable.shapes.OvalShape;
 
+import androidx.annotation.AttrRes;
 import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
 
@@ -32,10 +33,18 @@
 
 class IconUtil {
 
-    static Drawable applyTint(@NonNull Context context, @NonNull Drawable icon) {
+    static Drawable applyNormalTint(@NonNull Context context, @NonNull Drawable icon) {
+        return applyTint(context, icon, android.R.attr.colorControlNormal);
+    }
+
+    static Drawable applyAccentTint(@NonNull Context context, @NonNull Drawable icon) {
+        return applyTint(context, icon, android.R.attr.colorAccent);
+    }
+
+    private static Drawable applyTint(@NonNull Context context, @NonNull Drawable icon,
+            @AttrRes int colorAttr) {
         icon = icon.mutate();
-        icon.setTintList(
-                Utils.getColorAttr(context, android.R.attr.colorControlNormal));
+        icon.setTintList(Utils.getColorAttr(context, colorAttr));
         return icon;
     }
 
diff --git a/src/com/android/settings/notification/modes/InterruptionFilterPreferenceController.java b/src/com/android/settings/notification/modes/InterruptionFilterPreferenceController.java
index ee4c702..8bdeea4 100644
--- a/src/com/android/settings/notification/modes/InterruptionFilterPreferenceController.java
+++ b/src/com/android/settings/notification/modes/InterruptionFilterPreferenceController.java
@@ -26,6 +26,8 @@
 import androidx.preference.TwoStatePreference;
 
 import com.android.settings.R;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 class InterruptionFilterPreferenceController extends AbstractZenModePreferenceController
         implements Preference.OnPreferenceChangeListener {
diff --git a/src/com/android/settings/notification/modes/ZenMode.java b/src/com/android/settings/notification/modes/ZenMode.java
deleted file mode 100644
index 4ece2e3..0000000
--- a/src/com/android/settings/notification/modes/ZenMode.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.notification.modes;
-
-import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent;
-import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime;
-import static android.service.notification.ZenModeConfig.tryParseEventConditionId;
-import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId;
-
-import static com.google.common.base.Preconditions.checkState;
-
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.SuppressLint;
-import android.app.AutomaticZenRule;
-import android.app.NotificationManager;
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.service.notification.SystemZenRules;
-import android.service.notification.ZenDeviceEffects;
-import android.service.notification.ZenModeConfig;
-import android.service.notification.ZenPolicy;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.settings.R;
-
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.Objects;
-
-/**
- * Represents either an {@link AutomaticZenRule} or the manual DND rule in a unified way.
- *
- * <p>It also adapts other rule features that we don't want to expose in the UI, such as
- * interruption filters other than {@code PRIORITY}, rules without specific icons, etc.
- */
-class ZenMode {
-
-    private static final String TAG = "ZenMode";
-
-    static final String MANUAL_DND_MODE_ID = "manual_dnd";
-
-    // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
-    private static final ZenPolicy POLICY_INTERRUPTION_FILTER_ALARMS =
-            new ZenPolicy.Builder()
-                    .disallowAllSounds()
-                    .allowAlarms(true)
-                    .allowMedia(true)
-                    .allowPriorityChannels(false)
-                    .build();
-
-    // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
-    private static final ZenPolicy POLICY_INTERRUPTION_FILTER_NONE =
-            new ZenPolicy.Builder()
-                    .disallowAllSounds()
-                    .hideAllVisualEffects()
-                    .allowPriorityChannels(false)
-                    .build();
-
-    private final String mId;
-    private AutomaticZenRule mRule;
-    private final boolean mIsActive;
-    private final boolean mIsManualDnd;
-
-    ZenMode(String id, AutomaticZenRule rule, boolean isActive) {
-        this(id, rule, isActive, false);
-    }
-
-    private ZenMode(String id, AutomaticZenRule rule, boolean isActive, boolean isManualDnd) {
-        mId = id;
-        mRule = rule;
-        mIsActive = isActive;
-        mIsManualDnd = isManualDnd;
-    }
-
-    static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
-        return new ZenMode(MANUAL_DND_MODE_ID, manualRule, isActive, true);
-    }
-
-    @NonNull
-    public String getId() {
-        return mId;
-    }
-
-    @NonNull
-    public AutomaticZenRule getRule() {
-        return mRule;
-    }
-
-    @NonNull
-    public ListenableFuture<Drawable> getIcon(@NonNull Context context,
-            @NonNull IconLoader iconLoader) {
-        if (mIsManualDnd) {
-            return Futures.immediateFuture(requireNonNull(
-                    context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
-        }
-
-        return iconLoader.getIcon(context, mRule);
-    }
-
-    @NonNull
-    public ZenPolicy getPolicy() {
-        switch (mRule.getInterruptionFilter()) {
-            case INTERRUPTION_FILTER_PRIORITY:
-            case NotificationManager.INTERRUPTION_FILTER_ALL:
-                return requireNonNull(mRule.getZenPolicy());
-
-            case NotificationManager.INTERRUPTION_FILTER_ALARMS:
-                return POLICY_INTERRUPTION_FILTER_ALARMS;
-
-            case NotificationManager.INTERRUPTION_FILTER_NONE:
-                return POLICY_INTERRUPTION_FILTER_NONE;
-
-            case NotificationManager.INTERRUPTION_FILTER_UNKNOWN:
-            default:
-                Log.wtf(TAG, "Rule " + mId + " with unexpected interruptionFilter "
-                        + mRule.getInterruptionFilter());
-                return requireNonNull(mRule.getZenPolicy());
-        }
-    }
-
-    /**
-     * Updates the {@link ZenPolicy} of the associated {@link AutomaticZenRule} based on the
-     * supplied policy. In some cases this involves conversions, so that the following call
-     * to {@link #getPolicy} might return a different policy from the one supplied here.
-     */
-    @SuppressLint("WrongConstant")
-    public void setPolicy(@NonNull ZenPolicy policy) {
-        ZenPolicy currentPolicy = getPolicy();
-        if (currentPolicy.equals(policy)) {
-            return;
-        }
-
-        if (mRule.getInterruptionFilter() == INTERRUPTION_FILTER_ALL) {
-            Log.wtf(TAG, "Able to change policy without filtering being enabled");
-        }
-
-        // If policy is customized from any of the "special" ones, make the rule PRIORITY.
-        if (mRule.getInterruptionFilter() != INTERRUPTION_FILTER_PRIORITY) {
-            mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
-        }
-        mRule.setZenPolicy(policy);
-    }
-
-    @NonNull
-    public ZenDeviceEffects getDeviceEffects() {
-        return mRule.getDeviceEffects() != null
-                ? mRule.getDeviceEffects()
-                : new ZenDeviceEffects.Builder().build();
-    }
-
-    public void setCustomModeConditionId(Context context, Uri conditionId) {
-        checkState(SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()),
-                "Trying to change condition of non-system-owned rule %s (to %s)",
-                mRule, conditionId);
-
-        Uri oldCondition = mRule.getConditionId();
-        mRule.setConditionId(conditionId);
-
-        ZenModeConfig.ScheduleInfo scheduleInfo = tryParseScheduleConditionId(conditionId);
-        if (scheduleInfo != null) {
-            mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_TIME);
-            mRule.setOwner(ZenModeConfig.getScheduleConditionProvider());
-            mRule.setTriggerDescription(
-                    getTriggerDescriptionForScheduleTime(context, scheduleInfo));
-            return;
-        }
-
-        ZenModeConfig.EventInfo eventInfo = tryParseEventConditionId(conditionId);
-        if (eventInfo != null) {
-            mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR);
-            mRule.setOwner(ZenModeConfig.getEventConditionProvider());
-            mRule.setTriggerDescription(getTriggerDescriptionForScheduleEvent(context, eventInfo));
-            return;
-        }
-
-        if (ZenModeConfig.isValidCustomManualConditionId(conditionId)) {
-            mRule.setType(AutomaticZenRule.TYPE_OTHER);
-            mRule.setOwner(ZenModeConfig.getCustomManualConditionProvider());
-            mRule.setTriggerDescription("");
-            return;
-        }
-
-        Log.wtf(TAG, String.format(
-                "Changed condition of rule %s (%s -> %s) but cannot recognize which kind of "
-                        + "condition it was!",
-                mRule, oldCondition, conditionId));
-    }
-
-    public boolean canEditName() {
-        return !isManualDnd();
-    }
-
-    public boolean canEditIcon() {
-        return !isManualDnd();
-    }
-
-    public boolean canBeDeleted() {
-        return !mIsManualDnd;
-    }
-
-    public boolean isManualDnd() {
-        return mIsManualDnd;
-    }
-
-    public boolean isActive() {
-        return mIsActive;
-    }
-
-    public boolean isSystemOwned() {
-        return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName());
-    }
-
-    @AutomaticZenRule.Type
-    public int getType() {
-        return mRule.getType();
-    }
-
-    @Override
-    public boolean equals(@Nullable Object obj) {
-        return obj instanceof ZenMode other
-                && mId.equals(other.mId)
-                && mRule.equals(other.mRule)
-                && mIsActive == other.mIsActive;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(mId, mRule, mIsActive);
-    }
-
-    @Override
-    public String toString() {
-        return mId + "(" + (mIsActive ? "active" : "inactive") + ") -> " + mRule;
-    }
-}
diff --git a/src/com/android/settings/notification/modes/ZenModeActionsPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeActionsPreferenceController.java
index 8585234..914683f 100644
--- a/src/com/android/settings/notification/modes/ZenModeActionsPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeActionsPreferenceController.java
@@ -16,7 +16,7 @@
 
 package com.android.settings.notification.modes;
 
-import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.content.Context;
 import android.os.Bundle;
@@ -27,6 +27,8 @@
 
 import com.android.settings.R;
 import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.ActionButtonsPreference;
 
 class ZenModeActionsPreferenceController extends AbstractZenModePreferenceController {
@@ -50,7 +52,7 @@
         buttonsPreference.setButton2Enabled(zenMode.canEditIcon());
         buttonsPreference.setButton2OnClickListener(v -> {
             Bundle bundle = new Bundle();
-            bundle.putString(MODE_ID, zenMode.getId());
+            bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
             new SubSettingLauncher(mContext)
                     .setDestination(ZenModeIconPickerFragment.class.getName())
                     // TODO: b/332937635 - Update metrics category
diff --git a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
index 6835e6c..f62dfdd 100644
--- a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java
@@ -17,8 +17,7 @@
 package com.android.settings.notification.modes;
 
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
-
-import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.content.Context;
 import android.os.Bundle;
@@ -32,6 +31,8 @@
 
 import com.android.settings.core.SubSettingLauncher;
 import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -72,7 +73,7 @@
     @Override
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, zenMode.getId());
+        bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
         // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeAppsFragment.class.getName())
@@ -129,11 +130,13 @@
         return appsBypassingDnd;
     }
 
-    @VisibleForTesting final ApplicationsState.Callbacks mAppSessionCallbacks =
+    @VisibleForTesting
+    final ApplicationsState.Callbacks mAppSessionCallbacks =
             new ApplicationsState.Callbacks() {
 
                 @Override
-                public void onRunningStateChanged(boolean running) { }
+                public void onRunningStateChanged(boolean running) {
+                }
 
                 @Override
                 public void onPackageListChanged() {
@@ -146,16 +149,20 @@
                 }
 
                 @Override
-                public void onPackageIconChanged() { }
+                public void onPackageIconChanged() {
+                }
 
                 @Override
-                public void onPackageSizeChanged(String packageName) { }
+                public void onPackageSizeChanged(String packageName) {
+                }
 
                 @Override
-                public void onAllSizesComputed() { }
+                public void onAllSizesComputed() {
+                }
 
                 @Override
-                public void onLauncherInfoChanged() { }
+                public void onLauncherInfoChanged() {
+                }
 
                 @Override
                 public void onLoadEntriesCompleted() {
diff --git a/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java
index d5ef044..1d807ed 100644
--- a/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeAppsPreferenceController.java
@@ -16,7 +16,7 @@
 
 package com.android.settings.notification.modes;
 
-import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.app.settings.SettingsEnums;
 import android.content.Context;
@@ -31,6 +31,8 @@
 import androidx.preference.TwoStatePreference;
 
 import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.SelectorWithWidgetPreference;
 
 public class ZenModeAppsPreferenceController extends
@@ -103,7 +105,7 @@
     private void launchPrioritySettings() {
         Bundle bundle = new Bundle();
         if (mModeId != null) {
-            bundle.putString(MODE_ID, mModeId);
+            bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, mModeId);
         }
         // TODO(b/332937635): Update metrics category
         new SubSettingLauncher(mContext)
diff --git a/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java
index 1846dfc..4a99b33 100644
--- a/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeButtonPreferenceController.java
@@ -23,9 +23,11 @@
 import androidx.preference.Preference;
 
 import com.android.settings.R;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.LayoutPreference;
 
-public class ZenModeButtonPreferenceController extends AbstractZenModePreferenceController {
+class ZenModeButtonPreferenceController extends AbstractZenModePreferenceController {
 
     private Button mZenButton;
 
diff --git a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java
index 8d27d4c..e5c1e48 100644
--- a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java
@@ -16,7 +16,7 @@
 
 package com.android.settings.notification.modes;
 
-import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.content.Context;
 import android.os.Bundle;
@@ -25,6 +25,8 @@
 import androidx.preference.Preference;
 
 import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 class ZenModeCallsLinkPreferenceController extends AbstractZenModePreferenceController  {
 
@@ -39,7 +41,7 @@
     @Override
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, zenMode.getId());
+        bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
         // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeCallsFragment.class.getName())
diff --git a/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceController.java
index bca7b55..b0d3952 100644
--- a/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceController.java
@@ -23,7 +23,10 @@
 import androidx.preference.Preference;
 import androidx.preference.TwoStatePreference;
 
-public class ZenModeDisplayEffectPreferenceController extends AbstractZenModePreferenceController
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
+class ZenModeDisplayEffectPreferenceController extends AbstractZenModePreferenceController
         implements Preference.OnPreferenceChangeListener {
 
     public ZenModeDisplayEffectPreferenceController(Context context, String key,
@@ -34,24 +37,20 @@
     @Override
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         TwoStatePreference pref = (TwoStatePreference) preference;
-        ZenDeviceEffects effects =  zenMode.getRule().getDeviceEffects();
-        if (effects == null) {
-            pref.setChecked(false);
-        } else {
-            switch (getPreferenceKey()) {
-                case "effect_greyscale":
-                    pref.setChecked(effects.shouldDisplayGrayscale());
-                    break;
-                case "effect_aod":
-                    pref.setChecked(effects.shouldSuppressAmbientDisplay());
-                    break;
-                case "effect_wallpaper":
-                    pref.setChecked(effects.shouldDimWallpaper());
-                    break;
-                case "effect_dark_theme":
-                    pref.setChecked(effects.shouldUseNightMode());
-                    break;
-            }
+        ZenDeviceEffects effects =  zenMode.getDeviceEffects();
+        switch (getPreferenceKey()) {
+            case "effect_greyscale":
+                pref.setChecked(effects.shouldDisplayGrayscale());
+                break;
+            case "effect_aod":
+                pref.setChecked(effects.shouldSuppressAmbientDisplay());
+                break;
+            case "effect_wallpaper":
+                pref.setChecked(effects.shouldDimWallpaper());
+                break;
+            case "effect_dark_theme":
+                pref.setChecked(effects.shouldUseNightMode());
+                break;
         }
     }
 
diff --git a/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java
index 712c78a..d3559f1 100644
--- a/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java
@@ -16,7 +16,7 @@
 
 package com.android.settings.notification.modes;
 
-import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.content.Context;
 import android.os.Bundle;
@@ -25,6 +25,8 @@
 import androidx.preference.Preference;
 
 import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 class ZenModeDisplayLinkPreferenceController extends AbstractZenModePreferenceController  {
 
@@ -39,7 +41,7 @@
     @Override
     void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, zenMode.getId());
+        bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
         // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeDisplayFragment.class.getName())
diff --git a/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceController.java
index 8517af1..326bc97 100644
--- a/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceController.java
@@ -23,6 +23,9 @@
 import androidx.preference.Preference;
 import androidx.preference.TwoStatePreference;
 
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 /**
  * Preference controller controlling whether a time schedule-based mode ends at the next alarm.
  */
diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java
index f2f47b9..5897c4d 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragment.java
@@ -16,20 +16,28 @@
 
 package com.android.settings.notification.modes;
 
+import android.app.AlertDialog;
 import android.app.Application;
 import android.app.AutomaticZenRule;
 import android.app.settings.SettingsEnums;
 import android.content.Context;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
 
 import com.android.settings.R;
 import com.android.settingslib.applications.ApplicationsState;
 import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.notification.modes.ZenMode;
 
 import java.util.ArrayList;
 import java.util.List;
 
 public class ZenModeFragment extends ZenModeFragmentBase {
 
+    // for mode deletion menu
+    private static final int DELETE_MODE = 1;
+
     @Override
     protected int getPreferenceScreenResId() {
         return R.xml.modes_rule_settings;
@@ -76,4 +84,43 @@
         // TODO: b/332937635 - make this the correct metrics category
         return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION;
     }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        menu.add(Menu.NONE, DELETE_MODE, Menu.NONE, R.string.zen_mode_menu_delete_mode);
+        super.onCreateOptionsMenu(menu, inflater);
+    }
+
+    @Override
+    protected boolean onOptionsItemSelected(MenuItem item, ZenMode zenMode) {
+        switch (item.getItemId()) {
+            case DELETE_MODE:
+                new AlertDialog.Builder(mContext)
+                        .setTitle(mContext.getString(R.string.zen_mode_delete_mode_confirmation,
+                                zenMode.getRule().getName()))
+                        .setPositiveButton(R.string.zen_mode_schedule_delete,
+                                (dialog, which) -> {
+                                    // start finishing before calling removeMode() so that we don't
+                                    // try to update this activity with a nonexistent mode when the
+                                    // zen mode config is updated
+                                    finish();
+                                    mBackend.removeMode(zenMode);
+                                })
+                        .setNegativeButton(R.string.cancel, null)
+                        .show();
+                return true;
+            default:
+                return super.onOptionsItemSelected(item);
+        }
+    }
+
+    @Override
+    protected void updateZenModeState() {
+        // Because this fragment may be asked to finish by the delete menu but not be done doing
+        // so yet, ignore any attempts to update info in that case.
+        if (getActivity() != null && getActivity().isFinishing()) {
+            return;
+        }
+        super.updateZenModeState();
+    }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
index 67cc13b..d08f7ea 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java
@@ -16,10 +16,13 @@
 
 package com.android.settings.notification.modes;
 
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
+
 import android.app.AutomaticZenRule;
 import android.content.Context;
 import android.os.Bundle;
 import android.util.Log;
+import android.view.MenuItem;
 import android.widget.Toast;
 
 import androidx.annotation.NonNull;
@@ -29,6 +32,7 @@
 
 import com.android.settings.R;
 import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.notification.modes.ZenMode;
 
 import java.util.List;
 
@@ -37,7 +41,6 @@
  */
 abstract class ZenModeFragmentBase extends ZenModesFragmentBase {
     static final String TAG = "ZenModeSettings";
-    static final String MODE_ID = "MODE_ID";
 
     @Nullable  // only until reloadMode() is called
     private ZenMode mZenMode;
@@ -46,17 +49,21 @@
     public void onAttach(@NonNull Context context) {
         super.onAttach(context);
 
-        // TODO: b/322373473 - Update if modes page ends up using a different method of passing id
+        String id = null;
+        if (getActivity() != null && getActivity().getIntent() != null) {
+            id = getActivity().getIntent().getStringExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID);
+        }
         Bundle bundle = getArguments();
-        if (bundle != null && bundle.containsKey(MODE_ID)) {
-            String id = bundle.getString(MODE_ID);
-            if (!reloadMode(id)) {
-                Log.e(TAG, "Mode id " + id + " not found");
-                toastAndFinish();
-                return;
-            }
-        } else {
-            Log.e(TAG, "Mode id required to set mode config settings");
+        if (id == null && bundle != null && bundle.containsKey(EXTRA_AUTOMATIC_ZEN_RULE_ID)) {
+            id = bundle.getString(EXTRA_AUTOMATIC_ZEN_RULE_ID);
+        }
+        if (id == null) {
+            Log.d(TAG, "No id provided");
+            toastAndFinish();
+            return;
+        }
+        if (!reloadMode(id)) {
+            Log.d(TAG, "Mode id " + id + " not found");
             toastAndFinish();
             return;
         }
@@ -108,6 +115,18 @@
         updateControllers();
     }
 
+    @Override
+    public final boolean onOptionsItemSelected(MenuItem item) {
+        if (mZenMode != null) {
+            return onOptionsItemSelected(item, mZenMode);
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    protected boolean onOptionsItemSelected(MenuItem item, @NonNull ZenMode zenMode) {
+        return true;
+    }
+
     private void updateControllers() {
         if (getPreferenceControllers() == null || mZenMode == null) {
             return;
diff --git a/src/com/android/settings/notification/modes/ZenModeHeaderController.java b/src/com/android/settings/notification/modes/ZenModeHeaderController.java
index d8f0a67..1845ee8 100644
--- a/src/com/android/settings/notification/modes/ZenModeHeaderController.java
+++ b/src/com/android/settings/notification/modes/ZenModeHeaderController.java
@@ -25,6 +25,9 @@
 import com.android.settings.R;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.notification.modes.ZenIconLoader;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.LayoutPreference;
 
 class ZenModeHeaderController extends AbstractZenModePreferenceController {
@@ -62,8 +65,8 @@
         }
 
         FutureUtil.whenDone(
-                zenMode.getIcon(mContext, IconLoader.getInstance()),
-                icon -> mHeaderController.setIcon(IconUtil.applyTint(mContext, icon))
+                zenMode.getIcon(mContext, ZenIconLoader.getInstance()),
+                icon -> mHeaderController.setIcon(IconUtil.applyNormalTint(mContext, icon))
                         .done(/* rebindActions= */ false),
                 mContext.getMainExecutor());
     }
diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java
index 9eaaa97..d1d53af 100644
--- a/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeIconPickerIconPreferenceController.java
@@ -25,6 +25,9 @@
 import com.android.settings.R;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.notification.modes.ZenIconLoader;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.LayoutPreference;
 
 class ZenModeIconPickerIconPreferenceController extends AbstractZenModePreferenceController {
@@ -51,8 +54,8 @@
         }
 
         FutureUtil.whenDone(
-                zenMode.getIcon(mContext, IconLoader.getInstance()),
-                icon -> mHeaderController.setIcon(IconUtil.applyTint(mContext, icon))
+                zenMode.getIcon(mContext, ZenIconLoader.getInstance()),
+                icon -> mHeaderController.setIcon(IconUtil.applyNormalTint(mContext, icon))
                         .done(/* rebindActions= */ false),
                 mContext.getMainExecutor());
     }
diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java
index fc991dc..85ceafe 100644
--- a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java
@@ -33,6 +33,8 @@
 
 import com.android.settings.R;
 import com.android.settings.dashboard.DashboardFragment;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.LayoutPreference;
 
 import com.google.common.collect.ImmutableList;
diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java
index 6e563c4..9b7c8a1 100644
--- a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java
@@ -16,7 +16,7 @@
 
 package com.android.settings.notification.modes;
 
-import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.content.Context;
 import android.os.Bundle;
@@ -25,6 +25,8 @@
 import androidx.preference.Preference;
 
 import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 class ZenModeMessagesLinkPreferenceController extends AbstractZenModePreferenceController {
     private final ZenModeSummaryHelper mSummaryHelper;
@@ -38,7 +40,7 @@
     @Override
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, zenMode.getId());
+        bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
         // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeMessagesFragment.class.getName())
diff --git a/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java
index 1b1fec4..a2d9411 100644
--- a/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java
@@ -17,8 +17,7 @@
 package com.android.settings.notification.modes;
 
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
-
-import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.content.Context;
 import android.os.Bundle;
@@ -27,6 +26,8 @@
 import androidx.preference.Preference;
 
 import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 class ZenModeNotifVisLinkPreferenceController extends AbstractZenModePreferenceController  {
 
@@ -46,7 +47,7 @@
     @Override
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, zenMode.getId());
+        bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
         // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeNotifVisFragment.class.getName())
diff --git a/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceController.java
index f918b25..3d9f713 100644
--- a/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceController.java
@@ -21,19 +21,21 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
-import androidx.preference.CheckBoxPreference;
 import androidx.preference.Preference;
 import androidx.preference.TwoStatePreference;
 
-import com.android.settings.widget.DisabledCheckBoxPreference;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 public class ZenModeNotifVisPreferenceController extends AbstractZenModePreferenceController
         implements Preference.OnPreferenceChangeListener {
 
-    @VisibleForTesting protected @ZenPolicy.VisualEffect int mEffect;
+    @VisibleForTesting
+    protected @ZenPolicy.VisualEffect int mEffect;
 
     // if any of these effects are suppressed, this effect must be too
-    @VisibleForTesting protected @ZenPolicy.VisualEffect int[] mParentSuppressedEffects;
+    @VisibleForTesting
+    protected @ZenPolicy.VisualEffect int[] mParentSuppressedEffects;
 
     public ZenModeNotifVisPreferenceController(Context context, String key,
             @ZenPolicy.VisualEffect int visualEffect,
diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
index f6df9e3..99625eb 100644
--- a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
@@ -17,8 +17,7 @@
 package com.android.settings.notification.modes;
 
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
-
-import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.content.Context;
 import android.os.Bundle;
@@ -27,6 +26,8 @@
 import androidx.preference.Preference;
 
 import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 /**
  * Preference with a link and summary about what other sounds can break through the mode
@@ -49,7 +50,7 @@
     @Override
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, zenMode.getId());
+        bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeOtherFragment.class.getName())
                 .setSourceMetricsCategory(0)
diff --git a/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java
index a770164e..ad5fa6a 100644
--- a/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java
@@ -28,6 +28,9 @@
 import androidx.preference.Preference;
 import androidx.preference.TwoStatePreference;
 
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 class ZenModeOtherPreferenceController extends AbstractZenModePreferenceController
         implements Preference.OnPreferenceChangeListener {
 
diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
index db8e135..1613a01 100644
--- a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
@@ -17,8 +17,7 @@
 package com.android.settings.notification.modes;
 
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
-
-import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import android.content.Context;
 import android.os.Bundle;
@@ -27,6 +26,8 @@
 import androidx.preference.Preference;
 
 import com.android.settings.core.SubSettingLauncher;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 /**
  * Preference with a link and summary about what calls and messages can break through the mode
@@ -49,7 +50,7 @@
     @Override
     public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, zenMode.getId());
+        bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, zenMode.getId());
         // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModePeopleFragment.class.getName())
diff --git a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java
index 31a8a0d..0f9323d 100644
--- a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java
@@ -46,6 +46,8 @@
 import com.android.settings.R;
 import com.android.settings.core.SubSettingLauncher;
 import com.android.settings.notification.app.ConversationListSettings;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.SelectorWithWidgetPreference;
 
 import java.util.ArrayList;
diff --git a/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java
index 7569051..ae62e35 100644
--- a/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java
@@ -26,6 +26,8 @@
 import androidx.preference.TwoStatePreference;
 
 import com.android.settings.R;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 class ZenModeRepeatCallersPreferenceController extends AbstractZenModePreferenceController
         implements Preference.OnPreferenceChangeListener {
diff --git a/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java
index e879076..4f45c5c8 100644
--- a/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceController.java
@@ -31,6 +31,8 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.settings.R;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 import java.util.ArrayList;
 import java.util.Arrays;
diff --git a/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java
index 3432ed5..878a508 100644
--- a/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceController.java
@@ -31,6 +31,8 @@
 import androidx.preference.Preference;
 
 import com.android.settings.R;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.LayoutPreference;
 
 import java.text.SimpleDateFormat;
diff --git a/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java
index fd27958..1c96fee 100644
--- a/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceController.java
@@ -29,6 +29,8 @@
 import com.android.settings.R;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settingslib.PrimarySwitchPreference;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 /**
  * Preference controller for the link to an individual mode's configuration page.
@@ -63,7 +65,6 @@
         }
         switchPref.setChecked(zenMode.getRule().isEnabled());
         switchPref.setOnPreferenceChangeListener(mSwitchChangeListener);
-
         switchPref.setSummary(zenMode.getRule().getTriggerDescription());
         switchPref.setIcon(null);
         switchPref.setOnPreferenceClickListener(null);
diff --git a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java
index bd0b798..48a4c36 100644
--- a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java
+++ b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java
@@ -48,6 +48,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.settings.R;
+import com.android.settingslib.notification.modes.ZenMode;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -193,7 +194,7 @@
             enabledEffects.add(getBlockedEffectsSummary(zenMode));
             isFirst = false;
         }
-        ZenDeviceEffects currEffects =  zenMode.getRule().getDeviceEffects();
+        ZenDeviceEffects currEffects = zenMode.getRule().getDeviceEffects();
         if (currEffects != null) {
             if (currEffects.shouldDisplayGrayscale()) {
                 if (isFirst) {
diff --git a/src/com/android/settings/notification/modes/ZenModesBackend.java b/src/com/android/settings/notification/modes/ZenModesBackend.java
deleted file mode 100644
index 4f86778..0000000
--- a/src/com/android/settings/notification/modes/ZenModesBackend.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.notification.modes;
-
-import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.AutomaticZenRule;
-import android.app.NotificationManager;
-import android.content.Context;
-import android.net.Uri;
-import android.provider.Settings;
-import android.service.notification.Condition;
-import android.service.notification.ZenModeConfig;
-import android.util.Log;
-
-import com.android.settings.R;
-
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Class used for Settings-NMS interactions related to Mode management.
- *
- * <p>This class converts {@link AutomaticZenRule} instances, as well as the manual zen mode,
- * into the unified {@link ZenMode} format.
- */
-class ZenModesBackend {
-
-    private static final String TAG = "ZenModeBackend";
-
-    @Nullable // Until first usage
-    private static ZenModesBackend sInstance;
-
-    private final NotificationManager mNotificationManager;
-
-    private final Context mContext;
-
-    static ZenModesBackend getInstance(Context context) {
-        if (sInstance == null) {
-            sInstance = new ZenModesBackend(context.getApplicationContext());
-        }
-        return sInstance;
-    }
-
-    ZenModesBackend(Context context) {
-        mContext = context;
-        mNotificationManager = context.getSystemService(NotificationManager.class);
-    }
-
-    List<ZenMode> getModes() {
-        ArrayList<ZenMode> modes = new ArrayList<>();
-        ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
-        modes.add(getManualDndMode(currentConfig));
-
-        Map<String, AutomaticZenRule> zenRules = mNotificationManager.getAutomaticZenRules();
-        for (Map.Entry<String, AutomaticZenRule> zenRuleEntry : zenRules.entrySet()) {
-            String ruleId = zenRuleEntry.getKey();
-            modes.add(new ZenMode(ruleId, zenRuleEntry.getValue(),
-                    isRuleActive(ruleId, currentConfig)));
-        }
-
-        modes.sort((l, r) -> {
-            if (l.isManualDnd()) {
-                return -1;
-            } else if (r.isManualDnd()) {
-                return 1;
-            }
-            return l.getRule().getName().compareTo(r.getRule().getName());
-        });
-
-        return modes;
-    }
-
-    @Nullable
-    ZenMode getMode(String id) {
-        ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
-        if (ZenMode.MANUAL_DND_MODE_ID.equals(id)) {
-            return getManualDndMode(currentConfig);
-        } else {
-            AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id);
-            if (rule == null) {
-                return null;
-            }
-            return new ZenMode(id, rule, isRuleActive(id, currentConfig));
-        }
-    }
-
-    private ZenMode getManualDndMode(ZenModeConfig config) {
-        ZenModeConfig.ZenRule manualRule = config.manualRule;
-        // TODO: b/333682392 - Replace with final strings for name & trigger description
-        AutomaticZenRule manualDndRule = new AutomaticZenRule.Builder(
-                mContext.getString(R.string.zen_mode_settings_title), manualRule.conditionId)
-                .setType(manualRule.type)
-                .setZenPolicy(manualRule.zenPolicy)
-                .setDeviceEffects(manualRule.zenDeviceEffects)
-                .setManualInvocationAllowed(manualRule.allowManualInvocation)
-                .setConfigurationActivity(null) // No further settings
-                .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
-                .build();
-
-        return ZenMode.manualDndMode(manualDndRule, config != null && config.isManualActive());
-    }
-
-    private static boolean isRuleActive(String id, ZenModeConfig config) {
-        if (config == null) {
-            // shouldn't happen if the config is coming from NM, but be safe
-            return false;
-        }
-        ZenModeConfig.ZenRule configRule = config.automaticRules.get(id);
-        return configRule != null && configRule.isAutomaticActive();
-    }
-
-    void updateMode(ZenMode mode) {
-        if (mode.isManualDnd()) {
-            try {
-                NotificationManager.Policy dndPolicy =
-                        new ZenModeConfig().toNotificationPolicy(mode.getPolicy());
-                mNotificationManager.setNotificationPolicy(dndPolicy, /* fromUser= */ true);
-
-                mNotificationManager.setManualZenRuleDeviceEffects(
-                        mode.getRule().getDeviceEffects());
-            } catch (Exception e) {
-                Log.w(TAG, "Error updating manual mode", e);
-            }
-        } else {
-            mNotificationManager.updateAutomaticZenRule(mode.getId(), mode.getRule(),
-                    /* fromUser= */ true);
-        }
-    }
-
-    void activateMode(ZenMode mode, @Nullable Duration forDuration) {
-        if (mode.isManualDnd()) {
-            Uri durationConditionId = null;
-            if (forDuration != null) {
-                durationConditionId = ZenModeConfig.toTimeCondition(mContext,
-                        (int) forDuration.toMinutes(), ActivityManager.getCurrentUser(), true).id;
-            }
-            mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
-                    durationConditionId, TAG, /* fromUser= */ true);
-
-        } else {
-            if (forDuration != null) {
-                throw new IllegalArgumentException(
-                        "Only the manual DND mode can be activated for a specific duration");
-            }
-            mNotificationManager.setAutomaticZenRuleState(mode.getId(),
-                    new Condition(mode.getRule().getConditionId(), "", Condition.STATE_TRUE,
-                            Condition.SOURCE_USER_ACTION));
-        }
-    }
-
-    void deactivateMode(ZenMode mode) {
-        if (mode.isManualDnd()) {
-            // When calling with fromUser=true this will not snooze other modes.
-            mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG,
-                    /* fromUser= */ true);
-        } else {
-            // TODO: b/333527800 - This should (potentially) snooze the rule if it was active.
-            mNotificationManager.setAutomaticZenRuleState(mode.getId(),
-                    new Condition(mode.getRule().getConditionId(), "", Condition.STATE_FALSE,
-                            Condition.SOURCE_USER_ACTION));
-        }
-    }
-
-    void removeMode(ZenMode mode) {
-        if (!mode.canBeDeleted()) {
-            throw new IllegalArgumentException("Mode " + mode + " cannot be deleted!");
-        }
-        mNotificationManager.removeAutomaticZenRule(mode.getId(), /* fromUser= */ true);
-    }
-
-    /**
-     * Creates a new custom mode with the provided {@code name}. The mode will be "manual" (i.e.
-     * not have a schedule), this can be later updated by the user in the mode settings page.
-     *
-     * @return the created mode. Only {@code null} if creation failed due to an internal error
-     */
-    @Nullable
-    ZenMode addCustomMode(String name) {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder(name,
-                ZenModeConfig.toCustomManualConditionId())
-                .setPackage(ZenModeConfig.getCustomManualConditionProvider().getPackageName())
-                .setType(AutomaticZenRule.TYPE_OTHER)
-                .setOwner(ZenModeConfig.getCustomManualConditionProvider())
-                .setManualInvocationAllowed(true)
-                .build();
-
-        String ruleId = mNotificationManager.addAutomaticZenRule(rule);
-        return getMode(ruleId);
-    }
-}
diff --git a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
index d99593a..e1156fe 100644
--- a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
+++ b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java
@@ -27,6 +27,7 @@
 import android.util.Log;
 
 import com.android.settings.dashboard.RestrictedDashboardFragment;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 /**
  * Base class for all Settings pages controlling Modes behavior.
diff --git a/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceController.java b/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceController.java
index c229fb1..ba74b93 100644
--- a/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModesListAddModePreferenceController.java
@@ -22,6 +22,8 @@
 
 import com.android.settings.utils.ZenServiceListing;
 import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 import java.util.Random;
 
diff --git a/src/com/android/settings/notification/modes/ZenModesListFragment.java b/src/com/android/settings/notification/modes/ZenModesListFragment.java
index 80678f6..77107f8 100644
--- a/src/com/android/settings/notification/modes/ZenModesListFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModesListFragment.java
@@ -29,6 +29,7 @@
 import com.android.settings.utils.ManagedServiceSettings;
 import com.android.settings.utils.ZenServiceListing;
 import com.android.settingslib.core.AbstractPreferenceController;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.search.SearchIndexable;
 
 import com.google.common.collect.ImmutableList;
diff --git a/src/com/android/settings/notification/modes/ZenModesListItemPreference.java b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java
index 7ecfb3a..1bc6e55 100644
--- a/src/com/android/settings/notification/modes/ZenModesListItemPreference.java
+++ b/src/com/android/settings/notification/modes/ZenModesListItemPreference.java
@@ -16,16 +16,31 @@
 package com.android.settings.notification.modes;
 
 import android.content.Context;
+import android.widget.TextView;
 
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
 import com.android.settingslib.RestrictedPreference;
+import com.android.settingslib.Utils;
+import com.android.settingslib.notification.modes.ZenIconLoader;
+import com.android.settingslib.notification.modes.ZenMode;
+
+import com.google.common.base.Strings;
 
 /**
  * Preference representing a single mode item on the modes aggregator page. Clicking on this
  * preference leads to an individual mode's configuration page.
  */
 class ZenModesListItemPreference extends RestrictedPreference {
-    final Context mContext;
-    ZenMode mZenMode;
+
+    private final Context mContext;
+    private ZenMode mZenMode;
+
+    private TextView mTitleView;
+    private TextView mSummaryView;
 
     ZenModesListItemPreference(Context context, ZenMode zenMode) {
         super(context);
@@ -35,19 +50,65 @@
     }
 
     @Override
+    public void onBindViewHolder(PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+        if (holder.findViewById(android.R.id.title) instanceof TextView titleView) {
+            mTitleView = titleView;
+        }
+        if (holder.findViewById(android.R.id.summary) instanceof TextView summaryView) {
+            mSummaryView = summaryView;
+        }
+        updateTextColor(mZenMode);
+    }
+
+    @Override
     public void onClick() {
         ZenSubSettingLauncher.forMode(mContext, mZenMode.getId()).launch();
     }
 
     public void setZenMode(ZenMode zenMode) {
         mZenMode = zenMode;
-        setTitle(mZenMode.getRule().getName());
-        setSummary(mZenMode.getRule().getTriggerDescription());
-        setIconSize(ICON_SIZE_SMALL);
+        setTitle(mZenMode.getName());
+        CharSequence statusText = switch (mZenMode.getStatus()) {
+            case ENABLED_AND_ACTIVE ->
+                    Strings.isNullOrEmpty(mZenMode.getTriggerDescription())
+                            ? mContext.getString(R.string.zen_mode_active_text)
+                            : mContext.getString(
+                                    R.string.zen_mode_format_status_and_trigger,
+                                    mContext.getString(R.string.zen_mode_active_text),
+                                    mZenMode.getRule().getTriggerDescription());
+            case ENABLED -> mZenMode.getRule().getTriggerDescription();
+            case DISABLED_BY_USER -> mContext.getString(R.string.zen_mode_disabled_by_user);
+            case DISABLED_BY_OTHER -> mContext.getString(R.string.zen_mode_disabled_tap_to_set_up);
+        };
+        setSummary(statusText);
 
+        setIconSize(ICON_SIZE_SMALL);
         FutureUtil.whenDone(
-                mZenMode.getIcon(mContext, IconLoader.getInstance()),
-                icon -> setIcon(IconUtil.applyTint(mContext, icon)),
+                mZenMode.getIcon(mContext, ZenIconLoader.getInstance()),
+                icon -> setIcon(
+                        zenMode.isActive()
+                                ? IconUtil.applyAccentTint(mContext, icon)
+                                : IconUtil.applyNormalTint(mContext, icon)),
                 mContext.getMainExecutor());
+
+        updateTextColor(zenMode);
+    }
+
+    private void updateTextColor(@Nullable ZenMode zenMode) {
+        boolean isActive = zenMode != null && zenMode.isActive();
+        if (mTitleView != null) {
+            mTitleView.setTextColor(Utils.getColorAttr(mContext,
+                    isActive ? android.R.attr.colorAccent : android.R.attr.textColorPrimary));
+        }
+        if (mSummaryView != null) {
+            mSummaryView.setTextColor(Utils.getColorAttr(mContext,
+                    isActive ? android.R.attr.colorAccent : android.R.attr.textColorSecondary));
+        }
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+    ZenMode getZenMode() {
+        return mZenMode;
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
index 5dcd9eb..fb07078 100644
--- a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
@@ -27,6 +27,8 @@
 
 import com.android.settings.R;
 import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.search.SearchIndexableRaw;
 
 import java.util.HashMap;
diff --git a/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java b/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java
index aa66e6c..529f7fa 100644
--- a/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java
+++ b/src/com/android/settings/notification/modes/ZenSubSettingLauncher.java
@@ -16,6 +16,8 @@
 
 package com.android.settings.notification.modes;
 
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
+
 import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.os.Bundle;
@@ -33,7 +35,7 @@
             Class<? extends ZenModeFragmentBase> fragmentClass, String modeId,
             int sourceMetricsCategory) {
         Bundle bundle = new Bundle();
-        bundle.putString(ZenModeFragmentBase.MODE_ID, modeId);
+        bundle.putString(EXTRA_AUTOMATIC_ZEN_RULE_ID, modeId);
 
         return new SubSettingLauncher(context)
                 .setDestination(fragmentClass.getName())
diff --git a/src/com/android/settings/notification/zen/ZenModeBackend.java b/src/com/android/settings/notification/zen/ZenModeBackend.java
index 426f52d..de641c5 100644
--- a/src/com/android/settings/notification/zen/ZenModeBackend.java
+++ b/src/com/android/settings/notification/zen/ZenModeBackend.java
@@ -116,7 +116,7 @@
                 ActivityManager.getCurrentUser(), true).id;
         if (android.app.Flags.modesApi()) {
             mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
-                        conditionId, TAG, /* fromUser= */ true);
+                    conditionId, TAG, /* fromUser= */ true);
         } else {
             mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                     conditionId, TAG);
@@ -241,12 +241,14 @@
         }
 
         savePolicy(getNewDefaultPriorityCategories(allowSenders, category),
-            priorityCallSenders, priorityMessagesSenders, mPolicy.suppressedVisualEffects,
+                priorityCallSenders, priorityMessagesSenders, mPolicy.suppressedVisualEffects,
                 mPolicy.priorityConversationSenders);
 
-        if (ZenModeSettingsBase.DEBUG) Log.d(TAG, "onPrefChange allow" +
-                stringCategory + "=" + allowSenders + " allow" + stringCategory + "From="
-                + ZenModeConfig.sourceToString(allowSendersFrom));
+        if (ZenModeSettingsBase.DEBUG) {
+            Log.d(TAG, "onPrefChange allow"
+                    + stringCategory + "=" + allowSenders + " allow" + stringCategory + "From="
+                    + ZenModeConfig.sourceToString(allowSendersFrom));
+        }
     }
 
     protected void saveConversationSenders(int val) {
@@ -280,7 +282,7 @@
         switch (contactType) {
             case ZenPolicy.PEOPLE_TYPE_ANYONE:
                 return ZEN_MODE_FROM_ANYONE;
-            case  ZenPolicy.PEOPLE_TYPE_CONTACTS:
+            case ZenPolicy.PEOPLE_TYPE_CONTACTS:
                 return ZEN_MODE_FROM_CONTACTS;
             case ZenPolicy.PEOPLE_TYPE_STARRED:
                 return ZEN_MODE_FROM_STARRED;
@@ -308,7 +310,7 @@
         switch (setting) {
             case ZenPolicy.PEOPLE_TYPE_ANYONE:
                 return NotificationManager.Policy.PRIORITY_SENDERS_ANY;
-            case  ZenPolicy.PEOPLE_TYPE_CONTACTS:
+            case ZenPolicy.PEOPLE_TYPE_CONTACTS:
                 return NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS;
             case ZenPolicy.PEOPLE_TYPE_STARRED:
                 return NotificationManager.Policy.PRIORITY_SENDERS_STARRED;
@@ -321,7 +323,7 @@
     protected int getAlarmsTotalSilencePeopleSummary(int category) {
         if (category == NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES) {
             return R.string.zen_mode_none_messages;
-        } else if (category == NotificationManager.Policy.PRIORITY_CATEGORY_CALLS){
+        } else if (category == NotificationManager.Policy.PRIORITY_CATEGORY_CALLS) {
             return R.string.zen_mode_none_calls;
         } else if (category == NotificationManager.Policy.PRIORITY_CATEGORY_CONVERSATIONS) {
             return R.string.zen_mode_from_no_conversations;
@@ -470,8 +472,8 @@
         if (cursor != null && cursor.moveToFirst()) {
             do {
                 String contact = cursor.getString(0);
-                starredContacts.add(contact != null ? contact :
-                        mContext.getString(R.string.zen_mode_starred_contacts_empty_name));
+                int emptyNameId = R.string.zen_mode_starred_contacts_empty_name;
+                starredContacts.add(contact != null ? contact : mContext.getString(emptyNameId));
 
             } while (cursor.moveToNext());
         }
diff --git a/src/com/android/settings/notification/zen/ZenModeSliceBuilder.java b/src/com/android/settings/notification/zen/ZenModeSliceBuilder.java
index 4f6f058..d16b1e4 100644
--- a/src/com/android/settings/notification/zen/ZenModeSliceBuilder.java
+++ b/src/com/android/settings/notification/zen/ZenModeSliceBuilder.java
@@ -132,8 +132,8 @@
         final Uri contentUri = new Uri.Builder().appendPath(ZEN_MODE_SLICE_KEY).build();
         final String screenTitle = context.getText(R.string.zen_mode_settings_title).toString();
         return SliceBuilderUtils.buildSearchResultPageIntent(context,
-                ZenModeSettings.class.getName(), ZEN_MODE_SLICE_KEY, screenTitle,
-                SettingsEnums.NOTIFICATION_ZEN_MODE, R.string.menu_key_notifications)
+                        ZenModeSettings.class.getName(), ZEN_MODE_SLICE_KEY, screenTitle,
+                        SettingsEnums.NOTIFICATION_ZEN_MODE, R.string.menu_key_notifications)
                 .setClassName(context.getPackageName(), SubSettings.class.getName())
                 .setData(contentUri);
     }
diff --git a/src/com/android/settings/system/FactoryResetPreferenceController.java b/src/com/android/settings/system/FactoryResetPreferenceController.java
index df7cc3d..54c97a3 100644
--- a/src/com/android/settings/system/FactoryResetPreferenceController.java
+++ b/src/com/android/settings/system/FactoryResetPreferenceController.java
@@ -16,6 +16,7 @@
 package com.android.settings.system;
 
 import android.Manifest;
+import android.annotation.RequiresPermission;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageInfo;
@@ -37,8 +38,8 @@
 
     private static final String TAG = "FactoryResetPreference";
 
-    @VisibleForTesting
-    static final String ACTION_PREPARE_FACTORY_RESET =
+    @RequiresPermission(Manifest.permission.PREPARE_FACTORY_RESET)
+    public static final String ACTION_PREPARE_FACTORY_RESET =
             "com.android.settings.ACTION_PREPARE_FACTORY_RESET";
 
     private final UserManager mUm;
diff --git a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java
index ce5533e..fe90a2a 100644
--- a/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java
+++ b/src/com/android/settings/users/AddUserWhenLockedPreferenceController.java
@@ -42,9 +42,18 @@
         if (!isAvailable()) {
             restrictedSwitchPreference.setVisible(false);
         } else {
-            restrictedSwitchPreference.setDisabledByAdmin(
-                    mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null);
-            restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled);
+            if (android.multiuser.Flags.newMultiuserSettingsUx()) {
+                restrictedSwitchPreference.setVisible(true);
+                if (mUserCaps.mDisallowAddUserSetByAdmin) {
+                    restrictedSwitchPreference.setDisabledByAdmin(mUserCaps.mEnforcedAdmin);
+                } else if (mUserCaps.mDisallowAddUser) {
+                    restrictedSwitchPreference.setVisible(false);
+                }
+            } else {
+                restrictedSwitchPreference.setDisabledByAdmin(
+                        mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null);
+                restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled);
+            }
         }
     }
 
@@ -52,6 +61,8 @@
     public int getAvailabilityStatus() {
         if (!mUserCaps.isAdmin()) {
             return DISABLED_FOR_USER;
+        } else if (android.multiuser.Flags.newMultiuserSettingsUx()) {
+            return AVAILABLE;
         } else if (mUserCaps.disallowAddUser() || mUserCaps.disallowAddUserSetByAdmin()) {
             return DISABLED_FOR_USER;
         } else {
diff --git a/src/com/android/settings/users/GuestTelephonyPreferenceController.java b/src/com/android/settings/users/GuestTelephonyPreferenceController.java
index 4fbd449..e730cbf 100644
--- a/src/com/android/settings/users/GuestTelephonyPreferenceController.java
+++ b/src/com/android/settings/users/GuestTelephonyPreferenceController.java
@@ -19,12 +19,16 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
+import android.os.UserHandle;
 import android.os.UserManager;
 
 import androidx.preference.Preference;
 
 import com.android.settings.R;
 import com.android.settings.core.TogglePreferenceController;
+import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.RestrictedLockUtilsInternal;
+import com.android.settingslib.RestrictedSwitchPreference;
 
 /**
  * Controls the preference on the user settings screen which determines whether the guest user
@@ -43,10 +47,21 @@
 
     @Override
     public int getAvailabilityStatus() {
-        if (!mUserCaps.isAdmin() || !mUserCaps.mCanAddGuest) {
+        if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
+                || UserManager.isHeadlessSystemUserMode()) {
             return DISABLED_FOR_USER;
+        }
+        if (android.multiuser.Flags.newMultiuserSettingsUx()) {
+            if (!mUserCaps.isAdmin()) {
+                return DISABLED_FOR_USER;
+            }
+            return AVAILABLE;
         } else {
-            return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+            if (!mUserCaps.isAdmin() || !mUserCaps.mCanAddGuest) {
+                return DISABLED_FOR_USER;
+            } else {
+                return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+            }
         }
     }
 
@@ -74,8 +89,31 @@
     public void updateState(Preference preference) {
         super.updateState(preference);
         mUserCaps.updateAddUserCapabilities(mContext);
-        preference.setVisible(isAvailable() && mUserCaps.mUserSwitcherEnabled
-                && mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
-                && !UserManager.isHeadlessSystemUserMode());
+        final RestrictedSwitchPreference restrictedSwitchPreference =
+                (RestrictedSwitchPreference) preference;
+        restrictedSwitchPreference.setChecked(isChecked());
+        if (!isAvailable()) {
+            restrictedSwitchPreference.setVisible(false);
+        } else {
+            if (android.multiuser.Flags.newMultiuserSettingsUx()) {
+                restrictedSwitchPreference.setVisible(true);
+                final RestrictedLockUtils.EnforcedAdmin disallowRemoveUserAdmin =
+                        RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
+                                UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId());
+                if (disallowRemoveUserAdmin != null) {
+                    restrictedSwitchPreference.setDisabledByAdmin(disallowRemoveUserAdmin);
+                } else if (mUserCaps.mDisallowAddUserSetByAdmin) {
+                    restrictedSwitchPreference.setDisabledByAdmin(mUserCaps.mEnforcedAdmin);
+                } else if (mUserCaps.mDisallowAddUser) {
+                    // Adding user is restricted by system
+                    restrictedSwitchPreference.setVisible(false);
+                }
+            } else {
+                restrictedSwitchPreference.setDisabledByAdmin(
+                        mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null);
+                restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled
+                        && isAvailable());
+            }
+        }
     }
 }
diff --git a/src/com/android/settings/users/MultiUserSwitchBarController.java b/src/com/android/settings/users/MultiUserSwitchBarController.java
index f57b795..641ae51 100644
--- a/src/com/android/settings/users/MultiUserSwitchBarController.java
+++ b/src/com/android/settings/users/MultiUserSwitchBarController.java
@@ -57,11 +57,6 @@
             mSwitchBar.setDisabledByAdmin(RestrictedLockUtilsInternal
                     .checkIfRestrictionEnforced(mContext, UserManager.DISALLOW_USER_SWITCH,
                             UserHandle.myUserId()));
-
-        } else if (mUserCapabilities.mDisallowAddUser) {
-            mSwitchBar.setDisabledByAdmin(RestrictedLockUtilsInternal
-                    .checkIfRestrictionEnforced(mContext, UserManager.DISALLOW_ADD_USER,
-                            UserHandle.myUserId()));
         } else {
             mSwitchBar.setEnabled(mUserCapabilities.mIsMain);
         }
diff --git a/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java b/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java
index 01df5fd..345b506 100644
--- a/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java
+++ b/src/com/android/settings/users/RemoveGuestOnExitPreferenceController.java
@@ -22,6 +22,7 @@
 import android.content.pm.UserInfo;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
 import android.util.Log;
@@ -33,6 +34,8 @@
 import com.android.settings.R;
 import com.android.settings.core.BasePreferenceController;
 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.RestrictedLockUtilsInternal;
 import com.android.settingslib.RestrictedSwitchPreference;
 
 /**
@@ -70,9 +73,24 @@
         if (!isAvailable()) {
             restrictedSwitchPreference.setVisible(false);
         } else {
-            restrictedSwitchPreference.setDisabledByAdmin(
-                    mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null);
-            restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled);
+            if (android.multiuser.Flags.newMultiuserSettingsUx()) {
+                restrictedSwitchPreference.setVisible(true);
+                final RestrictedLockUtils.EnforcedAdmin disallowRemoveUserAdmin =
+                        RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
+                                UserManager.DISALLOW_REMOVE_USER, UserHandle.myUserId());
+                if (disallowRemoveUserAdmin != null) {
+                    restrictedSwitchPreference.setDisabledByAdmin(disallowRemoveUserAdmin);
+                } else if (mUserCaps.mDisallowAddUserSetByAdmin) {
+                    restrictedSwitchPreference.setDisabledByAdmin(mUserCaps.mEnforcedAdmin);
+                } else if (mUserCaps.mDisallowAddUser) {
+                    // Adding user is restricted by system
+                    restrictedSwitchPreference.setVisible(false);
+                }
+            } else {
+                restrictedSwitchPreference.setDisabledByAdmin(
+                        mUserCaps.disallowAddUser() ? mUserCaps.getEnforcedAdmin() : null);
+                restrictedSwitchPreference.setVisible(mUserCaps.mUserSwitcherEnabled);
+            }
         }
     }
 
@@ -82,14 +100,24 @@
         // then disable this controller
         // also disable this controller for non-admin users
         // also disable when config_guestUserAllowEphemeralStateChange is false
-        if (mUserManager.isGuestUserAlwaysEphemeral()
-                || !UserManager.isGuestUserAllowEphemeralStateChange()
-                || !mUserCaps.isAdmin()
-                || mUserCaps.disallowAddUser()
-                || mUserCaps.disallowAddUserSetByAdmin()) {
-            return DISABLED_FOR_USER;
+        if (android.multiuser.Flags.newMultiuserSettingsUx()) {
+            if (mUserManager.isGuestUserAlwaysEphemeral()
+                    || !UserManager.isGuestUserAllowEphemeralStateChange()
+                    || !mUserCaps.isAdmin()) {
+                return DISABLED_FOR_USER;
+            } else {
+                return AVAILABLE;
+            }
         } else {
-            return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+            if (mUserManager.isGuestUserAlwaysEphemeral()
+                    || !UserManager.isGuestUserAllowEphemeralStateChange()
+                    || !mUserCaps.isAdmin()
+                    || mUserCaps.disallowAddUser()
+                    || mUserCaps.disallowAddUserSetByAdmin()) {
+                return DISABLED_FOR_USER;
+            } else {
+                return mUserCaps.mUserSwitcherEnabled ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+            }
         }
     }
 
diff --git a/src/com/android/settings/users/UserDetailsSettings.java b/src/com/android/settings/users/UserDetailsSettings.java
index 71dd43f..588f01a 100644
--- a/src/com/android/settings/users/UserDetailsSettings.java
+++ b/src/com/android/settings/users/UserDetailsSettings.java
@@ -126,7 +126,11 @@
     @Override
     public void onResume() {
         super.onResume();
-        mSwitchUserPref.setEnabled(canSwitchUserNow());
+        if (android.multiuser.Flags.newMultiuserSettingsUx()) {
+            mSwitchUserPref.setEnabled(canSwitchUserNow() && mUserCaps.mUserSwitcherEnabled);
+        } else {
+            mSwitchUserPref.setEnabled(canSwitchUserNow());
+        }
         if (mUserInfo.isGuest() && mGuestUserAutoCreated) {
             mRemoveUserPref.setEnabled((mUserInfo.flags & UserInfo.FLAG_INITIALIZED) != 0);
         }
@@ -358,7 +362,12 @@
             mSwitchUserPref.setDisabledByAdmin(RestrictedLockUtilsInternal.getDeviceOwner(context));
         } else {
             mSwitchUserPref.setDisabledByAdmin(null);
-            mSwitchUserPref.setSelectable(true);
+            if (android.multiuser.Flags.newMultiuserSettingsUx()) {
+                mSwitchUserPref.setEnabled(mUserCaps.mUserSwitcherEnabled);
+                mSwitchUserPref.setSelectable(mUserCaps.mUserSwitcherEnabled);
+            } else {
+                mSwitchUserPref.setSelectable(true);
+            }
             mSwitchUserPref.setOnPreferenceClickListener(this);
         }
         if (mUserInfo.isMain() || mUserInfo.isGuest() || !UserManager.isMultipleAdminEnabled()
diff --git a/src/com/android/settings/users/UserSettings.java b/src/com/android/settings/users/UserSettings.java
index bf21c9b..0cf01e3 100644
--- a/src/com/android/settings/users/UserSettings.java
+++ b/src/com/android/settings/users/UserSettings.java
@@ -463,7 +463,8 @@
     @Override
     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
         int pos = 0;
-        if (!isCurrentUserAdmin() && canSwitchUserNow() && !isCurrentUserGuest()) {
+        if (!isCurrentUserAdmin() && (canSwitchUserNow() || Flags.newMultiuserSettingsUx())
+                && !isCurrentUserGuest()) {
             String nickname = mUserManager.getUserName();
             MenuItem removeThisUser = menu.add(0, MENU_REMOVE_USER, pos++,
                     getResources().getString(R.string.user_remove_user_menu, nickname));
@@ -1198,15 +1199,23 @@
         }
 
         List<UserInfo> users;
-        if (mUserCaps.mUserSwitcherEnabled) {
+        if (Flags.newMultiuserSettingsUx()) {
             // Only users that can be switched to should show up here.
             // e.g. Managed profiles appear under Accounts Settings instead
             users = mUserManager.getAliveUsers().stream()
                     .filter(UserInfo::supportsSwitchToByUser)
                     .collect(Collectors.toList());
         } else {
-            // Only current user will be displayed in case of multi-user switch is disabled
-            users = List.of(mUserManager.getUserInfo(context.getUserId()));
+            if (mUserCaps.mUserSwitcherEnabled) {
+                // Only users that can be switched to should show up here.
+                // e.g. Managed profiles appear under Accounts Settings instead
+                users = mUserManager.getAliveUsers().stream()
+                        .filter(UserInfo::supportsSwitchToByUser)
+                        .collect(Collectors.toList());
+            } else {
+                // Only current user will be displayed in case of multi-user switch is disabled
+                users = List.of(mUserManager.getUserInfo(context.getUserId()));
+            }
         }
 
         final ArrayList<Integer> missingIcons = new ArrayList<>();
@@ -1257,7 +1266,10 @@
                     pref.setSummary(R.string.user_summary_not_set_up);
                     // Disallow setting up user which results in user switching when the
                     // restriction is set.
-                    pref.setEnabled(!mUserCaps.mDisallowSwitchUser && canSwitchUserNow());
+                    // If newMultiuserSettingsUx flag is enabled, allow opening user details page
+                    // since switch to user will be disabled
+                    pref.setEnabled((!mUserCaps.mDisallowSwitchUser && canSwitchUserNow())
+                            || Flags.newMultiuserSettingsUx());
                 }
             } else if (user.isRestricted()) {
                 pref.setSummary(R.string.user_summary_restricted_profile);
@@ -1417,16 +1429,22 @@
                             getContext().getResources(), icon)));
             pref.setKey(KEY_USER_GUEST);
             pref.setOrder(Preference.DEFAULT_ORDER);
-            if (mUserCaps.mDisallowSwitchUser) {
+            if (mUserCaps.mDisallowSwitchUser && !Flags.newMultiuserSettingsUx()) {
                 pref.setDisabledByAdmin(
                         RestrictedLockUtilsInternal.getDeviceOwner(context));
             } else {
                 pref.setDisabledByAdmin(null);
             }
-            if (mUserCaps.mUserSwitcherEnabled) {
+            if (Flags.newMultiuserSettingsUx()) {
                 mGuestUserCategory.addPreference(pref);
                 // guest user preference is shown hence also make guest category visible
                 mGuestUserCategory.setVisible(true);
+            } else {
+                if (mUserCaps.mUserSwitcherEnabled) {
+                    mGuestUserCategory.addPreference(pref);
+                    // guest user preference is shown hence also make guest category visible
+                    mGuestUserCategory.setVisible(true);
+                }
             }
             isGuestAlreadyCreated = true;
         }
@@ -1450,10 +1468,11 @@
 
     private boolean updateAddGuestPreference(Context context, boolean isGuestAlreadyCreated) {
         boolean isVisible = false;
-        if (!isGuestAlreadyCreated && mUserCaps.mCanAddGuest
+        if (!isGuestAlreadyCreated && (mUserCaps.mCanAddGuest
+                || (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUser))
                 && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_GUEST)
                 && WizardManagerHelper.isDeviceProvisioned(context)
-                && mUserCaps.mUserSwitcherEnabled) {
+                && (mUserCaps.mUserSwitcherEnabled || Flags.newMultiuserSettingsUx())) {
             Drawable icon = context.getDrawable(
                     com.android.settingslib.R.drawable.ic_account_circle);
             mAddGuest.setIcon(centerAndTint(icon));
@@ -1466,7 +1485,25 @@
                 mAddGuest.setEnabled(false);
             } else {
                 mAddGuest.setTitle(com.android.settingslib.R.string.guest_new_guest);
-                mAddGuest.setEnabled(canSwitchUserNow());
+                if (Flags.newMultiuserSettingsUx()
+                        && mUserCaps.mDisallowAddUserSetByAdmin) {
+                    mAddGuest.setDisabledByAdmin(mUserCaps.mEnforcedAdmin);
+                } else if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUser) {
+                    final List<UserManager.EnforcingUser> enforcingUsers =
+                            mUserManager.getUserRestrictionSources(UserManager.DISALLOW_ADD_USER,
+                                    UserHandle.of(UserHandle.myUserId()));
+                    if (!enforcingUsers.isEmpty()) {
+                        final UserManager.EnforcingUser enforcingUser = enforcingUsers.get(0);
+                        final int restrictionSource = enforcingUser.getUserRestrictionSource();
+                        if (restrictionSource == UserManager.RESTRICTION_SOURCE_SYSTEM) {
+                            mAddGuest.setEnabled(false);
+                        } else {
+                            mAddGuest.setVisible(false);
+                        }
+                    }
+                } else {
+                    mAddGuest.setEnabled(canSwitchUserNow() || Flags.newMultiuserSettingsUx());
+                }
             }
         } else {
             mAddGuest.setVisible(false);
@@ -1494,16 +1531,18 @@
 
     private void updateAddUserCommon(Context context, RestrictedPreference addUser,
             boolean canAddRestrictedProfile) {
-        if ((mUserCaps.mCanAddUser && !mUserCaps.mDisallowAddUserSetByAdmin)
+        if ((mUserCaps.mCanAddUser
+                && !(mUserCaps.mDisallowAddUserSetByAdmin && Flags.newMultiuserSettingsUx()))
                 && WizardManagerHelper.isDeviceProvisioned(context)
-                && mUserCaps.mUserSwitcherEnabled) {
+                && (mUserCaps.mUserSwitcherEnabled || Flags.newMultiuserSettingsUx())) {
             addUser.setVisible(true);
             addUser.setSelectable(true);
             final boolean canAddMoreUsers =
                     mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY)
                             || (canAddRestrictedProfile
                             && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_RESTRICTED));
-            addUser.setEnabled(canAddMoreUsers && !mAddingUser && canSwitchUserNow());
+            addUser.setEnabled(canAddMoreUsers && !mAddingUser
+                    && (canSwitchUserNow() || Flags.newMultiuserSettingsUx()));
 
             if (!canAddMoreUsers) {
                 addUser.setSummary(getString(R.string.user_add_max_count));
@@ -1514,6 +1553,23 @@
                 addUser.setDisabledByAdmin(
                         mUserCaps.mDisallowAddUser ? mUserCaps.mEnforcedAdmin : null);
             }
+        } else if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUserSetByAdmin) {
+            addUser.setVisible(true);
+            addUser.setDisabledByAdmin(mUserCaps.mEnforcedAdmin);
+        } else if (Flags.newMultiuserSettingsUx() && mUserCaps.mDisallowAddUser) {
+            final List<UserManager.EnforcingUser> enforcingUsers =
+                    mUserManager.getUserRestrictionSources(UserManager.DISALLOW_ADD_USER,
+                            UserHandle.of(UserHandle.myUserId()));
+            if (!enforcingUsers.isEmpty()) {
+                final UserManager.EnforcingUser enforcingUser = enforcingUsers.get(0);
+                final int restrictionSource = enforcingUser.getUserRestrictionSource();
+                if (restrictionSource == UserManager.RESTRICTION_SOURCE_SYSTEM) {
+                    addUser.setVisible(true);
+                    addUser.setEnabled(false);
+                } else {
+                    addUser.setVisible(false);
+                }
+            }
         } else {
             addUser.setVisible(false);
         }
diff --git a/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java b/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java
index 82537d4..e5581d3 100644
--- a/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java
+++ b/src/com/android/settings/wifi/calling/WifiCallingSettingsForSub.java
@@ -33,7 +33,6 @@
 import android.telephony.TelephonyManager;
 import android.telephony.ims.ImsManager;
 import android.telephony.ims.ImsMmTelManager;
-import android.telephony.ims.ProvisioningManager;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -42,12 +41,14 @@
 import android.widget.CompoundButton;
 import android.widget.CompoundButton.OnCheckedChangeListener;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appcompat.app.AlertDialog;
+import androidx.lifecycle.LifecycleOwner;
 import androidx.preference.Preference;
 import androidx.preference.Preference.OnPreferenceClickListener;
 import androidx.preference.PreferenceScreen;
 
-import com.android.ims.ImsConfig;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.flags.Flags;
@@ -57,8 +58,12 @@
 import com.android.settings.Utils;
 import com.android.settings.core.SubSettingLauncher;
 import com.android.settings.network.ims.WifiCallingQueryImsState;
+import com.android.settings.network.telephony.wificalling.IWifiCallingRepository;
+import com.android.settings.network.telephony.wificalling.WifiCallingRepository;
 import com.android.settings.widget.SettingsMainSwitchPreference;
 
+import kotlin.Unit;
+
 import java.util.List;
 
 /**
@@ -103,7 +108,6 @@
 
     private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
     private ImsMmTelManager mImsMmTelManager;
-    private ProvisioningManager mProvisioningManager;
     private TelephonyManager mTelephonyManager;
 
     private PhoneTelephonyCallback mTelephonyCallback;
@@ -188,19 +192,6 @@
                 return true;
             };
 
-    private final ProvisioningManager.Callback mProvisioningCallback =
-            new ProvisioningManager.Callback() {
-                @Override
-                public void onProvisioningIntChanged(int item, int value) {
-                    if (item == ImsConfig.ConfigConstants.VOICE_OVER_WIFI_SETTING_ENABLED
-                            || item == ImsConfig.ConfigConstants.VLT_SETTING_ENABLED) {
-                        // The provisioning policy might have changed. Update the body to make sure
-                        // this change takes effect if needed.
-                        updateBody();
-                    }
-                }
-            };
-
     @VisibleForTesting
     void showAlert(Intent intent) {
         final Context context = getActivity();
@@ -264,14 +255,6 @@
     }
 
     @VisibleForTesting
-    ProvisioningManager getImsProvisioningManager() {
-        if (!SubscriptionManager.isValidSubscriptionId(mSubId)) {
-            return null;
-        }
-        return ProvisioningManager.createForSubscriptionId(mSubId);
-    }
-
-    @VisibleForTesting
     ImsMmTelManager getImsMmTelManager() {
         if (!SubscriptionManager.isValidSubscriptionId(mSubId)) {
             return null;
@@ -294,7 +277,6 @@
                     FRAGMENT_BUNDLE_SUBID, SubscriptionManager.INVALID_SUBSCRIPTION_ID);
         }
 
-        mProvisioningManager = getImsProvisioningManager();
         mImsMmTelManager = getImsMmTelManager();
 
         mSwitchBar = (SettingsMainSwitchPreference) findPreference(SWITCH_BAR);
@@ -336,19 +318,33 @@
         return view;
     }
 
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        getWifiCallingRepository().collectIsWifiCallingReadyFlow(
+                getLifecycleOwner(), (isWifiWifiCallingReadyFlow) -> {
+                    if (!isWifiWifiCallingReadyFlow) {
+                        // This screen is not allowed to be shown due to provisioning policy and
+                        // should therefore be closed.
+                        finish();
+                    }
+                    return Unit.INSTANCE;
+                });
+    }
+
     @VisibleForTesting
-    boolean isWfcProvisionedOnDevice() {
-        return queryImsState(mSubId).isWifiCallingProvisioned();
+    @NonNull
+    IWifiCallingRepository getWifiCallingRepository() {
+        return new WifiCallingRepository(requireContext(), mSubId);
+    }
+
+    @VisibleForTesting
+    @NonNull
+    LifecycleOwner getLifecycleOwner() {
+        return getViewLifecycleOwner();
     }
 
     private void updateBody() {
-        if (!isWfcProvisionedOnDevice()) {
-            // This screen is not allowed to be shown due to provisioning policy and should
-            // therefore be closed.
-            finish();
-            return;
-        }
-
         final CarrierConfigManager configManager = (CarrierConfigManager)
                 getSystemService(Context.CARRIER_CONFIG_SERVICE);
         boolean isWifiOnlySupported = true;
@@ -448,8 +444,6 @@
         if (intent.getBooleanExtra(Phone.EXTRA_KEY_ALERT_SHOW, false)) {
             showAlert(intent);
         }
-        // Register callback for provisioning changes.
-        registerProvisioningChangedCallback();
     }
 
     @Override
@@ -462,8 +456,6 @@
             mSwitchBar.removeOnSwitchChangeListener(this);
         }
         context.unregisterReceiver(mIntentReceiver);
-        // Remove callback for provisioning changes.
-        unregisterProvisioningChangedCallback();
     }
 
     /**
@@ -699,27 +691,6 @@
         return SubscriptionManager.getResourcesForSubId(getContext(), mSubId);
     }
 
-    @VisibleForTesting
-    void registerProvisioningChangedCallback() {
-        if (mProvisioningManager == null) {
-            return;
-        }
-        try {
-            mProvisioningManager.registerProvisioningChangedCallback(getContext().getMainExecutor(),
-                    mProvisioningCallback);
-        } catch (Exception ex) {
-            Log.w(TAG, "onResume: Unable to register callback for provisioning changes.");
-        }
-    }
-
-    @VisibleForTesting
-    void unregisterProvisioningChangedCallback() {
-        if (mProvisioningManager == null) {
-            return;
-        }
-        mProvisioningManager.unregisterProvisioningChangedCallback(mProvisioningCallback);
-    }
-
     /**
      * Determine whether to override roaming Wi-Fi calling preference when device is connected to
      * non-terrestrial network.
diff --git a/tests/Enable16KbTests/Android.bp b/tests/Enable16KbTests/Android.bp
index 781ea8f..fa05d33 100644
--- a/tests/Enable16KbTests/Android.bp
+++ b/tests/Enable16KbTests/Android.bp
@@ -33,7 +33,6 @@
     ],
     platform_apis: true,
     certificate: "platform",
-    test_suites: ["device-tests"],
     libs: [
         "android.test.runner",
         "android.test.base",
@@ -57,6 +56,6 @@
     data: [
         ":test_16kb_app",
     ],
-    test_suites: ["device-tests"],
+    test_suites: ["general-tests"],
     test_config: "AndroidTest.xml",
 }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java
index d28ab3b..5a9f2bc 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java
@@ -17,6 +17,8 @@
 
 import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE;
 import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+import static com.android.settings.flags.Flags.FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING;
+import static com.android.settings.flags.Flags.FLAG_ROTATION_CONNECTED_DISPLAY_SETTING;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -30,6 +32,7 @@
 import android.bluetooth.BluetoothDevice;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.hardware.input.InputManager;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
@@ -40,13 +43,16 @@
 import androidx.preference.PreferenceGroup;
 import androidx.preference.PreferenceManager;
 import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
 
 import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater;
 import com.android.settings.bluetooth.Utils;
+import com.android.settings.connecteddevice.display.ExternalDisplayUpdater;
 import com.android.settings.connecteddevice.dock.DockUpdater;
 import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater;
 import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater;
 import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.flags.FakeFeatureFlagsImpl;
 import com.android.settings.flags.Flags;
 import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
 import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
@@ -65,7 +71,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
 import org.robolectric.Shadows;
 import org.robolectric.annotation.Config;
 import org.robolectric.shadows.ShadowApplicationPackageManager;
@@ -84,6 +89,8 @@
     @Mock
     private DashboardFragment mDashboardFragment;
     @Mock
+    private ExternalDisplayUpdater mExternalDisplayUpdater;
+    @Mock
     private ConnectedBluetoothDeviceUpdater mConnectedBluetoothDeviceUpdater;
     @Mock
     private ConnectedUsbDeviceUpdater mConnectedUsbDeviceUpdater;
@@ -105,6 +112,9 @@
     private CachedBluetoothDevice mCachedDevice;
     @Mock
     private BluetoothDevice mDevice;
+    @Mock
+    private Resources mResources;
+    private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl();
 
     private ShadowApplicationPackageManager mPackageManager;
     private PreferenceGroup mPreferenceGroup;
@@ -118,8 +128,10 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        mFakeFeatureFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, true);
+        mFakeFeatureFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, true);
 
-        mContext = spy(RuntimeEnvironment.application);
+        mContext = spy(ApplicationProvider.getApplicationContext());
         mPreference = new Preference(mContext);
         mPreference.setKey(PREFERENCE_KEY_1);
         mPackageManager = (ShadowApplicationPackageManager) Shadows.shadowOf(
@@ -129,15 +141,19 @@
         doReturn(mContext).when(mDashboardFragment).getContext();
         mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true);
         when(mContext.getSystemService(InputManager.class)).thenReturn(mInputManager);
+        when(mContext.getResources()).thenReturn(mResources);
         when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{});
 
         ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
         mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
         when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
 
-        mConnectedDeviceGroupController = new ConnectedDeviceGroupController(mContext);
-        mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater,
-                mConnectedUsbDeviceUpdater, mConnectedDockUpdater, mStylusDeviceUpdater);
+        mConnectedDeviceGroupController = spy(new ConnectedDeviceGroupController(mContext));
+        when(mConnectedDeviceGroupController.getFeatureFlags()).thenReturn(mFakeFeatureFlags);
+
+        mConnectedDeviceGroupController.init(mExternalDisplayUpdater,
+                mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater, mConnectedDockUpdater,
+                mStylusDeviceUpdater);
         mConnectedDeviceGroupController.mPreferenceGroup = mPreferenceGroup;
 
         when(mCachedDevice.getName()).thenReturn(DEVICE_NAME);
@@ -147,6 +163,7 @@
 
         FeatureFlagUtils.setEnabled(mContext, FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES,
                 true);
+        when(mPreferenceScreen.getContext()).thenReturn(mContext);
     }
 
     @Test
@@ -193,6 +210,7 @@
         // register the callback in onStart()
         mConnectedDeviceGroupController.onStart();
 
+        verify(mExternalDisplayUpdater).registerCallback();
         verify(mConnectedBluetoothDeviceUpdater).registerCallback();
         verify(mConnectedUsbDeviceUpdater).registerCallback();
         verify(mConnectedDockUpdater).registerCallback();
@@ -204,6 +222,7 @@
     public void onStop_shouldUnregisterUpdaters() {
         // unregister the callback in onStop()
         mConnectedDeviceGroupController.onStop();
+        verify(mExternalDisplayUpdater).unregisterCallback();
         verify(mConnectedBluetoothDeviceUpdater).unregisterCallback();
         verify(mConnectedUsbDeviceUpdater).unregisterCallback();
         verify(mConnectedDockUpdater).unregisterCallback();
@@ -212,10 +231,12 @@
 
     @Test
     public void getAvailabilityStatus_noBluetoothUsbDockFeature_returnUnSupported() {
+        mFakeFeatureFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, false);
+        mFakeFeatureFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, false);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false);
-        mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater,
+        mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater,
                 mConnectedUsbDeviceUpdater, null, null);
 
         assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo(
@@ -223,11 +244,23 @@
     }
 
     @Test
+    public void getAvailabilityStatus_connectedDisplay_returnSupported() {
+        mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false);
+        mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false);
+        mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false);
+        mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater,
+                mConnectedUsbDeviceUpdater, null, null);
+
+        assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo(
+                AVAILABLE_UNSEARCHABLE);
+    }
+
+    @Test
     public void getAvailabilityStatus_BluetoothFeature_returnSupported() {
         mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false);
-        mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater,
+        mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater,
                 mConnectedUsbDeviceUpdater, null, null);
 
         assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo(
@@ -239,7 +272,7 @@
         mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, true);
-        mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater,
+        mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater,
                 mConnectedUsbDeviceUpdater, null, null);
 
         assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo(
@@ -251,7 +284,7 @@
         mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false);
-        mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater,
+        mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater,
                 mConnectedUsbDeviceUpdater, mConnectedDockUpdater, null);
 
         assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo(
@@ -261,6 +294,8 @@
 
     @Test
     public void getAvailabilityStatus_noUsiStylusFeature_returnUnSupported() {
+        mFakeFeatureFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, false);
+        mFakeFeatureFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, false);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false);
         mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false);
@@ -268,7 +303,7 @@
         when(mInputManager.getInputDevice(0)).thenReturn(new InputDevice.Builder().setSources(
                 InputDevice.SOURCE_DPAD).setExternal(false).build());
 
-        mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater,
+        mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater,
                 mConnectedUsbDeviceUpdater, null, mStylusDeviceUpdater);
 
         assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo(
@@ -284,7 +319,7 @@
         when(mInputManager.getInputDevice(0)).thenReturn(new InputDevice.Builder().setSources(
                 InputDevice.SOURCE_STYLUS).setExternal(false).build());
 
-        mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater,
+        mConnectedDeviceGroupController.init(null, mConnectedBluetoothDeviceUpdater,
                 mConnectedUsbDeviceUpdater, mConnectedDockUpdater, mStylusDeviceUpdater);
 
         assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo(
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceControllerTest.java
new file mode 100644
index 0000000..618e021
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceControllerTest.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.settings.SettingsEnums;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothLeBroadcast;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.VolumeControlProfile;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowLooper;
+
+import java.util.concurrent.Executor;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+        })
+public class AudioSharingNamePreferenceControllerTest {
+    private static final String PREF_KEY = "audio_sharing_stream_name";
+    private static final String BROADCAST_NAME = "broadcast_name";
+    private static final CharSequence UPDATED_NAME = "updated_name";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Spy Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
+    @Mock private VolumeControlProfile mVolumeControl;
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private BluetoothEventManager mEventManager;
+    @Mock private LocalBluetoothProfileManager mProfileManager;
+    @Mock private PreferenceScreen mScreen;
+    private AudioSharingNamePreferenceController mController;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private Lifecycle mLifecycle;
+    private LifecycleOwner mLifecycleOwner;
+    private AudioSharingNamePreference mPreference;
+    private FakeFeatureFactory mFeatureFactory;
+
+    @Before
+    public void setUp() {
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        mLocalBtManager = Utils.getLocalBtManager(mContext);
+        when(mLocalBtManager.getEventManager()).thenReturn(mEventManager);
+        when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager);
+        when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
+        when(mProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
+        when(mBroadcast.isProfileReady()).thenReturn(true);
+        when(mAssistant.isProfileReady()).thenReturn(true);
+        when(mVolumeControl.isProfileReady()).thenReturn(true);
+        when(mBroadcast.isProfileReady()).thenReturn(true);
+        mFeatureFactory = FakeFeatureFactory.setupForTest();
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        mController = new AudioSharingNamePreferenceController(mContext, PREF_KEY);
+        mPreference = spy(new AudioSharingNamePreference(mContext));
+        when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOn_available() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOff_unsupported() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+    }
+
+    @Test
+    public void onStart_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mBroadcast, never())
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
+    }
+
+    @Test
+    public void onStart_flagOn_registerCallbacks() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mBroadcast)
+                .registerServiceCallBack(
+                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
+    }
+
+    @Test
+    public void onStart_flagOn_serviceNotReady_registerCallbacks() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.isProfileReady()).thenReturn(false);
+        mController.onStart(mLifecycleOwner);
+        verify(mProfileManager)
+                .addServiceListener(any(LocalBluetoothProfileManager.ServiceListener.class));
+    }
+
+    @Test
+    public void onServiceConnected_removeCallbacks() {
+        mController.onServiceConnected();
+        verify(mProfileManager)
+                .removeServiceListener(any(LocalBluetoothProfileManager.ServiceListener.class));
+    }
+
+    @Test
+    public void onStop_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        mController.onStop(mLifecycleOwner);
+        verify(mBroadcast, never())
+                .unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
+    }
+
+    @Test
+    public void onStop_flagOn_unregisterCallbacks() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        mController.onStop(mLifecycleOwner);
+        verify(mBroadcast).unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
+    }
+
+    @Test
+    public void displayPreference_updateName_showIcon() {
+        when(mBroadcast.getBroadcastName()).thenReturn(BROADCAST_NAME);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        mController.displayPreference(mScreen);
+        ShadowLooper.idleMainLooper();
+
+        assertThat(mPreference.getText()).isEqualTo(BROADCAST_NAME);
+        assertThat(mPreference.getSummary()).isEqualTo(BROADCAST_NAME);
+        verify(mPreference).setValidator(any());
+        verify(mPreference).setShowQrCodeIcon(true);
+    }
+
+    @Test
+    public void displayPreference_updateName_hideIcon() {
+        when(mBroadcast.getBroadcastName()).thenReturn(BROADCAST_NAME);
+        when(mBroadcast.isEnabled(any())).thenReturn(false);
+        mController.displayPreference(mScreen);
+        ShadowLooper.idleMainLooper();
+
+        assertThat(mPreference.getText()).isEqualTo(BROADCAST_NAME);
+        assertThat(mPreference.getSummary()).isEqualTo(BROADCAST_NAME);
+        verify(mPreference).setValidator(any());
+        verify(mPreference).setShowQrCodeIcon(false);
+    }
+
+    @Test
+    public void onPreferenceChange_noChange_doNothing() {
+        when(mPreference.getSummary()).thenReturn(BROADCAST_NAME);
+        mController.displayPreference(mScreen);
+        boolean changed = mController.onPreferenceChange(mPreference, BROADCAST_NAME);
+        ShadowLooper.idleMainLooper();
+
+        verify(mBroadcast, never()).setBroadcastName(anyString());
+        verify(mBroadcast, never()).setProgramInfo(anyString());
+        verify(mBroadcast, never()).updateBroadcast();
+        verify(mFeatureFactory.metricsFeatureProvider, never()).action(any(), anyInt(), anyInt());
+
+        assertThat(changed).isFalse();
+    }
+
+    @Test
+    public void onPreferenceChange_changed_updateName_broadcasting() {
+        when(mPreference.getSummary()).thenReturn(BROADCAST_NAME);
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        mController.displayPreference(mScreen);
+        boolean changed = mController.onPreferenceChange(mPreference, UPDATED_NAME);
+        ShadowLooper.idleMainLooper();
+
+        verify(mBroadcast).setBroadcastName(UPDATED_NAME.toString());
+        verify(mBroadcast).setProgramInfo(UPDATED_NAME.toString());
+        verify(mBroadcast).updateBroadcast();
+        verify(mFeatureFactory.metricsFeatureProvider)
+                .action(mContext, SettingsEnums.ACTION_AUDIO_STREAM_NAME_UPDATED, 1);
+        assertThat(changed).isTrue();
+    }
+
+    @Test
+    public void onPreferenceChange_changed_updateName_notBroadcasting() {
+        when(mPreference.getSummary()).thenReturn(BROADCAST_NAME);
+        when(mBroadcast.isEnabled(any())).thenReturn(false);
+        mController.displayPreference(mScreen);
+        boolean changed = mController.onPreferenceChange(mPreference, UPDATED_NAME);
+        ShadowLooper.idleMainLooper();
+
+        verify(mBroadcast).setBroadcastName(UPDATED_NAME.toString());
+        verify(mBroadcast).setProgramInfo(UPDATED_NAME.toString());
+        verify(mBroadcast, never()).updateBroadcast();
+        verify(mFeatureFactory.metricsFeatureProvider)
+                .action(mContext, SettingsEnums.ACTION_AUDIO_STREAM_NAME_UPDATED, 0);
+        assertThat(changed).isTrue();
+    }
+
+    @Test
+    public void unrelatedCallbacks_doNotUpdateIcon() {
+        mController.displayPreference(mScreen);
+        mController.mBroadcastCallback.onBroadcastStartFailed(/* reason= */ 0);
+        mController.mBroadcastCallback.onBroadcastStarted(/* reason= */ 0, /* broadcastId= */ 0);
+        mController.mBroadcastCallback.onBroadcastStopFailed(/* reason= */ 0);
+        mController.mBroadcastCallback.onBroadcastUpdateFailed(
+                /* reason= */ 0, /* broadcastId= */ 0);
+        mController.mBroadcastCallback.onBroadcastUpdated(/* reason= */ 0, /* broadcastId= */ 0);
+        mController.mBroadcastCallback.onPlaybackStarted(/* reason= */ 0, /* broadcastId= */ 0);
+        mController.mBroadcastCallback.onPlaybackStopped(/* reason= */ 0, /* broadcastId= */ 0);
+
+        ShadowLooper.idleMainLooper();
+        // Should be called once in displayPreference, but not called after callbacks
+        verify(mPreference).setShowQrCodeIcon(anyBoolean());
+    }
+
+    @Test
+    public void broadcastOnCallback_updateIcon() {
+        mController.displayPreference(mScreen);
+        mController.mBroadcastCallback.onBroadcastMetadataChanged(
+                /* broadcastId= */ 0, mock(BluetoothLeBroadcastMetadata.class));
+
+        ShadowLooper.idleMainLooper();
+
+        // Should be called twice, in displayPreference and also after callback
+        verify(mPreference, times(2)).setShowQrCodeIcon(anyBoolean());
+    }
+
+    @Test
+    public void broadcastStopCallback_updateIcon() {
+        mController.displayPreference(mScreen);
+        mController.mBroadcastCallback.onBroadcastStopped(/* reason= */ 0, /* broadcastId= */ 0);
+
+        ShadowLooper.idleMainLooper();
+
+        // Should be called twice, in displayPreference and also after callback
+        verify(mPreference, times(2)).setShowQrCodeIcon(anyBoolean());
+    }
+
+    @Test
+    public void idTextValid_emptyString() {
+        boolean valid = mController.isTextValid("");
+
+        assertThat(valid).isFalse();
+    }
+
+    @Test
+    public void idTextValid_validName() {
+        boolean valid = mController.isTextValid("valid name");
+
+        assertThat(valid).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceTest.java
new file mode 100644
index 0000000..13e2a9d
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNamePreferenceTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.content.Intent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import androidx.preference.PreferenceViewHolder;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioSharingNamePreferenceTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private Context mContext;
+    private AudioSharingNamePreference mPreference;
+
+    @Before
+    public void setup() {
+        mContext = ApplicationProvider.getApplicationContext();
+        mPreference = spy(new AudioSharingNamePreference(mContext, null));
+    }
+
+    @Test
+    public void initialize_correctLayout() {
+        assertThat(mPreference.getLayoutResource())
+                .isEqualTo(
+                        com.android.settingslib.widget.preference.twotarget.R.layout
+                                .preference_two_target);
+        assertThat(mPreference.getWidgetLayoutResource())
+                .isEqualTo(R.layout.preference_widget_qrcode);
+    }
+
+    @Test
+    public void onBindViewHolder_correctLayout_noQrCodeButton() {
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        View view = inflater.inflate(mPreference.getLayoutResource(), null);
+        LinearLayout widgetView = view.findViewById(android.R.id.widget_frame);
+        assertThat(widgetView).isNotNull();
+        inflater.inflate(mPreference.getWidgetLayoutResource(), widgetView, true);
+
+        var holder = PreferenceViewHolder.createInstanceForTests(view);
+        mPreference.setShowQrCodeIcon(false);
+        mPreference.onBindViewHolder(holder);
+
+        ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
+        View divider =
+                holder.findViewById(
+                        com.android.settingslib.widget.preference.twotarget.R.id
+                                .two_target_divider);
+
+        assertThat(shareButton).isNotNull();
+        assertThat(shareButton.getVisibility()).isEqualTo(View.GONE);
+        assertThat(shareButton.hasOnClickListeners()).isFalse();
+        assertThat(divider).isNotNull();
+        assertThat(divider.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void onBindViewHolder_correctLayout_showQrCodeButton() {
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        View view = inflater.inflate(mPreference.getLayoutResource(), null);
+        LinearLayout widgetView = view.findViewById(android.R.id.widget_frame);
+        assertThat(widgetView).isNotNull();
+        inflater.inflate(mPreference.getWidgetLayoutResource(), widgetView, true);
+
+        var holder = PreferenceViewHolder.createInstanceForTests(view);
+        mPreference.setShowQrCodeIcon(true);
+        mPreference.onBindViewHolder(holder);
+
+        ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
+        View divider =
+                holder.findViewById(
+                        com.android.settingslib.widget.preference.twotarget.R.id
+                                .two_target_divider);
+
+        assertThat(shareButton).isNotNull();
+        assertThat(shareButton.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(shareButton.getDrawable()).isNotNull();
+        assertThat(shareButton.hasOnClickListeners()).isTrue();
+        assertThat(divider).isNotNull();
+        assertThat(divider.getVisibility()).isEqualTo(View.VISIBLE);
+
+        // mContext is not an Activity context, calling startActivity() from outside of an Activity
+        // context requires the FLAG_ACTIVITY_NEW_TASK flag, create a mock to avoid this
+        // AndroidRuntimeException.
+        Context activityContext = mock(Context.class);
+        when(mPreference.getContext()).thenReturn(activityContext);
+        shareButton.callOnClick();
+
+        ArgumentCaptor<Intent> argumentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(activityContext).startActivity(argumentCaptor.capture());
+
+        Intent intent = argumentCaptor.getValue();
+        assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
+                .isEqualTo(AudioStreamsQrCodeFragment.class.getName());
+        assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0))
+                .isEqualTo(R.string.audio_streams_qr_code_page_title);
+        assertThat(intent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 0))
+                .isEqualTo(SettingsEnums.AUDIO_SHARING_SETTINGS);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidatorTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidatorTest.java
new file mode 100644
index 0000000..ada6117
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingNameTextValidatorTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioSharingNameTextValidatorTest {
+    private AudioSharingNameTextValidator mValidator;
+
+    @Before
+    public void setUp() {
+        mValidator = new AudioSharingNameTextValidator();
+    }
+
+    @Test
+    public void testValidNames() {
+        assertThat(mValidator.isTextValid("ValidName")).isTrue();
+        assertThat(mValidator.isTextValid("12345678")).isTrue();
+        assertThat(mValidator.isTextValid("Name_With_Underscores")).isTrue();
+        assertThat(mValidator.isTextValid("ÄÖÜß")).isTrue();
+        assertThat(mValidator.isTextValid("ThisNameIsExactly32Characters!")).isTrue();
+    }
+
+    @Test
+    public void testInvalidNames() {
+        assertThat(mValidator.isTextValid(null)).isFalse();
+        assertThat(mValidator.isTextValid("")).isFalse();
+        assertThat(mValidator.isTextValid("abc")).isFalse();
+        assertThat(mValidator.isTextValid("ThisNameIsWayTooLongForAnAudioSharingName")).isFalse();
+        assertThat(mValidator.isTextValid("Invalid\uDC00")).isFalse();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceControllerTest.java
new file mode 100644
index 0000000..5bfb966
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceControllerTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.settings.SettingsEnums;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.bluetooth.Utils;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowLooper;
+
+import java.nio.charset.StandardCharsets;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothAdapter.class,
+            ShadowBluetoothUtils.class,
+        })
+public class AudioSharingPasswordPreferenceControllerTest {
+    private static final String PREF_KEY = "audio_sharing_stream_password";
+    private static final String SHARED_PREF_KEY = "default_password";
+    private static final String BROADCAST_PASSWORD = "password";
+    private static final String EDITTEXT_PASSWORD = "edittext_password";
+    private static final String HIDDEN_PASSWORD = "********";
+
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+    @Spy Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private LocalBluetoothLeBroadcast mBroadcast;
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private LocalBluetoothProfileManager mProfileManager;
+    @Mock private SharedPreferences mSharedPreferences;
+    @Mock private SharedPreferences.Editor mEditor;
+    @Mock private ContentResolver mContentResolver;
+    @Mock private PreferenceScreen mScreen;
+    private AudioSharingPasswordPreferenceController mController;
+    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
+    private Lifecycle mLifecycle;
+    private LifecycleOwner mLifecycleOwner;
+    private AudioSharingPasswordPreference mPreference;
+    private FakeFeatureFactory mFeatureFactory;
+
+    @Before
+    public void setUp() {
+        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+        mShadowBluetoothAdapter.setEnabled(true);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
+                BluetoothStatusCodes.FEATURE_SUPPORTED);
+        mLocalBtManager = Utils.getLocalBtManager(mContext);
+        when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager);
+        when(mProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+        mFeatureFactory = FakeFeatureFactory.setupForTest();
+        when(mContext.getContentResolver()).thenReturn(mContentResolver);
+        when(mContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mSharedPreferences);
+        when(mSharedPreferences.edit()).thenReturn(mEditor);
+        when(mEditor.putString(anyString(), anyString())).thenReturn(mEditor);
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        mController = new AudioSharingPasswordPreferenceController(mContext, PREF_KEY);
+        mPreference = spy(new AudioSharingPasswordPreference(mContext));
+        when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOn_available() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagOff_unsupported() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
+    }
+
+    @Test
+    public void onStart_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mContentResolver, never()).registerContentObserver(any(), anyBoolean(), any());
+        verify(mSharedPreferences, never()).registerOnSharedPreferenceChangeListener(any());
+    }
+
+    @Test
+    public void onStart_flagOn_registerCallbacks() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStart(mLifecycleOwner);
+        verify(mContentResolver).registerContentObserver(any(), anyBoolean(), any());
+        verify(mSharedPreferences).registerOnSharedPreferenceChangeListener(any());
+    }
+
+    @Test
+    public void onStop_flagOff_doNothing() {
+        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStop(mLifecycleOwner);
+        verify(mContentResolver, never()).unregisterContentObserver(any());
+        verify(mSharedPreferences, never()).unregisterOnSharedPreferenceChangeListener(any());
+    }
+
+    @Test
+    public void onStop_flagOn_registerCallbacks() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        mController.onStop(mLifecycleOwner);
+        verify(mContentResolver).unregisterContentObserver(any());
+        verify(mSharedPreferences).unregisterOnSharedPreferenceChangeListener(any());
+    }
+
+    @Test
+    public void displayPreference_setupPreference_noPassword() {
+        when(mSharedPreferences.getString(anyString(), anyString())).thenReturn(EDITTEXT_PASSWORD);
+        when(mBroadcast.getBroadcastCode()).thenReturn(new byte[] {});
+
+        mController.displayPreference(mScreen);
+        ShadowLooper.idleMainLooper();
+
+        assertThat(mPreference.isPassword()).isTrue();
+        assertThat(mPreference.getDialogLayoutResource())
+                .isEqualTo(R.layout.audio_sharing_password_dialog);
+        assertThat(mPreference.getText()).isEqualTo(EDITTEXT_PASSWORD);
+        assertThat(mPreference.getSummary())
+                .isEqualTo(mContext.getString(R.string.audio_streams_no_password_summary));
+        verify(mPreference).setValidator(any());
+        verify(mPreference).setOnDialogEventListener(any());
+    }
+
+    @Test
+    public void contentObserver_updatePreferenceOnChange() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.getBroadcastCode())
+                .thenReturn(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8));
+        mController.onStart(mLifecycleOwner);
+        mController.displayPreference(mScreen);
+        ShadowLooper.idleMainLooper();
+
+        ArgumentCaptor<ContentObserver> observerCaptor =
+                ArgumentCaptor.forClass(ContentObserver.class);
+        verify(mContentResolver)
+                .registerContentObserver(any(), anyBoolean(), observerCaptor.capture());
+
+        var observer = observerCaptor.getValue();
+        assertThat(observer).isNotNull();
+        observer.onChange(true);
+        verify(mPreference).setText(anyString());
+        verify(mPreference).setSummary(anyString());
+    }
+
+    @Test
+    public void sharedPrefChangeListener_updatePreferenceOnChange() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
+        when(mBroadcast.getBroadcastCode())
+                .thenReturn(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8));
+        mController.onStart(mLifecycleOwner);
+        mController.displayPreference(mScreen);
+        ShadowLooper.idleMainLooper();
+
+        ArgumentCaptor<SharedPreferences.OnSharedPreferenceChangeListener> captor =
+                ArgumentCaptor.forClass(SharedPreferences.OnSharedPreferenceChangeListener.class);
+        verify(mSharedPreferences).registerOnSharedPreferenceChangeListener(captor.capture());
+
+        var observer = captor.getValue();
+        assertThat(captor).isNotNull();
+        observer.onSharedPreferenceChanged(mSharedPreferences, SHARED_PREF_KEY);
+        verify(mPreference).setText(anyString());
+        verify(mPreference).setSummary(anyString());
+    }
+
+    @Test
+    public void displayPreference_setupPreference_hasPassword() {
+        when(mBroadcast.getBroadcastCode())
+                .thenReturn(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8));
+        mController.displayPreference(mScreen);
+        ShadowLooper.idleMainLooper();
+
+        assertThat(mPreference.isPassword()).isTrue();
+        assertThat(mPreference.getDialogLayoutResource())
+                .isEqualTo(R.layout.audio_sharing_password_dialog);
+        assertThat(mPreference.getText()).isEqualTo(BROADCAST_PASSWORD);
+        assertThat(mPreference.getSummary()).isEqualTo(HIDDEN_PASSWORD);
+        verify(mPreference).setValidator(any());
+        verify(mPreference).setOnDialogEventListener(any());
+    }
+
+    @Test
+    public void onBindDialogView_updatePreference_isBroadcasting_noPassword() {
+        when(mBroadcast.getBroadcastCode()).thenReturn(new byte[] {});
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        mController.displayPreference(mScreen);
+        mController.onBindDialogView();
+        ShadowLooper.idleMainLooper();
+
+        verify(mPreference).setEditable(false);
+        verify(mPreference).setChecked(true);
+    }
+
+    @Test
+    public void onBindDialogView_updatePreference_isNotBroadcasting_hasPassword() {
+        when(mBroadcast.getBroadcastCode())
+                .thenReturn(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8));
+        mController.displayPreference(mScreen);
+        mController.onBindDialogView();
+        ShadowLooper.idleMainLooper();
+
+        verify(mPreference).setEditable(true);
+        verify(mPreference).setChecked(false);
+    }
+
+    @Test
+    public void onPreferenceDataChanged_isBroadcasting_doNothing() {
+        when(mBroadcast.isEnabled(any())).thenReturn(true);
+        mController.displayPreference(mScreen);
+        mController.onPreferenceDataChanged(BROADCAST_PASSWORD, /* isPublicBroadcast= */ false);
+        ShadowLooper.idleMainLooper();
+
+        verify(mBroadcast, never()).setBroadcastCode(any());
+        verify(mFeatureFactory.metricsFeatureProvider, never()).action(any(), anyInt(), anyInt());
+    }
+
+    @Test
+    public void onPreferenceDataChanged_noChange_doNothing() {
+        when(mSharedPreferences.getString(anyString(), anyString())).thenReturn(EDITTEXT_PASSWORD);
+        when(mBroadcast.getBroadcastCode()).thenReturn(new byte[] {});
+        mController.displayPreference(mScreen);
+        mController.onPreferenceDataChanged(EDITTEXT_PASSWORD, /* isPublicBroadcast= */ true);
+        ShadowLooper.idleMainLooper();
+
+        verify(mBroadcast, never()).setBroadcastCode(any());
+        verify(mFeatureFactory.metricsFeatureProvider, never()).action(any(), anyInt(), anyInt());
+    }
+
+    @Test
+    public void onPreferenceDataChanged_updateToNonPublicBroadcast() {
+        when(mSharedPreferences.getString(anyString(), anyString())).thenReturn(EDITTEXT_PASSWORD);
+        when(mBroadcast.getBroadcastCode()).thenReturn(new byte[] {});
+        mController.displayPreference(mScreen);
+        mController.onPreferenceDataChanged(BROADCAST_PASSWORD, /* isPublicBroadcast= */ false);
+        ShadowLooper.idleMainLooper();
+
+        verify(mBroadcast).setBroadcastCode(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8));
+        verify(mEditor).putString(anyString(), eq(BROADCAST_PASSWORD));
+        verify(mFeatureFactory.metricsFeatureProvider)
+                .action(mContext, SettingsEnums.ACTION_AUDIO_STREAM_PASSWORD_UPDATED, 0);
+    }
+
+    @Test
+    public void onPreferenceDataChanged_updateToPublicBroadcast() {
+        when(mSharedPreferences.getString(anyString(), anyString())).thenReturn(EDITTEXT_PASSWORD);
+        when(mBroadcast.getBroadcastCode())
+                .thenReturn(BROADCAST_PASSWORD.getBytes(StandardCharsets.UTF_8));
+        mController.displayPreference(mScreen);
+        mController.onPreferenceDataChanged(EDITTEXT_PASSWORD, /* isPublicBroadcast= */ true);
+        ShadowLooper.idleMainLooper();
+
+        verify(mBroadcast).setBroadcastCode("".getBytes(StandardCharsets.UTF_8));
+        verify(mEditor, never()).putString(anyString(), eq(EDITTEXT_PASSWORD));
+        verify(mFeatureFactory.metricsFeatureProvider)
+                .action(mContext, SettingsEnums.ACTION_AUDIO_STREAM_PASSWORD_UPDATED, 1);
+    }
+
+    @Test
+    public void idTextValid_emptyString() {
+        boolean valid = mController.isTextValid("");
+
+        assertThat(valid).isFalse();
+    }
+
+    @Test
+    public void idTextValid_validPassword() {
+        boolean valid = mController.isTextValid(BROADCAST_PASSWORD);
+
+        assertThat(valid).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceTest.java
new file mode 100644
index 0000000..0b87e8c
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordPreferenceTest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioSharingPasswordPreferenceTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private static final String EDIT_TEXT_CONTENT = "text";
+    private Context mContext;
+    private AudioSharingPasswordPreference mPreference;
+
+    @Before
+    public void setup() {
+        mContext = ApplicationProvider.getApplicationContext();
+        mPreference = new AudioSharingPasswordPreference(mContext, null);
+    }
+
+    @Test
+    public void onBindDialogView_correctLayout() {
+        View view =
+                LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null);
+        mPreference.onBindDialogView(view);
+
+        var editText = view.findViewById(android.R.id.edit);
+        var checkBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox);
+        var dialogMessage = view.findViewById(android.R.id.message);
+
+        assertThat(editText).isNotNull();
+        assertThat(checkBox).isNotNull();
+        assertThat(dialogMessage).isNotNull();
+    }
+
+    @Test
+    public void setEditable_true() {
+        View view =
+                LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null);
+        mPreference.onBindDialogView(view);
+
+        var editText = view.findViewById(android.R.id.edit);
+        var checkBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox);
+        var dialogMessage = view.findViewById(android.R.id.message);
+
+        mPreference.setEditable(true);
+
+        assertThat(editText).isNotNull();
+        assertThat(editText.isEnabled()).isTrue();
+        assertThat(editText.getAlpha()).isEqualTo(1.0f);
+        assertThat(checkBox).isNotNull();
+        assertThat(checkBox.isEnabled()).isTrue();
+        assertThat(dialogMessage).isNotNull();
+        assertThat(dialogMessage.getVisibility()).isEqualTo(GONE);
+    }
+
+    @Test
+    public void setEditable_false() {
+        View view =
+                LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null);
+        mPreference.onBindDialogView(view);
+
+        var editText = view.findViewById(android.R.id.edit);
+        var checkBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox);
+        var dialogMessage = view.findViewById(android.R.id.message);
+
+        mPreference.setEditable(false);
+
+        assertThat(editText).isNotNull();
+        assertThat(editText.isEnabled()).isFalse();
+        assertThat(editText.getAlpha()).isLessThan(1.0f);
+        assertThat(checkBox).isNotNull();
+        assertThat(checkBox.isEnabled()).isFalse();
+        assertThat(dialogMessage).isNotNull();
+        assertThat(dialogMessage.getVisibility()).isEqualTo(VISIBLE);
+    }
+
+    @Test
+    public void setChecked_true() {
+        View view =
+                LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null);
+        mPreference.onBindDialogView(view);
+
+        CheckBox checkBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox);
+
+        mPreference.setChecked(true);
+
+        assertThat(checkBox).isNotNull();
+        assertThat(checkBox.isChecked()).isTrue();
+    }
+
+    @Test
+    public void setChecked_false() {
+        View view =
+                LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null);
+        mPreference.onBindDialogView(view);
+
+        CheckBox checkBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox);
+
+        mPreference.setChecked(false);
+
+        assertThat(checkBox).isNotNull();
+        assertThat(checkBox.isChecked()).isFalse();
+    }
+
+    @Test
+    public void onDialogEventListener_onClick_positiveButton() {
+        AudioSharingPasswordPreference.OnDialogEventListener listener =
+                mock(AudioSharingPasswordPreference.OnDialogEventListener.class);
+        mPreference.setOnDialogEventListener(listener);
+        View view =
+                LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null);
+        mPreference.onBindDialogView(view);
+
+        EditText editText = view.findViewById(android.R.id.edit);
+        assertThat(editText).isNotNull();
+        editText.setText(EDIT_TEXT_CONTENT);
+
+        mPreference.onClick(mock(DialogInterface.class), DialogInterface.BUTTON_POSITIVE);
+
+        verify(listener).onBindDialogView();
+        verify(listener).onPreferenceDataChanged(eq(EDIT_TEXT_CONTENT), anyBoolean());
+    }
+
+    @Test
+    public void onDialogEventListener_onClick_negativeButton_doNothing() {
+        AudioSharingPasswordPreference.OnDialogEventListener listener =
+                mock(AudioSharingPasswordPreference.OnDialogEventListener.class);
+        mPreference.setOnDialogEventListener(listener);
+        View view =
+                LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null);
+        mPreference.onBindDialogView(view);
+
+        EditText editText = view.findViewById(android.R.id.edit);
+        assertThat(editText).isNotNull();
+        editText.setText(EDIT_TEXT_CONTENT);
+
+        mPreference.onClick(mock(DialogInterface.class), DialogInterface.BUTTON_NEGATIVE);
+
+        verify(listener).onBindDialogView();
+        verify(listener, never()).onPreferenceDataChanged(anyString(), anyBoolean());
+    }
+
+    @Test
+    public void onPrepareDialogBuilder_editable_doNothing() {
+        View view =
+                LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null);
+        mPreference.onBindDialogView(view);
+        mPreference.setEditable(true);
+
+        var dialogBuilder = mock(AlertDialog.Builder.class);
+        mPreference.onPrepareDialogBuilder(
+                dialogBuilder, mock(DialogInterface.OnClickListener.class));
+
+        verify(dialogBuilder, never()).setPositiveButton(any(), any());
+    }
+
+    @Test
+    public void onPrepareDialogBuilder_notEditable_disableButton() {
+        View view =
+                LayoutInflater.from(mContext).inflate(R.layout.audio_sharing_password_dialog, null);
+        mPreference.onBindDialogView(view);
+        mPreference.setEditable(false);
+
+        var dialogBuilder = mock(AlertDialog.Builder.class);
+        mPreference.onPrepareDialogBuilder(
+                dialogBuilder, mock(DialogInterface.OnClickListener.class));
+
+        verify(dialogBuilder).setPositiveButton(any(), any());
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidatorTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidatorTest.java
new file mode 100644
index 0000000..5c96fe1
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPasswordValidatorTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioSharingPasswordValidatorTest {
+    private AudioSharingPasswordValidator mValidator;
+
+    @Before
+    public void setUp() {
+        mValidator = new AudioSharingPasswordValidator();
+    }
+
+    @Test
+    public void testValidPasswords() {
+        assertThat(mValidator.isTextValid("1234")).isTrue();
+        assertThat(mValidator.isTextValid("Password")).isTrue();
+        assertThat(mValidator.isTextValid("SecurePass123!")).isTrue();
+        assertThat(mValidator.isTextValid("ÄÖÜß")).isTrue();
+        assertThat(mValidator.isTextValid("1234567890abcdef")).isTrue();
+    }
+
+    @Test
+    public void testInvalidPasswords() {
+        assertThat(mValidator.isTextValid(null)).isFalse();
+        assertThat(mValidator.isTextValid("")).isFalse();
+        assertThat(mValidator.isTextValid("abc")).isFalse();
+        assertThat(mValidator.isTextValid("ThisIsAVeryLongPasswordThatExceedsSixteenOctets"))
+                .isFalse();
+        assertThat(mValidator.isTextValid("Invalid\uDC00")).isFalse();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java
index 2fddff5..391a7b1 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceBadCodeStateTest.java
@@ -20,22 +20,45 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.testutils.FakeFeatureFactory;
+
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 import org.robolectric.RobolectricTestRunner;
 
 @RunWith(RobolectricTestRunner.class)
 public class AddSourceBadCodeStateTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private AudioStreamPreference mPreference;
+    @Mock private AudioStreamsProgressCategoryController mController;
+    @Mock private AudioStreamsHelper mHelper;
+    private FakeFeatureFactory mFeatureFactory;
     private AddSourceBadCodeState mInstance;
 
     @Before
     public void setUp() {
-        mInstance = AddSourceBadCodeState.getInstance();
+        mFeatureFactory = FakeFeatureFactory.setupForTest();
+        mInstance = new AddSourceBadCodeState();
     }
 
     @Test
     public void testGetInstance() {
+        mInstance = AddSourceBadCodeState.getInstance();
         assertThat(mInstance).isNotNull();
         assertThat(mInstance).isInstanceOf(SyncedState.class);
     }
@@ -55,4 +78,19 @@
                         AudioStreamsProgressCategoryController.AudioStreamState
                                 .ADD_SOURCE_BAD_CODE);
     }
+
+    @Test
+    public void testPerformAction() {
+        when(mPreference.getContext()).thenReturn(mContext);
+        when(mPreference.getSourceOriginForLogging())
+                .thenReturn(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS);
+
+        mInstance.performAction(mPreference, mController, mHelper);
+
+        verify(mFeatureFactory.metricsFeatureProvider)
+                .action(
+                        eq(mContext),
+                        eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_FAILED_BAD_CODE),
+                        eq(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS.ordinal()));
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java
index d8b1fcf..917d8de 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceFailedStateTest.java
@@ -20,22 +20,45 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.testutils.FakeFeatureFactory;
+
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 import org.robolectric.RobolectricTestRunner;
 
 @RunWith(RobolectricTestRunner.class)
 public class AddSourceFailedStateTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private AudioStreamPreference mPreference;
+    @Mock private AudioStreamsProgressCategoryController mController;
+    @Mock private AudioStreamsHelper mHelper;
+    private FakeFeatureFactory mFeatureFactory;
     private AddSourceFailedState mInstance;
 
     @Before
     public void setUp() {
-        mInstance = AddSourceFailedState.getInstance();
+        mFeatureFactory = FakeFeatureFactory.setupForTest();
+        mInstance = new AddSourceFailedState();
     }
 
     @Test
     public void testGetInstance() {
+        mInstance = AddSourceFailedState.getInstance();
         assertThat(mInstance).isNotNull();
         assertThat(mInstance).isInstanceOf(SyncedState.class);
     }
@@ -54,4 +77,19 @@
                 .isEqualTo(
                         AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED);
     }
+
+    @Test
+    public void testPerformAction() {
+        when(mPreference.getContext()).thenReturn(mContext);
+        when(mPreference.getSourceOriginForLogging())
+                .thenReturn(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS);
+
+        mInstance.performAction(mPreference, mController, mHelper);
+
+        verify(mFeatureFactory.metricsFeatureProvider)
+                .action(
+                        eq(mContext),
+                        eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_FAILED_OTHER),
+                        eq(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS.ordinal()));
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java
index 6e5342b..ce21658 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AddSourceWaitForResponseStateTest.java
@@ -22,11 +22,21 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.settings.SettingsEnums;
 import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.testutils.FakeFeatureFactory;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -36,27 +46,41 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowAlertDialog;
 import org.robolectric.shadows.ShadowLooper;
 
 import java.util.concurrent.TimeUnit;
 
 @RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowAlertDialog.class,
+        })
 public class AddSourceWaitForResponseStateTest {
-    private static final int BROADCAST_ID = 1;
     @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private static final int BROADCAST_ID = 1;
+    private final Context mContext = spy(ApplicationProvider.getApplicationContext());
     @Mock private AudioStreamPreference mMockPreference;
     @Mock private AudioStreamsProgressCategoryController mMockController;
     @Mock private AudioStreamsHelper mMockHelper;
     @Mock private BluetoothLeBroadcastMetadata mMockMetadata;
+    @Mock private AudioStreamsRepository mMockRepository;
+    private FakeFeatureFactory mFeatureFactory;
     private AddSourceWaitForResponseState mInstance;
 
     @Before
     public void setUp() {
-        mInstance = AddSourceWaitForResponseState.getInstance();
+        mFeatureFactory = FakeFeatureFactory.setupForTest();
+        mInstance = new AddSourceWaitForResponseState();
+        when(mMockPreference.getContext()).thenReturn(mContext);
+        when(mMockPreference.getSourceOriginForLogging())
+                .thenReturn(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS);
     }
 
     @Test
     public void testGetInstance() {
+        mInstance = AddSourceWaitForResponseState.getInstance();
         assertThat(mInstance).isNotNull();
         assertThat(mInstance).isInstanceOf(AudioStreamStateHandler.class);
     }
@@ -93,11 +117,18 @@
     public void testPerformAction_metadataIsNotNull_addSource() {
         when(mMockPreference.getAudioStreamMetadata()).thenReturn(mMockMetadata);
         when(mMockPreference.getSourceOriginForLogging())
-                .thenReturn(SourceOriginForLogging.UNKNOWN);
+                .thenReturn(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS);
+        mInstance.setAudioStreamsRepositoryForTesting(mMockRepository);
 
         mInstance.performAction(mMockPreference, mMockController, mMockHelper);
 
         verify(mMockHelper).addSource(mMockMetadata);
+        verify(mFeatureFactory.metricsFeatureProvider)
+                .action(
+                        eq(mContext),
+                        eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN),
+                        eq(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS.ordinal()));
+        verify(mMockRepository).cacheMetadata(mMockMetadata);
         verify(mMockController, never()).handleSourceFailedToConnect(anyInt());
     }
 
@@ -108,12 +139,28 @@
         when(mMockPreference.getAudioStreamState()).thenReturn(mInstance.getStateEnum());
         when(mMockPreference.getAudioStreamBroadcastId()).thenReturn(BROADCAST_ID);
         when(mMockPreference.getSourceOriginForLogging())
-                .thenReturn(SourceOriginForLogging.UNKNOWN);
+                .thenReturn(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS);
+        when(mMockController.getFragment()).thenReturn(mock(AudioStreamsDashboardFragment.class));
+        mInstance.setAudioStreamsRepositoryForTesting(mMockRepository);
 
         mInstance.performAction(mMockPreference, mMockController, mMockHelper);
         ShadowLooper.idleMainLooper(ADD_SOURCE_WAIT_FOR_RESPONSE_TIMEOUT_MILLIS, TimeUnit.SECONDS);
 
         verify(mMockHelper).addSource(mMockMetadata);
         verify(mMockController).handleSourceFailedToConnect(BROADCAST_ID);
+        verify(mFeatureFactory.metricsFeatureProvider)
+                .action(
+                        eq(mContext),
+                        eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN),
+                        eq(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS.ordinal()));
+        verify(mMockRepository).cacheMetadata(mMockMetadata);
+        verify(mFeatureFactory.metricsFeatureProvider)
+                .action(
+                        eq(mContext),
+                        eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_FAILED_TIMEOUT),
+                        eq(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS.ordinal()));
+        verify(mContext).getString(R.string.audio_streams_dialog_stream_is_not_available);
+        verify(mContext).getString(R.string.audio_streams_is_not_playing);
+        verify(mContext).getString(R.string.audio_streams_dialog_close);
     }
 }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandlerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandlerTest.java
new file mode 100644
index 0000000..adc77a1
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamStateHandlerTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioStreamStateHandlerTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private static final int SUMMARY_RES = 1;
+    private static final String SUMMARY = "summary";
+    private final Context mContext = spy(ApplicationProvider.getApplicationContext());
+    @Mock private AudioStreamsProgressCategoryController mController;
+    @Mock private AudioStreamsHelper mHelper;
+    @Mock private AudioStreamPreference mPreference;
+    private AudioStreamStateHandler mHandler;
+
+    @Before
+    public void setUp() {
+        mHandler = spy(new AudioStreamStateHandler());
+    }
+
+    @Test
+    public void testHandleStateChange_noChange_doNothing() {
+        when(mHandler.getStateEnum())
+                .thenReturn(
+                        AudioStreamsProgressCategoryController.AudioStreamState
+                                .ADD_SOURCE_BAD_CODE);
+        when(mPreference.getAudioStreamState())
+                .thenReturn(
+                        AudioStreamsProgressCategoryController.AudioStreamState
+                                .ADD_SOURCE_BAD_CODE);
+
+        mHandler.handleStateChange(mPreference, mController, mHelper);
+
+        verify(mPreference, never()).setAudioStreamState(any());
+        verify(mHandler, never()).performAction(any(), any(), any());
+        verify(mPreference, never()).setIsConnected(anyBoolean(), anyString(), any());
+    }
+
+    @Test
+    public void testHandleStateChange_setNewState() {
+        when(mHandler.getStateEnum())
+                .thenReturn(AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED);
+        when(mPreference.getAudioStreamState())
+                .thenReturn(
+                        AudioStreamsProgressCategoryController.AudioStreamState
+                                .ADD_SOURCE_BAD_CODE);
+
+        mHandler.handleStateChange(mPreference, mController, mHelper);
+
+        verify(mPreference)
+                .setAudioStreamState(
+                        AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED);
+        verify(mHandler).performAction(any(), any(), any());
+        verify(mPreference).setIsConnected(eq(true), eq(""), eq(null));
+    }
+
+    @Test
+    public void testHandleStateChange_setNewState_newSummary_newListener() {
+        Preference.OnPreferenceClickListener listener =
+                mock(Preference.OnPreferenceClickListener.class);
+        when(mHandler.getStateEnum())
+                .thenReturn(
+                        AudioStreamsProgressCategoryController.AudioStreamState
+                                .ADD_SOURCE_BAD_CODE);
+        when(mHandler.getSummary()).thenReturn(SUMMARY_RES);
+        when(mHandler.getOnClickListener(any())).thenReturn(listener);
+        when(mPreference.getAudioStreamState())
+                .thenReturn(
+                        AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED);
+        when(mPreference.getContext()).thenReturn(mContext);
+        doReturn(SUMMARY).when(mContext).getString(anyInt());
+
+        mHandler.handleStateChange(mPreference, mController, mHelper);
+
+        verify(mPreference)
+                .setAudioStreamState(
+                        AudioStreamsProgressCategoryController.AudioStreamState
+                                .ADD_SOURCE_BAD_CODE);
+        verify(mHandler).performAction(any(), any(), any());
+        verify(mPreference).setIsConnected(eq(false), eq(SUMMARY), eq(listener));
+    }
+
+    @Test
+    public void testGetSummary() {
+        int res = mHandler.getSummary();
+        assertThat(res).isEqualTo(AudioStreamStateHandler.EMPTY_STRING_RES);
+    }
+
+    @Test
+    public void testGetOnClickListener() {
+        Preference.OnPreferenceClickListener listener = mHandler.getOnClickListener(mController);
+        assertThat(listener).isNull();
+    }
+
+    @Test
+    public void testGetStateEnum() {
+        var state = mHandler.getStateEnum();
+        assertThat(state)
+                .isEqualTo(AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdaterTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdaterTest.java
index 4403528..d6b99a1 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdaterTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsActiveDeviceSummaryUpdaterTest.java
@@ -20,6 +20,7 @@
 
 import static org.mockito.Mockito.when;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
 
@@ -76,25 +77,46 @@
     }
 
     @Test
-    public void onActiveDeviceChanged_notLeProfile_doNothing() {
-        mUpdater.onActiveDeviceChanged(mCachedBluetoothDevice, 0);
+    public void unregister_doNothing() {
+        mUpdater.register(false);
 
         assertThat(mUpdatedSummary).isNull();
     }
 
     @Test
-    public void onActiveDeviceChanged_leProfile_summaryUpdated() {
+    public void onProfileConnectionStateChanged_notLeAssistProfile_doNothing() {
+        mUpdater.onProfileConnectionStateChanged(mCachedBluetoothDevice, 0, 0);
+
+        assertThat(mUpdatedSummary).isNull();
+    }
+
+    @Test
+    public void onProfileConnectionStateChanged_leAssistantProfile_summaryUpdated() {
         ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(
                 mCachedBluetoothDevice);
         when(mCachedBluetoothDevice.getName()).thenReturn(DEVICE_NAME);
-        mUpdater.onActiveDeviceChanged(mCachedBluetoothDevice, BluetoothProfile.LE_AUDIO);
+        mUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothAdapter.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
 
         assertThat(mUpdatedSummary).isEqualTo(DEVICE_NAME);
     }
 
     @Test
-    public void onActiveDeviceChanged_leProfile_noDevice_summaryUpdated() {
-        mUpdater.onActiveDeviceChanged(mCachedBluetoothDevice, BluetoothProfile.LE_AUDIO);
+    public void onActiveDeviceChanged_leAssistantProfile_noDevice_summaryUpdated() {
+        mUpdater.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothAdapter.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+
+        assertThat(mUpdatedSummary)
+                .isEqualTo(mContext.getString(R.string.audio_streams_dialog_no_le_device_title));
+    }
+
+    @Test
+    public void onBluetoothStateOff_summaryUpdated() {
+        mUpdater.onBluetoothStateChanged(BluetoothAdapter.STATE_OFF);
 
         assertThat(mUpdatedSummary)
                 .isEqualTo(mContext.getString(R.string.audio_streams_dialog_no_le_device_title));
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsCategoryControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsCategoryControllerTest.java
index e4b6903..0e00309 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsCategoryControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsCategoryControllerTest.java
@@ -23,11 +23,13 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.robolectric.Shadows.shadowOf;
 
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothStatusCodes;
 import android.content.Context;
 import android.os.Looper;
@@ -42,6 +44,7 @@
 import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper;
 import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
 import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settingslib.bluetooth.BluetoothCallback;
 import com.android.settingslib.bluetooth.BluetoothEventManager;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
@@ -57,6 +60,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -116,7 +120,7 @@
         when(mBroadcast.isProfileReady()).thenReturn(true);
         when(mAssistant.isProfileReady()).thenReturn(true);
         when(mVolumeControl.isProfileReady()).thenReturn(true);
-        mController = new AudioStreamsCategoryController(mContext, KEY);
+        mController = spy(new AudioStreamsCategoryController(mContext, KEY));
         mPreference = new Preference(mContext);
         when(mScreen.findPreference(KEY)).thenReturn(mPreference);
         mController.displayPreference(mScreen);
@@ -228,4 +232,21 @@
         shadowOf(Looper.getMainLooper()).idle();
         assertThat(mPreference.isVisible()).isTrue();
     }
+
+    @Test
+    public void onProfileConnectionStateChanged_updateVisibility() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_QR_CODE_PRIVATE_BROADCAST_SHARING);
+        ArgumentCaptor<BluetoothCallback> argumentCaptor =
+                ArgumentCaptor.forClass(BluetoothCallback.class);
+        mController.onStart(mLifecycleOwner);
+        verify(mBluetoothEventManager).registerCallback(argumentCaptor.capture());
+
+        BluetoothCallback callback = argumentCaptor.getValue();
+        callback.onProfileConnectionStateChanged(
+                mCachedBluetoothDevice,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT,
+                BluetoothAdapter.STATE_DISCONNECTED);
+
+        verify(mController).updateVisibility();
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragmentTest.java
new file mode 100644
index 0000000..e83dade
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsDialogFragmentTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowAlertDialog;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowAlertDialog.class,
+        })
+public class AudioStreamsDialogFragmentTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private AudioStreamsDialogFragment.DialogBuilder mDialogBuilder;
+    private AudioStreamsDialogFragment mFragment;
+
+    @Before
+    public void setUp() {
+        mDialogBuilder = spy(new AudioStreamsDialogFragment.DialogBuilder(mContext));
+        mFragment = new AudioStreamsDialogFragment(mDialogBuilder, SettingsEnums.PAGE_UNKNOWN);
+    }
+
+    @After
+    public void tearDown() {
+        ShadowAlertDialog.reset();
+    }
+
+    @Test
+    public void testGetMetricsCategory() {
+        int dialogId = mFragment.getMetricsCategory();
+
+        assertThat(dialogId).isEqualTo(SettingsEnums.PAGE_UNKNOWN);
+    }
+
+    @Test
+    public void testOnCreateDialog() {
+        mFragment.onCreateDialog(Bundle.EMPTY);
+
+        verify(mDialogBuilder).build();
+    }
+
+    @Test
+    public void testShowDialog() {
+        FragmentController.setupFragment(mFragment);
+        AudioStreamsDialogFragment.show(mFragment, mDialogBuilder, SettingsEnums.PAGE_UNKNOWN);
+        ShadowLooper.idleMainLooper();
+
+        var dialog = ShadowAlertDialog.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java
new file mode 100644
index 0000000..164c2f0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryCallbackTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing.audiostreams;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public class AudioStreamsProgressCategoryCallbackTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Mock private AudioStreamsProgressCategoryController mController;
+    @Mock private BluetoothDevice mDevice;
+    @Mock private BluetoothLeBroadcastReceiveState mState;
+    @Mock private BluetoothLeBroadcastMetadata mMetadata;
+    private AudioStreamsProgressCategoryCallback mCallback;
+
+    @Before
+    public void setUp() {
+        mCallback = new AudioStreamsProgressCategoryCallback(mController);
+    }
+
+    @Test
+    public void testOnReceiveStateChanged_connected() {
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(mState.getBisSyncState()).thenReturn(bisSyncState);
+        mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);
+
+        verify(mController).handleSourceConnected(any());
+    }
+
+    @Test
+    public void testOnReceiveStateChanged_badCode() {
+        when(mState.getPaSyncState())
+                .thenReturn(BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED);
+        when(mState.getBigEncryptionState())
+                .thenReturn(BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE);
+        mCallback.onReceiveStateChanged(mDevice, /* sourceId= */ 0, mState);
+
+        verify(mController).handleSourceConnectBadCode(any());
+    }
+
+    @Test
+    public void testOnSearchStartFailed() {
+        mCallback.onSearchStartFailed(/* reason= */ 0);
+
+        verify(mController).showToast(anyString());
+        verify(mController).setScanning(anyBoolean());
+    }
+
+    @Test
+    public void testOnSearchStarted() {
+        mCallback.onSearchStarted(/* reason= */ 0);
+
+        verify(mController).setScanning(anyBoolean());
+    }
+
+    @Test
+    public void testOnSearchStopFailed() {
+        mCallback.onSearchStopFailed(/* reason= */ 0);
+
+        verify(mController).showToast(anyString());
+    }
+
+    @Test
+    public void testOnSearchStopped() {
+        mCallback.onSearchStopped(/* reason= */ 0);
+
+        verify(mController).setScanning(anyBoolean());
+    }
+
+    @Test
+    public void testOnSourceAddFailed() {
+        when(mMetadata.getBroadcastId()).thenReturn(1);
+        mCallback.onSourceAddFailed(mDevice, mMetadata, /* reason= */ 0);
+
+        verify(mController).handleSourceFailedToConnect(1);
+    }
+
+    @Test
+    public void testOnSourceFound() {
+        mCallback.onSourceFound(mMetadata);
+
+        verify(mController).handleSourceFound(mMetadata);
+    }
+
+    @Test
+    public void testOnSourceLost() {
+        mCallback.onSourceLost(/* broadcastId= */ 1);
+
+        verify(mController).handleSourceLost(1);
+    }
+
+    @Test
+    public void testOnSourceRemoveFailed() {
+        mCallback.onSourceRemoveFailed(mDevice, /* sourceId= */ 0, /* reason= */ 0);
+
+        verify(mController).showToast(anyString());
+    }
+
+    @Test
+    public void testOnSourceRemoved() {
+        mCallback.onSourceRemoved(mDevice, /* sourceId= */ 0, /* reason= */ 0);
+
+        verify(mController).handleSourceRemoved();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java
new file mode 100644
index 0000000..d43ec81
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsProgressCategoryControllerTest.java
@@ -0,0 +1,671 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.connecteddevice.audiosharing.audiostreams;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_BAD_CODE;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.SYNCED;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC;
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController.UNSET_BROADCAST_ID;
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+import static java.util.Collections.emptyList;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothLeAudioContentMetadata;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.bluetooth.BluetoothLeBroadcastReceiveState;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.os.Looper;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper;
+import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
+import com.android.settings.testutils.shadow.ShadowThreadUtils;
+import com.android.settingslib.bluetooth.BluetoothEventManager;
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowAlertDialog;
+import org.robolectric.shadows.androidx.fragment.FragmentController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowBluetoothUtils.class,
+            ShadowAudioStreamsHelper.class,
+            ShadowThreadUtils.class,
+            ShadowAlertDialog.class,
+        })
+public class AudioStreamsProgressCategoryControllerTest {
+    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private static final String VALID_METADATA =
+            "BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;"
+                    + "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;";
+    private static final String KEY = "audio_streams_nearby_category";
+    private static final int QR_CODE_BROADCAST_ID = 1;
+    private static final int ALREADY_CONNECTED_BROADCAST_ID = 2;
+    private static final int NEWLY_FOUND_BROADCAST_ID = 3;
+    private static final String BROADCAST_NAME_1 = "name_1";
+    private static final String BROADCAST_NAME_2 = "name_2";
+    private static final byte[] BROADCAST_CODE = new byte[] {1};
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private LocalBluetoothManager mLocalBtManager;
+    @Mock private BluetoothEventManager mBluetoothEventManager;
+    @Mock private PreferenceScreen mScreen;
+    @Mock private AudioStreamsHelper mAudioStreamsHelper;
+    @Mock private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
+    @Mock private BluetoothLeBroadcastMetadata mMetadata;
+    @Mock private CachedBluetoothDevice mDevice;
+    @Mock private AudioStreamsProgressCategoryPreference mPreference;
+    private Lifecycle mLifecycle;
+    private LifecycleOwner mLifecycleOwner;
+    private Fragment mFragment;
+    private TestController mController;
+
+    @Before
+    public void setUp() {
+        ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper);
+        when(mAudioStreamsHelper.getLeBroadcastAssistant()).thenReturn(mLeBroadcastAssistant);
+        when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList());
+
+        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
+        when(mLocalBtManager.getEventManager()).thenReturn(mBluetoothEventManager);
+        when(mLeBroadcastAssistant.isSearchInProgress()).thenReturn(false);
+
+        when(mScreen.findPreference(anyString())).thenReturn(mPreference);
+
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+
+        mFragment = new Fragment();
+        mController = spy(new TestController(mContext, KEY));
+    }
+
+    @After
+    public void tearDown() {
+        ShadowBluetoothUtils.reset();
+        ShadowAudioStreamsHelper.reset();
+    }
+
+    @Test
+    public void testGetAvailabilityStatus() {
+        int status = mController.getAvailabilityStatus();
+
+        assertThat(status).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void testDisplayPreference() {
+        mController.displayPreference(mScreen);
+
+        verify(mPreference).setVisible(true);
+    }
+
+    @Test
+    public void testSetScanning() {
+        mController.displayPreference(mScreen);
+        mController.setScanning(true);
+
+        verify(mPreference).setProgress(true);
+    }
+
+    @Test
+    public void testOnStart_initNoDevice_showDialog() {
+        when(mLeBroadcastAssistant.isSearchInProgress()).thenReturn(true);
+
+        FragmentController.setupFragment(mFragment);
+        mController.setFragment(mFragment);
+        mController.displayPreference(mScreen);
+        mController.onStart(mLifecycleOwner);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        // Called twice, once in displayPreference, the other in init()
+        verify(mPreference, times(2)).setVisible(anyBoolean());
+        verify(mPreference).removeAudioStreamPreferences();
+        verify(mLeBroadcastAssistant).stopSearchingForSources();
+        verify(mLeBroadcastAssistant).unregisterServiceCallBack(any());
+
+        var dialog = ShadowAlertDialog.getLatestAlertDialog();
+        assertThat(dialog).isNotNull();
+        assertThat(dialog.isShowing()).isTrue();
+
+        TextView title = dialog.findViewById(R.id.dialog_title);
+        assertThat(title).isNotNull();
+        assertThat(title.getText())
+                .isEqualTo(mContext.getString(R.string.audio_streams_dialog_no_le_device_title));
+        TextView subtitle1 = dialog.findViewById(R.id.dialog_subtitle);
+        assertThat(subtitle1).isNotNull();
+        assertThat(subtitle1.getVisibility()).isEqualTo(View.GONE);
+        TextView subtitle2 = dialog.findViewById(R.id.dialog_subtitle_2);
+        assertThat(subtitle2).isNotNull();
+        assertThat(subtitle2.getText())
+                .isEqualTo(mContext.getString(R.string.audio_streams_dialog_no_le_device_subtitle));
+        View leftButton = dialog.findViewById(R.id.left_button);
+        assertThat(leftButton).isNotNull();
+        assertThat(leftButton.getVisibility()).isEqualTo(View.VISIBLE);
+        Button rightButton = dialog.findViewById(R.id.right_button);
+        assertThat(rightButton).isNotNull();
+        assertThat(rightButton.getText())
+                .isEqualTo(mContext.getString(R.string.audio_streams_dialog_no_le_device_button));
+        assertThat(rightButton.hasOnClickListeners()).isTrue();
+
+        dialog.cancel();
+    }
+
+    @Test
+    public void testBluetoothOff_triggerRunnable() {
+        mController.mBluetoothCallback.onBluetoothStateChanged(BluetoothAdapter.STATE_OFF);
+
+        verify(mController.mExecutor).execute(any());
+    }
+
+    @Test
+    public void testDeviceConnectionStateChanged_triggerRunnable() {
+        mController.mBluetoothCallback.onProfileConnectionStateChanged(
+                mDevice,
+                BluetoothAdapter.STATE_DISCONNECTED,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+
+        verify(mController.mExecutor).execute(any());
+    }
+
+    @Test
+    public void testOnStart_initHasDevice_noPreference() {
+        // Setup a device
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+        mController.onStart(mLifecycleOwner);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        verify(mLeBroadcastAssistant).registerServiceCallBack(any(), any());
+        verify(mLeBroadcastAssistant).startSearchingForSources(any());
+
+        var dialog = ShadowAlertDialog.getLatestAlertDialog();
+        assertThat(dialog).isNull();
+
+        verify(mController, never()).moveToState(any(), any());
+    }
+
+    @Test
+    public void testOnStart_handleSourceFromQrCode() {
+        // Setup a device
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+        // Setup a source from qr code
+        mController.setSourceFromQrCode(mMetadata, SourceOriginForLogging.UNKNOWN);
+        when(mMetadata.getBroadcastId()).thenReturn(QR_CODE_BROADCAST_ID);
+
+        // Handle the source from qr code in onStart
+        mController.displayPreference(mScreen);
+        mController.onStart(mLifecycleOwner);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        // Verify the connected source is created and moved to WAIT_FOR_SYNC
+        ArgumentCaptor<AudioStreamPreference> preference =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
+                ArgumentCaptor.forClass(
+                        AudioStreamsProgressCategoryController.AudioStreamState.class);
+
+        verify(mController).moveToState(preference.capture(), state.capture());
+        assertThat(preference.getValue()).isNotNull();
+        assertThat(preference.getValue().getAudioStreamBroadcastId())
+                .isEqualTo(QR_CODE_BROADCAST_ID);
+        assertThat(state.getValue()).isEqualTo(WAIT_FOR_SYNC);
+    }
+
+    @Test
+    public void testOnStart_handleSourceAlreadyConnected() {
+        // Setup a device
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+        // Setup a connected source
+        BluetoothLeBroadcastReceiveState connected =
+                createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID);
+        List<BluetoothLeBroadcastReceiveState> list = new ArrayList<>();
+        list.add(connected);
+        when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(list);
+
+        // Handle already connected source in onStart
+        mController.displayPreference(mScreen);
+        mController.onStart(mLifecycleOwner);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        ArgumentCaptor<AudioStreamPreference> preference =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
+                ArgumentCaptor.forClass(
+                        AudioStreamsProgressCategoryController.AudioStreamState.class);
+
+        // Verify the connected source is created and moved to SOURCE_ADDED
+        verify(mController).moveToState(preference.capture(), state.capture());
+        assertThat(preference.getValue()).isNotNull();
+        assertThat(preference.getValue().getAudioStreamBroadcastId())
+                .isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
+        assertThat(state.getValue()).isEqualTo(SOURCE_ADDED);
+    }
+
+    @Test
+    public void testOnStart_sourceFromQrCodeNoId_sourceAlreadyConnected_sameName_updateId() {
+        // Setup a device
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+        // Setup source from qr code with unset id and BROADCAST_NAME_1. Creating a real metadata
+        // for properly update its id.
+        var metadata =
+                BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(VALID_METADATA);
+        assertThat(metadata).isNotNull();
+        var metadataWithNoIdAndSameName =
+                new BluetoothLeBroadcastMetadata.Builder(metadata)
+                        .setBroadcastId(UNSET_BROADCAST_ID)
+                        .setBroadcastName(BROADCAST_NAME_1)
+                        .build();
+        mController.setSourceFromQrCode(
+                metadataWithNoIdAndSameName, SourceOriginForLogging.UNKNOWN);
+
+        // Setup a connected source with name BROADCAST_NAME_1 and id
+        BluetoothLeBroadcastReceiveState connected =
+                createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID);
+        var data = mock(BluetoothLeAudioContentMetadata.class);
+        when(connected.getSubgroupMetadata()).thenReturn(ImmutableList.of(data));
+        when(data.getProgramInfo()).thenReturn(BROADCAST_NAME_1);
+        when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected));
+
+        // Handle both source from qr code and already connected source in onStart
+        mController.displayPreference(mScreen);
+        mController.onStart(mLifecycleOwner);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        // Verify two preferences created, one moved to state WAIT_FOR_SYNC, one to SOURCE_ADDED.
+        // Both has ALREADY_CONNECTED_BROADCAST_ID as the UNSET_ID is updated to match.
+        ArgumentCaptor<AudioStreamPreference> preference =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
+                ArgumentCaptor.forClass(
+                        AudioStreamsProgressCategoryController.AudioStreamState.class);
+        verify(mController, times(2)).moveToState(preference.capture(), state.capture());
+
+        List<AudioStreamPreference> preferences = preference.getAllValues();
+        assertThat(preferences.size()).isEqualTo(2);
+        List<AudioStreamsProgressCategoryController.AudioStreamState> states = state.getAllValues();
+        assertThat(states.size()).isEqualTo(2);
+
+        // The preference contains source from qr code
+        assertThat(preferences.get(0).getAudioStreamBroadcastId())
+                .isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
+        assertThat(states.get(0)).isEqualTo(WAIT_FOR_SYNC);
+
+        // The preference contains already connected source
+        assertThat(preferences.get(1).getAudioStreamBroadcastId())
+                .isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
+        assertThat(states.get(1)).isEqualTo(SOURCE_ADDED);
+    }
+
+    @Test
+    public void testHandleSourceFound_addNew() {
+        // Setup a device
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+        when(mMetadata.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
+        // A new source is found
+        mController.handleSourceFound(mMetadata);
+
+        // Verify a preference is created with state SYNCED.
+        ArgumentCaptor<AudioStreamPreference> preference =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
+                ArgumentCaptor.forClass(
+                        AudioStreamsProgressCategoryController.AudioStreamState.class);
+
+        verify(mController).moveToState(preference.capture(), state.capture());
+        assertThat(preference.getValue()).isNotNull();
+        assertThat(preference.getValue().getAudioStreamBroadcastId())
+                .isEqualTo(NEWLY_FOUND_BROADCAST_ID);
+        assertThat(state.getValue()).isEqualTo(SYNCED);
+    }
+
+    @Test
+    public void testHandleSourceFound_sameIdWithSourceFromQrCode_updateMetadataAndState() {
+        // Setup a device
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+        // Setup source from qr code with QR_CODE_BROADCAST_ID, BROADCAST_NAME_1 and BROADCAST_CODE.
+        var metadata =
+                BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(VALID_METADATA);
+        assertThat(metadata).isNotNull();
+        var metadataFromQrCode =
+                new BluetoothLeBroadcastMetadata.Builder(metadata)
+                        .setBroadcastId(QR_CODE_BROADCAST_ID)
+                        .setBroadcastName(BROADCAST_NAME_1)
+                        .setBroadcastCode(BROADCAST_CODE)
+                        .build();
+        mController.setSourceFromQrCode(metadataFromQrCode, SourceOriginForLogging.UNKNOWN);
+
+        // Handle the source from qr code in onStart
+        mController.displayPreference(mScreen);
+        mController.onStart(mLifecycleOwner);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        // A new source is found
+        mController.handleSourceFound(
+                new BluetoothLeBroadcastMetadata.Builder(metadata)
+                        .setBroadcastId(QR_CODE_BROADCAST_ID)
+                        .setBroadcastName(BROADCAST_NAME_2)
+                        .build());
+        shadowOf(Looper.getMainLooper()).idle();
+
+        ArgumentCaptor<AudioStreamPreference> preference =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
+                ArgumentCaptor.forClass(
+                        AudioStreamsProgressCategoryController.AudioStreamState.class);
+
+        verify(mController, times(2)).moveToState(preference.capture(), state.capture());
+        List<AudioStreamPreference> preferences = preference.getAllValues();
+        List<AudioStreamsProgressCategoryController.AudioStreamState> states = state.getAllValues();
+
+        // Verify the qr code source is created with WAIT_FOR_SYNC, broadcast name got updated to
+        // BROADCAST_NAME_2
+        var sourceFromQrCode = preferences.get(0);
+        assertThat(sourceFromQrCode.getAudioStreamBroadcastId()).isEqualTo(QR_CODE_BROADCAST_ID);
+        assertThat(sourceFromQrCode.getAudioStreamMetadata()).isNotNull();
+        assertThat(sourceFromQrCode.getAudioStreamMetadata().getBroadcastName())
+                .isEqualTo(BROADCAST_NAME_2);
+        assertThat(sourceFromQrCode.getAudioStreamMetadata().getBroadcastCode())
+                .isEqualTo(BROADCAST_CODE);
+        assertThat(states.get(0)).isEqualTo(WAIT_FOR_SYNC);
+
+        // Verify the newly found source is created, broadcast code is retrieved from the source
+        // from qr code, and state updated to ADD_SOURCE_WAIT_FOR_RESPONSE
+        var newlyFoundSource = preferences.get(1);
+        assertThat(newlyFoundSource.getAudioStreamBroadcastId()).isEqualTo(QR_CODE_BROADCAST_ID);
+        assertThat(newlyFoundSource.getAudioStreamMetadata()).isNotNull();
+        assertThat(newlyFoundSource.getAudioStreamMetadata().getBroadcastName())
+                .isEqualTo(BROADCAST_NAME_2);
+        assertThat(newlyFoundSource.getAudioStreamMetadata().getBroadcastCode())
+                .isEqualTo(BROADCAST_CODE);
+        assertThat(states.get(1)).isEqualTo(ADD_SOURCE_WAIT_FOR_RESPONSE);
+    }
+
+    @Test
+    public void testHandleSourceFound_sameIdWithOtherState_doNothing() {
+        // Setup a device
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+        // Setup source already connected
+        BluetoothLeBroadcastReceiveState connected =
+                createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID);
+        when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected));
+
+        // Handle source already connected in onStart
+        mController.displayPreference(mScreen);
+        mController.onStart(mLifecycleOwner);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        // A new source found
+        when(mMetadata.getBroadcastId()).thenReturn(ALREADY_CONNECTED_BROADCAST_ID);
+        mController.handleSourceFound(mMetadata);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        // Verify only the connected source has created a preference, and its state remains as
+        // SOURCE_ADDED
+        ArgumentCaptor<AudioStreamPreference> preference =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
+                ArgumentCaptor.forClass(
+                        AudioStreamsProgressCategoryController.AudioStreamState.class);
+
+        verify(mController).moveToState(preference.capture(), state.capture());
+        assertThat(preference.getValue()).isNotNull();
+        assertThat(preference.getValue().getAudioStreamBroadcastId())
+                .isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
+        assertThat(preference.getValue().getAudioStreamState()).isEqualTo(SOURCE_ADDED);
+    }
+
+    @Test
+    public void testHandleSourceLost_removed() {
+        // Setup a device
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+        // Setup mPreference so it's not null
+        mController.displayPreference(mScreen);
+
+        // A new source found
+        when(mMetadata.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
+        mController.handleSourceFound(mMetadata);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        // A new source found is lost
+        mController.handleSourceLost(NEWLY_FOUND_BROADCAST_ID);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        ArgumentCaptor<AudioStreamPreference> preferenceToAdd =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamPreference> preferenceToRemove =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
+                ArgumentCaptor.forClass(
+                        AudioStreamsProgressCategoryController.AudioStreamState.class);
+
+        // Verify a new preference is created with state SYNCED.
+        verify(mController).moveToState(preferenceToAdd.capture(), state.capture());
+        assertThat(preferenceToAdd.getValue()).isNotNull();
+        assertThat(preferenceToAdd.getValue().getAudioStreamBroadcastId())
+                .isEqualTo(NEWLY_FOUND_BROADCAST_ID);
+        assertThat(state.getValue()).isEqualTo(SYNCED);
+
+        // Verify the preference with NEWLY_FOUND_BROADCAST_ID is removed.
+        verify(mPreference).removePreference(preferenceToRemove.capture());
+        assertThat(preferenceToRemove.getValue().getAudioStreamBroadcastId())
+                .isEqualTo(NEWLY_FOUND_BROADCAST_ID);
+    }
+
+    @Test
+    public void testHandleSourceRemoved_removed() {
+        // Setup a device
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+        // Setup already connected source
+        BluetoothLeBroadcastReceiveState connected =
+                createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID);
+        when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected));
+
+        // Handle connected source in onStart
+        mController.displayPreference(mScreen);
+        mController.onStart(mLifecycleOwner);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        // The connect source is no longer connected
+        when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(emptyList());
+        mController.handleSourceRemoved();
+        shadowOf(Looper.getMainLooper()).idle();
+
+        ArgumentCaptor<AudioStreamPreference> preferenceToAdd =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamPreference> preferenceToRemove =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
+                ArgumentCaptor.forClass(
+                        AudioStreamsProgressCategoryController.AudioStreamState.class);
+
+        // Verify a new preference is created with state SOURCE_ADDED.
+        verify(mController).moveToState(preferenceToAdd.capture(), state.capture());
+        assertThat(preferenceToAdd.getValue()).isNotNull();
+        assertThat(preferenceToAdd.getValue().getAudioStreamBroadcastId())
+                .isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
+        assertThat(state.getValue()).isEqualTo(SOURCE_ADDED);
+
+        // Verify the preference with ALREADY_CONNECTED_BROADCAST_ID is removed.
+        verify(mPreference).removePreference(preferenceToRemove.capture());
+        assertThat(preferenceToRemove.getValue().getAudioStreamBroadcastId())
+                .isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
+    }
+
+    @Test
+    public void testHandleSourceRemoved_updateState() {
+        // Setup a device
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+        // Setup a connected source
+        BluetoothLeBroadcastReceiveState connected =
+                createConnectedMock(ALREADY_CONNECTED_BROADCAST_ID);
+        when(mAudioStreamsHelper.getAllConnectedSources()).thenReturn(ImmutableList.of(connected));
+
+        // Handle connected source in onStart
+        mController.displayPreference(mScreen);
+        mController.onStart(mLifecycleOwner);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        // The connected source is identified as having a bad code
+        BluetoothLeBroadcastReceiveState badCode = mock(BluetoothLeBroadcastReceiveState.class);
+        when(badCode.getBroadcastId()).thenReturn(ALREADY_CONNECTED_BROADCAST_ID);
+        when(badCode.getPaSyncState())
+                .thenReturn(BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED);
+        when(badCode.getBigEncryptionState())
+                .thenReturn(BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE);
+        mController.handleSourceConnectBadCode(badCode);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        ArgumentCaptor<AudioStreamPreference> preference =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
+                ArgumentCaptor.forClass(
+                        AudioStreamsProgressCategoryController.AudioStreamState.class);
+
+        verify(mController, times(2)).moveToState(preference.capture(), state.capture());
+        List<AudioStreamPreference> preferences = preference.getAllValues();
+        assertThat(preferences.size()).isEqualTo(2);
+        List<AudioStreamsProgressCategoryController.AudioStreamState> states = state.getAllValues();
+        assertThat(states.size()).isEqualTo(2);
+
+        // Verify the connected source is created state SOURCE_ADDED
+        assertThat(preferences.get(0).getAudioStreamBroadcastId())
+                .isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
+        assertThat(states.get(0)).isEqualTo(SOURCE_ADDED);
+
+        // Verify the connected source is updated to state ADD_SOURCE_BAD_CODE
+        assertThat(preferences.get(1).getAudioStreamBroadcastId())
+                .isEqualTo(ALREADY_CONNECTED_BROADCAST_ID);
+        assertThat(states.get(1)).isEqualTo(ADD_SOURCE_BAD_CODE);
+    }
+
+    @Test
+    public void testHandleSourceFailedToConnect_updateState() {
+        // Setup a device
+        ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
+
+        // Setup mPreference so it's not null
+        mController.displayPreference(mScreen);
+
+        // A new source found
+        when(mMetadata.getBroadcastId()).thenReturn(NEWLY_FOUND_BROADCAST_ID);
+        mController.handleSourceFound(mMetadata);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        // The new found source is identified as failed to connect
+        mController.handleSourceFailedToConnect(NEWLY_FOUND_BROADCAST_ID);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        ArgumentCaptor<AudioStreamPreference> preference =
+                ArgumentCaptor.forClass(AudioStreamPreference.class);
+        ArgumentCaptor<AudioStreamsProgressCategoryController.AudioStreamState> state =
+                ArgumentCaptor.forClass(
+                        AudioStreamsProgressCategoryController.AudioStreamState.class);
+
+        verify(mController, times(2)).moveToState(preference.capture(), state.capture());
+        List<AudioStreamPreference> preferences = preference.getAllValues();
+        assertThat(preferences.size()).isEqualTo(2);
+        List<AudioStreamsProgressCategoryController.AudioStreamState> states = state.getAllValues();
+        assertThat(states.size()).isEqualTo(2);
+
+        // Verify one preference is created with SYNCED
+        assertThat(preferences.get(0).getAudioStreamBroadcastId())
+                .isEqualTo(NEWLY_FOUND_BROADCAST_ID);
+        assertThat(states.get(0)).isEqualTo(SYNCED);
+
+        // Verify the preference is updated to state ADD_SOURCE_FAILED
+        assertThat(preferences.get(1).getAudioStreamBroadcastId())
+                .isEqualTo(NEWLY_FOUND_BROADCAST_ID);
+        assertThat(states.get(1)).isEqualTo(ADD_SOURCE_FAILED);
+    }
+
+    private static BluetoothLeBroadcastReceiveState createConnectedMock(int id) {
+        var connected = mock(BluetoothLeBroadcastReceiveState.class);
+        List<Long> bisSyncState = new ArrayList<>();
+        bisSyncState.add(1L);
+        when(connected.getBroadcastId()).thenReturn(id);
+        when(connected.getBisSyncState()).thenReturn(bisSyncState);
+        return connected;
+    }
+
+    static class TestController extends AudioStreamsProgressCategoryController {
+        TestController(Context context, String preferenceKey) {
+            super(context, preferenceKey);
+            mExecutor = spy(mContext.getMainExecutor());
+        }
+
+        @Override
+        void moveToState(AudioStreamPreference preference, AudioStreamState state) {
+            preference.setAudioStreamState(state);
+            // Do nothing else to avoid side effect from AudioStreamStateHandler#performAction
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeControllerTest.java
index 4990f26..a83cbf0 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsScanQrCodeControllerTest.java
@@ -16,29 +16,38 @@
 
 package com.android.settings.connecteddevice.audiosharing.audiostreams;
 
+import static android.app.settings.SettingsEnums.AUDIO_STREAM_MAIN;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController.REQUEST_SCAN_BT_BROADCAST_QR_CODE;
 import static com.android.settings.core.BasePreferenceController.AVAILABLE;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothProfile;
 import android.content.Context;
+import android.content.Intent;
 
 import androidx.lifecycle.LifecycleOwner;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceScreen;
 import androidx.test.core.app.ApplicationProvider;
 
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
 import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper;
 import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
 import com.android.settingslib.bluetooth.BluetoothEventManager;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
 import com.android.settingslib.core.lifecycle.Lifecycle;
 
 import org.junit.After;
@@ -46,6 +55,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -139,17 +149,46 @@
     public void onPreferenceClick_hasFragment_launchSubSetting() {
         mController.displayPreference(mScreen);
         mController.setFragment(mFragment);
+        when(mFragment.getMetricsCategory()).thenReturn(AUDIO_STREAM_MAIN);
 
         var listener = mPreference.getOnPreferenceClickListener();
         assertThat(listener).isNotNull();
+
+        // mContext is not an Activity context, calling startActivity() from outside of an Activity
+        // context requires the FLAG_ACTIVITY_NEW_TASK flag, create a mock to avoid this
+        // AndroidRuntimeException.
+        Context activityContext = mock(Context.class);
+        when(mPreference.getContext()).thenReturn(activityContext);
+        when(mPreference.getKey()).thenReturn(AudioStreamsScanQrCodeController.KEY);
+
         var clicked = listener.onPreferenceClick(mPreference);
+
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        ArgumentCaptor<Integer> requestCodeCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mFragment)
+                .startActivityForResult(intentCaptor.capture(), requestCodeCaptor.capture());
+
+        Intent intent = intentCaptor.getValue();
+        assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
+                .isEqualTo(AudioStreamsQrCodeScanFragment.class.getName());
+        assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0))
+                .isEqualTo(R.string.audio_streams_main_page_scan_qr_code_title);
+        assertThat(intent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 0))
+                .isEqualTo(AUDIO_STREAM_MAIN);
+
+        int requestCode = requestCodeCaptor.getValue();
+        assertThat(requestCode).isEqualTo(REQUEST_SCAN_BT_BROADCAST_QR_CODE);
+
         assertThat(clicked).isTrue();
     }
 
     @Test
     public void updateVisibility_noConnected_invisible() {
         mController.displayPreference(mScreen);
-        mController.mBluetoothCallback.onActiveDeviceChanged(mDevice, BluetoothProfile.LE_AUDIO);
+        mController.mBluetoothCallback.onProfileConnectionStateChanged(
+                mDevice,
+                BluetoothAdapter.STATE_DISCONNECTED,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
 
         assertThat(mPreference.isVisible()).isFalse();
     }
@@ -158,7 +197,10 @@
     public void updateVisibility_hasConnected_visible() {
         mController.displayPreference(mScreen);
         ShadowAudioStreamsHelper.setCachedBluetoothDeviceInSharingOrLeConnected(mDevice);
-        mController.mBluetoothCallback.onActiveDeviceChanged(mDevice, BluetoothProfile.LE_AUDIO);
+        mController.mBluetoothCallback.onProfileConnectionStateChanged(
+                mDevice,
+                BluetoothAdapter.STATE_CONNECTED,
+                BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
 
         assertThat(mPreference.isVisible()).isTrue();
     }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java
index 0f0bafe..59a42a1 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SourceAddedStateTest.java
@@ -16,31 +16,76 @@
 
 package com.android.settings.connecteddevice.audiosharing.audiostreams;
 
+import static android.app.settings.SettingsEnums.AUDIO_STREAM_MAIN;
+
 import static com.android.settings.connecteddevice.audiosharing.audiostreams.SourceAddedState.AUDIO_STREAM_SOURCE_ADDED_STATE_SUMMARY;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.settings.SettingsEnums;
+import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.shadow.ShadowFragment;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
 
 @RunWith(RobolectricTestRunner.class)
+@Config(
+        shadows = {
+            ShadowFragment.class,
+        })
 public class SourceAddedStateTest {
-
     @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private static final int BROADCAST_ID = 1;
+    private static final String BROADCAST_TITLE = "title";
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    @Mock private AudioStreamPreference mPreference;
+    @Mock private AudioStreamsProgressCategoryController mController;
+    @Mock private AudioStreamsHelper mHelper;
+    @Mock private AudioStreamsRepository mRepository;
+    @Mock private AudioStreamsDashboardFragment mFragment;
+    @Mock private FragmentActivity mActivity;
+    private FakeFeatureFactory mFeatureFactory;
     private SourceAddedState mInstance;
 
     @Before
     public void setUp() {
-        mInstance = SourceAddedState.getInstance();
+        when(mFragment.getActivity()).thenReturn(mActivity);
+        mFeatureFactory = FakeFeatureFactory.setupForTest();
+        mInstance = new SourceAddedState();
+        when(mPreference.getAudioStreamBroadcastId()).thenReturn(BROADCAST_ID);
+        when(mPreference.getTitle()).thenReturn(BROADCAST_TITLE);
     }
 
     @Test
     public void testGetInstance() {
+        mInstance = SourceAddedState.getInstance();
         assertThat(mInstance).isNotNull();
         assertThat(mInstance).isInstanceOf(SourceAddedState.class);
     }
@@ -58,4 +103,59 @@
         assertThat(stateEnum)
                 .isEqualTo(AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED);
     }
+
+    @Test
+    public void testPerformAction() {
+        mInstance.setAudioStreamsRepositoryForTesting(mRepository);
+        BluetoothLeBroadcastMetadata mockMetadata = mock(BluetoothLeBroadcastMetadata.class);
+        when(mRepository.getCachedMetadata(anyInt())).thenReturn(mockMetadata);
+        when(mPreference.getContext()).thenReturn(mContext);
+        when(mPreference.getSourceOriginForLogging())
+                .thenReturn(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS);
+
+        mInstance.performAction(mPreference, mController, mHelper);
+
+        verify(mRepository).saveMetadata(eq(mContext), eq(mockMetadata));
+        verify(mFeatureFactory.metricsFeatureProvider)
+                .action(
+                        eq(mContext),
+                        eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_SUCCEED),
+                        eq(SourceOriginForLogging.QR_CODE_SCAN_SETTINGS.ordinal()));
+        verify(mHelper).startMediaService(eq(mContext), eq(BROADCAST_ID), eq(BROADCAST_TITLE));
+    }
+
+    @Test
+    public void testGetOnClickListener_startSubSettings() {
+        when(mController.getFragment()).thenReturn(mFragment);
+        when(mFragment.getMetricsCategory()).thenReturn(AUDIO_STREAM_MAIN);
+
+        Preference.OnPreferenceClickListener listener = mInstance.getOnClickListener(mController);
+        assertThat(listener).isNotNull();
+
+        // mContext is not an Activity context, calling startActivity() from outside of an Activity
+        // context requires the FLAG_ACTIVITY_NEW_TASK flag, create a mock to avoid this
+        // AndroidRuntimeException.
+        Context activityContext = mock(Context.class);
+        when(mPreference.getContext()).thenReturn(activityContext);
+
+        listener.onPreferenceClick(mPreference);
+
+        ArgumentCaptor<Intent> argumentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(activityContext).startActivity(argumentCaptor.capture());
+
+        Intent intent = argumentCaptor.getValue();
+        assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
+                .isEqualTo(AudioStreamDetailsFragment.class.getName());
+        assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0))
+                .isEqualTo(R.string.audio_streams_detail_page_title);
+        assertThat(intent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 0))
+                .isEqualTo(AUDIO_STREAM_MAIN);
+
+        Bundle bundle = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
+        assertThat(bundle).isNotNull();
+        assertThat(bundle.getString(AudioStreamDetailsFragment.BROADCAST_NAME_ARG))
+                .isEqualTo(BROADCAST_TITLE);
+        assertThat(bundle.getInt(AudioStreamDetailsFragment.BROADCAST_ID_ARG))
+                .isEqualTo(BROADCAST_ID);
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SyncedStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SyncedStateTest.java
index e9eab50..2b19e20 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SyncedStateTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/SyncedStateTest.java
@@ -20,7 +20,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.mockito.Mockito.never;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
@@ -28,10 +28,16 @@
 import android.app.AlertDialog;
 import android.bluetooth.BluetoothLeBroadcastMetadata;
 import android.content.Context;
+import android.content.DialogInterface;
+import android.widget.Button;
+import android.widget.TextView;
 
 import androidx.preference.Preference;
 import androidx.test.core.app.ApplicationProvider;
 
+import com.android.settings.R;
+import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -42,7 +48,9 @@
 import org.mockito.junit.MockitoRule;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
 import org.robolectric.shadows.ShadowAlertDialog;
+import org.robolectric.shadows.ShadowLooper;
 
 @RunWith(RobolectricTestRunner.class)
 @Config(
@@ -51,6 +59,10 @@
         })
 public class SyncedStateTest {
     @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private static final String ENCRYPTED_METADATA =
+            "BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;"
+                    + "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;";
+    private static final String BROADCAST_TITLE = "title";
     @Mock private AudioStreamsProgressCategoryController mMockController;
     @Mock private AudioStreamPreference mMockPreference;
     @Mock private BluetoothLeBroadcastMetadata mMockMetadata;
@@ -105,18 +117,47 @@
 
     @Test
     public void testGetOnClickListener_isEncrypted_passwordDialogShowing() {
+        when(mMockPreference.getAudioStreamMetadata())
+                .thenReturn(
+                        BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
+                                ENCRYPTED_METADATA));
+        when(mMockPreference.getContext()).thenReturn(mMockContext);
+        when(mMockPreference.getTitle()).thenReturn(BROADCAST_TITLE);
+
         Preference.OnPreferenceClickListener listener =
                 mInstance.getOnClickListener(mMockController);
-        when(mMockPreference.getAudioStreamMetadata()).thenReturn(mMockMetadata);
-        when(mMockPreference.getContext()).thenReturn(mMockContext);
-        when(mMockMetadata.isEncrypted()).thenReturn(true);
+        assertThat(listener).isNotNull();
 
         listener.onPreferenceClick(mMockPreference);
         shadowMainLooper().idle();
 
         AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
+
         assertThat(dialog).isNotNull();
         assertThat(dialog.isShowing()).isTrue();
-        verify(mMockController, never()).handleSourceAddRequest(mMockPreference, mMockMetadata);
+
+        Button neutralButton = dialog.getButton(DialogInterface.BUTTON_NEUTRAL);
+        assertThat(neutralButton).isNotNull();
+        assertThat(neutralButton.getText().toString())
+                .isEqualTo(mMockContext.getString(android.R.string.cancel));
+
+        Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+        assertThat(positiveButton).isNotNull();
+        assertThat(positiveButton.getText().toString())
+                .isEqualTo(
+                        mMockContext.getString(R.string.bluetooth_connect_access_dialog_positive));
+
+        positiveButton.callOnClick();
+        ShadowLooper.idleMainLooper();
+        verify(mMockController).handleSourceAddRequest(any(), any());
+
+        ShadowAlertDialog shadowDialog = Shadow.extract(dialog);
+        TextView title = shadowDialog.getView().findViewById(R.id.broadcast_name_text);
+        assertThat(title).isNotNull();
+        assertThat(title.getText().toString()).isEqualTo(BROADCAST_TITLE);
+        assertThat(shadowDialog.getTitle().toString())
+                .isEqualTo(mMockContext.getString(R.string.find_broadcast_password_dialog_title));
+
+        dialog.cancel();
     }
 }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java
index 3eb07a4..813ed2b 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/WaitForSyncStateTest.java
@@ -16,22 +16,39 @@
 
 package com.android.settings.connecteddevice.audiosharing.audiostreams;
 
+import static android.app.settings.SettingsEnums.DIALOG_AUDIO_STREAM_MAIN_WAIT_FOR_SYNC_TIMEOUT;
+
+import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController.REQUEST_SCAN_BT_BROADCAST_QR_CODE;
 import static com.android.settings.connecteddevice.audiosharing.audiostreams.WaitForSyncState.AUDIO_STREAM_WAIT_FOR_SYNC_STATE_SUMMARY;
 import static com.android.settings.connecteddevice.audiosharing.audiostreams.WaitForSyncState.WAIT_FOR_SYNC_TIMEOUT_MILLIS;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.settings.SettingsEnums;
 import android.bluetooth.BluetoothLeBroadcastMetadata;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
 
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -43,19 +60,23 @@
 @RunWith(RobolectricTestRunner.class)
 public class WaitForSyncStateTest {
     @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    private final Context mContext = spy(ApplicationProvider.getApplicationContext());
     @Mock private AudioStreamPreference mMockPreference;
     @Mock private AudioStreamsProgressCategoryController mMockController;
     @Mock private AudioStreamsHelper mMockHelper;
     @Mock private BluetoothLeBroadcastMetadata mMockMetadata;
+    private FakeFeatureFactory mFeatureFactory;
     private WaitForSyncState mInstance;
 
     @Before
     public void setUp() {
-        mInstance = WaitForSyncState.getInstance();
+        mFeatureFactory = FakeFeatureFactory.setupForTest();
+        mInstance = new WaitForSyncState();
     }
 
     @Test
     public void testGetInstance() {
+        mInstance = WaitForSyncState.getInstance();
         assertThat(mInstance).isNotNull();
         assertThat(mInstance).isInstanceOf(AudioStreamStateHandler.class);
     }
@@ -93,12 +114,49 @@
                 .thenReturn(AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC);
         when(mMockPreference.getAudioStreamBroadcastId()).thenReturn(1);
         when(mMockPreference.getAudioStreamMetadata()).thenReturn(mMockMetadata);
+        when(mMockPreference.getContext()).thenReturn(mContext);
         when(mMockPreference.getSourceOriginForLogging())
-                .thenReturn(SourceOriginForLogging.UNKNOWN);
+                .thenReturn(SourceOriginForLogging.BROADCAST_SEARCH);
+        when(mMockController.getFragment()).thenReturn(mock(AudioStreamsDashboardFragment.class));
 
         mInstance.performAction(mMockPreference, mMockController, mMockHelper);
         ShadowLooper.idleMainLooper(WAIT_FOR_SYNC_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
 
         verify(mMockController).handleSourceLost(1);
+        verify(mFeatureFactory.metricsFeatureProvider)
+                .action(
+                        eq(mContext),
+                        eq(SettingsEnums.ACTION_AUDIO_STREAM_JOIN_FAILED_WAIT_FOR_SYNC_TIMEOUT),
+                        eq(SourceOriginForLogging.BROADCAST_SEARCH.ordinal()));
+        verify(mContext).getString(R.string.audio_streams_dialog_stream_is_not_available);
+        verify(mContext).getString(R.string.audio_streams_is_not_playing);
+        verify(mContext).getString(R.string.audio_streams_dialog_close);
+        verify(mContext).getString(R.string.audio_streams_dialog_retry);
+    }
+
+    @Test
+    public void testLaunchQrCodeScanFragment() {
+        // mContext is not an Activity context, calling startActivity() from outside of an Activity
+        // context requires the FLAG_ACTIVITY_NEW_TASK flag, create a mock to avoid this
+        // AndroidRuntimeException.
+        Context activityContext = mock(Context.class);
+        AudioStreamsDashboardFragment fragment = mock(AudioStreamsDashboardFragment.class);
+        mInstance.launchQrCodeScanFragment(activityContext, fragment);
+
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        ArgumentCaptor<Integer> requestCodeCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(fragment)
+                .startActivityForResult(intentCaptor.capture(), requestCodeCaptor.capture());
+
+        Intent intent = intentCaptor.getValue();
+        assertThat(intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT))
+                .isEqualTo(AudioStreamsQrCodeScanFragment.class.getName());
+        assertThat(intent.getIntExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE_RESID, 0))
+                .isEqualTo(R.string.audio_streams_main_page_scan_qr_code_title);
+        assertThat(intent.getIntExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, 0))
+                .isEqualTo(DIALOG_AUDIO_STREAM_MAIN_WAIT_FOR_SYNC_TIMEOUT);
+
+        int requestCode = requestCodeCaptor.getValue();
+        assertThat(requestCode).isEqualTo(REQUEST_SCAN_BT_BROADCAST_QR_CODE);
     }
 }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java
index 3a0a6c4..13c19ca 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/audiostreams/testshadows/ShadowAudioStreamsHelper.java
@@ -32,7 +32,7 @@
 import java.util.List;
 import java.util.Optional;
 
-@Implements(value = AudioStreamsHelper.class, callThroughByDefault = false)
+@Implements(value = AudioStreamsHelper.class, callThroughByDefault = true)
 public class ShadowAudioStreamsHelper {
     private static AudioStreamsHelper sMockHelper;
     @Nullable private static CachedBluetoothDevice sCachedBluetoothDevice;
diff --git a/tests/robotests/src/com/android/settings/inputmethod/PointerScaleSeekBarControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/PointerScaleSeekBarControllerTest.java
new file mode 100644
index 0000000..152649f
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/inputmethod/PointerScaleSeekBarControllerTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.inputmethod;
+
+import static android.view.flags.Flags.enableVectorCursorA11ySettings;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.widget.SeekBar;
+
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.testutils.shadow.ShadowSystemSettings;
+import com.android.settings.widget.LabeledSeekBarPreference;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link PointerScaleSeekBarController} */
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {
+        ShadowSystemSettings.class,
+})
+public class PointerScaleSeekBarControllerTest {
+
+    private static final String PREFERENCE_KEY = "pointer_scale";
+
+    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock private PreferenceScreen mPreferenceScreen;
+
+    private Context mContext;
+    private LabeledSeekBarPreference mPreference;
+    private PointerScaleSeekBarController mController;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mPreference = new LabeledSeekBarPreference(mContext, null);
+        mController = new PointerScaleSeekBarController(mContext, PREFERENCE_KEY);
+    }
+
+    @Test
+    public void getAvailabilityStatus_flagEnabled() {
+        assumeTrue(enableVectorCursorA11ySettings());
+
+        assertEquals(mController.getAvailabilityStatus(), AVAILABLE);
+    }
+
+    @Test
+    public void onProgressChanged_changeListenerUpdatesSetting() {
+        when(mPreferenceScreen.findPreference(anyString())).thenReturn(mPreference);
+        mController.displayPreference(mPreferenceScreen);
+        SeekBar seekBar = mPreference.getSeekbar();
+        int sliderValue = 1;
+
+        mPreference.onProgressChanged(seekBar, sliderValue, false);
+
+        float expectedScale = 1.5f;
+        float currentScale = Settings.System.getFloatForUser(mContext.getContentResolver(),
+                Settings.System.POINTER_SCALE, -1, UserHandle.USER_CURRENT);
+        assertEquals(expectedScale, currentScale, /* delta= */ 0.001f);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/IconLoaderTest.java b/tests/robotests/src/com/android/settings/notification/modes/IconLoaderTest.java
deleted file mode 100644
index 7d4a367..0000000
--- a/tests/robotests/src/com/android/settings/notification/modes/IconLoaderTest.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.notification.modes;
-
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.app.AutomaticZenRule;
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.service.notification.ZenPolicy;
-
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.MoreExecutors;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-
-@RunWith(RobolectricTestRunner.class)
-public class IconLoaderTest {
-
-    private Context mContext;
-    private IconLoader mLoader;
-
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mLoader = new IconLoader(MoreExecutors.newDirectExecutorService());
-    }
-
-    @Test
-    public void getIcon_systemOwnedRuleWithIcon_loads() throws Exception {
-        AutomaticZenRule systemRule = newRuleBuilder()
-                .setPackage("android")
-                .setIconResId(android.R.drawable.ic_media_play)
-                .build();
-
-        ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, systemRule);
-        assertThat(loadFuture.isDone()).isTrue();
-        assertThat(loadFuture.get()).isNotNull();
-    }
-
-    @Test
-    public void getIcon_ruleWithoutSpecificIcon_loadsFallback() throws Exception {
-        AutomaticZenRule rule = newRuleBuilder()
-                .setType(AutomaticZenRule.TYPE_DRIVING)
-                .setPackage("com.blah")
-                .build();
-
-        ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
-        assertThat(loadFuture.isDone()).isTrue();
-        assertThat(loadFuture.get()).isNotNull();
-    }
-
-    @Test
-    public void getIcon_ruleWithAppIconWithLoadFailure_loadsFallback() throws Exception {
-        AutomaticZenRule rule = newRuleBuilder()
-                .setType(AutomaticZenRule.TYPE_DRIVING)
-                .setPackage("com.blah")
-                .setIconResId(-123456)
-                .build();
-
-        ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
-        assertThat(loadFuture.get()).isNotNull();
-    }
-
-    private static AutomaticZenRule.Builder newRuleBuilder() {
-        return new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                .setZenPolicy(new ZenPolicy.Builder().build());
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/InterruptionFilterPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/InterruptionFilterPreferenceControllerTest.java
index aeb1b8e..ff25322 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/InterruptionFilterPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/InterruptionFilterPreferenceControllerTest.java
@@ -18,27 +18,24 @@
 
 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-import static android.service.notification.ZenPolicy.STATE_ALLOW;
 import static android.service.notification.ZenPolicy.STATE_DISALLOW;
-import static android.service.notification.ZenPolicy.STATE_UNSET;
 
 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.verify;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenPolicy;
 
-import androidx.preference.Preference;
 import androidx.preference.TwoStatePreference;
 
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -72,12 +69,9 @@
     @Test
     public void testUpdateState_all() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_ALL)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALL)
+                .build();
         mController.updateZenMode(preference, zenMode);
 
         verify(preference).setChecked(false);
@@ -86,12 +80,9 @@
     @Test
     public void testOnPreferenceChange_fromAll() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_ALL)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAlarms(false).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setInterruptionFilter(INTERRUPTION_FILTER_ALL)
+                .build();
 
         mController.updateZenMode(preference, zenMode);
 
@@ -108,12 +99,10 @@
     @Test
     public void testUpdateState_priority() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
+                .build();
         mController.updateZenMode(preference, zenMode);
 
         verify(preference).setChecked(true);
@@ -122,12 +111,10 @@
     @Test
     public void testOnPreferenceChange_fromPriority() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAlarms(false).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder().allowAlarms(false).build())
+                .build();
 
         mController.updateZenMode(preference, zenMode);
 
diff --git a/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java b/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java
new file mode 100644
index 0000000..fa12b30
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.notification.modes;
+
+import android.app.AutomaticZenRule;
+import android.app.NotificationManager;
+import android.net.Uri;
+import android.service.notification.Condition;
+import android.service.notification.ZenDeviceEffects;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.notification.modes.ZenMode;
+
+import java.util.Random;
+
+class TestModeBuilder {
+
+    private String mId;
+    private AutomaticZenRule mRule;
+    private ZenModeConfig.ZenRule mConfigZenRule;
+
+    public static final ZenMode EXAMPLE = new TestModeBuilder().build();
+
+    TestModeBuilder() {
+        // Reasonable defaults
+        int id = new Random().nextInt(1000);
+        mId = "rule_" + id;
+        mRule = new AutomaticZenRule.Builder("Test Rule #" + id, Uri.parse("rule://" + id))
+                .setPackage("some_package")
+                .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
+                .build();
+        mConfigZenRule = new ZenModeConfig.ZenRule();
+        mConfigZenRule.enabled = true;
+        mConfigZenRule.pkg = "some_package";
+    }
+
+    TestModeBuilder setId(String id) {
+        mId = id;
+        return this;
+    }
+
+    TestModeBuilder setAzr(AutomaticZenRule rule) {
+        mRule = rule;
+        mConfigZenRule.pkg = rule.getPackageName();
+        mConfigZenRule.conditionId = rule.getConditionId();
+        mConfigZenRule.enabled = rule.isEnabled();
+        return this;
+    }
+
+    TestModeBuilder setConfigZenRule(ZenModeConfig.ZenRule configZenRule) {
+        mConfigZenRule = configZenRule;
+        return this;
+    }
+
+    public TestModeBuilder setName(String name) {
+        mRule.setName(name);
+        mConfigZenRule.name = name;
+        return this;
+    }
+
+    public TestModeBuilder setPackage(String pkg) {
+        mRule.setPackageName(pkg);
+        mConfigZenRule.pkg = pkg;
+        return this;
+    }
+
+    TestModeBuilder setConditionId(Uri conditionId) {
+        mRule.setConditionId(conditionId);
+        mConfigZenRule.conditionId = conditionId;
+        return this;
+    }
+
+    TestModeBuilder setType(@AutomaticZenRule.Type int type) {
+        mRule.setType(type);
+        mConfigZenRule.type = type;
+        return this;
+    }
+
+    TestModeBuilder setInterruptionFilter(
+            @NotificationManager.InterruptionFilter int interruptionFilter) {
+        mRule.setInterruptionFilter(interruptionFilter);
+        mConfigZenRule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
+                interruptionFilter, NotificationManager.INTERRUPTION_FILTER_PRIORITY);
+        return this;
+    }
+
+    TestModeBuilder setZenPolicy(@Nullable ZenPolicy policy) {
+        mRule.setZenPolicy(policy);
+        mConfigZenRule.zenPolicy = policy;
+        return this;
+    }
+
+    TestModeBuilder setDeviceEffects(@Nullable ZenDeviceEffects deviceEffects) {
+        mRule.setDeviceEffects(deviceEffects);
+        mConfigZenRule.zenDeviceEffects = deviceEffects;
+        return this;
+    }
+
+    public TestModeBuilder setEnabled(boolean enabled) {
+        mRule.setEnabled(enabled);
+        mConfigZenRule.enabled = enabled;
+        return this;
+    }
+
+    TestModeBuilder setManualInvocationAllowed(boolean allowed) {
+        mRule.setManualInvocationAllowed(allowed);
+        mConfigZenRule.allowManualInvocation = allowed;
+        return this;
+    }
+
+    public TestModeBuilder setTriggerDescription(@Nullable String triggerDescription) {
+        mRule.setTriggerDescription(triggerDescription);
+        mConfigZenRule.triggerDescription = triggerDescription;
+        return this;
+    }
+
+    TestModeBuilder setActive(boolean active) {
+        if (active) {
+            mConfigZenRule.enabled = true;
+            mConfigZenRule.condition = new Condition(mRule.getConditionId(), "...",
+                    Condition.STATE_TRUE);
+        } else {
+            mConfigZenRule.condition = null;
+        }
+        return this;
+    }
+
+    ZenMode build() {
+        return new ZenMode(mId, mRule, mConfigZenRule);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
index b199a2b..83f8de0 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java
@@ -17,8 +17,7 @@
 package com.android.settings.notification.modes;
 
 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-
-import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -29,12 +28,10 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
-import android.net.Uri;
 import android.os.Bundle;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
@@ -46,6 +43,8 @@
 import com.android.settings.SettingsActivity;
 import com.android.settingslib.applications.ApplicationsState;
 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.SelectorWithWidgetPreference;
 
 import org.junit.Before;
@@ -102,14 +101,13 @@
     }
 
     private ZenMode createPriorityChannelsZenMode() {
-        return new ZenMode("id", new AutomaticZenRule.Builder("Bedtime",
-                Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
+        return new TestModeBuilder()
+                .setId("id")
                 .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
                 .setZenPolicy(new ZenPolicy.Builder()
                         .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
                         .build())
-                .build(), true);
+                .build();
     }
 
     @Test
@@ -138,7 +136,7 @@
         Bundle bundle = launcherIntent.getBundleExtra(
                 SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
         assertThat(bundle).isNotNull();
-        assertThat(bundle.getString(MODE_ID)).isEqualTo("id");
+        assertThat(bundle.getString(EXTRA_AUTOMATIC_ZEN_RULE_ID)).isEqualTo("id");
     }
 
     @Test
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsPreferenceControllerTest.java
index b67d332..c96dbb6 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsPreferenceControllerTest.java
@@ -16,8 +16,6 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
-import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 
 import static com.android.settings.notification.modes.ZenModeAppsPreferenceController.KEY_NONE;
@@ -28,10 +26,8 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenPolicy;
@@ -41,6 +37,8 @@
 import androidx.preference.PreferenceScreen;
 import androidx.preference.TwoStatePreference;
 
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.SelectorWithWidgetPreference;
 
 import org.junit.Before;
@@ -107,13 +105,12 @@
     @Test
     public void testUpdateState_None() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
+                        .build())
+                .build();
+
         mNoneController.updateZenMode(preference, zenMode);
 
         verify(preference).setChecked(true);
@@ -122,13 +119,12 @@
     @Test
     public void testUpdateState_None_Unchecked() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
+                        .build())
+                .build();
+
         mNoneController.updateZenMode(preference, zenMode);
 
         verify(preference).setChecked(false);
@@ -137,13 +133,12 @@
     @Test
     public void testUpdateState_Priority() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
+                        .build())
+                .build();
+
         mPriorityController.updateZenMode(preference, zenMode);
 
         verify(preference).setChecked(true);
@@ -152,13 +147,12 @@
     @Test
     public void testUpdateState_Priority_Unchecked() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
+                        .build())
+                .build();
+
         mPriorityController.updateZenMode(preference, zenMode);
 
         verify(preference).setChecked(false);
@@ -166,21 +160,19 @@
 
     @Test
     public void testPreferenceClick_passesCorrectCheckedState_None() {
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
+                        .build())
+                .build();
 
         mNoneController.updateZenMode(mNonePref, zenMode);
         mPriorityController.updateZenMode(mPriorityPref, zenMode);
 
-        assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
-                .isChecked());
-        assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
-                .isChecked());
+        assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
+                .isChecked()).isFalse();
+        assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
+                .isChecked()).isTrue();
 
         // Click on NONE
         mPrefCategory.findPreference(KEY_NONE).performClick();
@@ -192,30 +184,31 @@
         // See AbstractZenModePreferenceController.
         assertThat(captor.getValue().getRule().getInterruptionFilter())
                 .isEqualTo(INTERRUPTION_FILTER_PRIORITY);
-        // NONE is now checked; others are unchecked.
+
+        // After screen is refreshed, NONE is now checked; others are unchecked.
+        mNoneController.updateZenMode(mNonePref, captor.getValue());
+        mPriorityController.updateZenMode(mPriorityPref, captor.getValue());
         assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
-                .isChecked());
-        assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
-                .isChecked());
+                .isChecked()).isTrue();
+        assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
+                .isChecked()).isFalse();
     }
 
     @Test
     public void testPreferenceClick_passesCorrectCheckedState_Priority() {
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
+                        .build())
+                .build();
 
         mNoneController.updateZenMode(mNonePref, zenMode);
         mPriorityController.updateZenMode(mPriorityPref, zenMode);
 
         assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
-                .isChecked());
-        assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
-                .isChecked());
+                .isChecked()).isTrue();
+        assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
+                .isChecked()).isFalse();
 
         // Click on PRIORITY
         mPrefCategory.findPreference(KEY_PRIORITY).performClick();
@@ -225,11 +218,13 @@
         // Checks the policy value for PRIORITY is propagated to the backend.
         assertThat(captor.getValue().getRule().getInterruptionFilter())
                 .isEqualTo(INTERRUPTION_FILTER_PRIORITY);
-        // PRIORITY is now checked; others are unchecked.
-        assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
-                .isChecked());
-        assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
-                .isChecked());
-    }
 
+        // After screen is refreshed, PRIORITY is now checked; others are unchecked.
+        mNoneController.updateZenMode(mNonePref, captor.getValue());
+        mPriorityController.updateZenMode(mPriorityPref, captor.getValue());
+        assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
+                .isChecked()).isTrue();
+        assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
+                .isChecked()).isFalse();
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeButtonPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeButtonPreferenceControllerTest.java
index bda3843..625f231 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeButtonPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeButtonPreferenceControllerTest.java
@@ -16,8 +16,6 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -25,15 +23,14 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
-import android.service.notification.ZenPolicy;
 import android.widget.Button;
 
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.LayoutPreference;
 
 import org.junit.Before;
@@ -71,43 +68,34 @@
 
     @Test
     public void isAvailable_notIfAppOptsOut() {
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                .setType(AutomaticZenRule.TYPE_DRIVING)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+        ZenMode zenMode = new TestModeBuilder()
                 .setManualInvocationAllowed(false)
-                .setEnabled(true)
-                .build(), false);
+                .build();
         mController.setZenMode(zenMode);
         assertThat(mController.isAvailable()).isFalse();
     }
 
     @Test
     public void isAvailable_notIfModeDisabled() {
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                        .setManualInvocationAllowed(true)
-                        .setEnabled(false)
-                        .build(), false);
+        ZenMode zenMode = new TestModeBuilder()
+                .setManualInvocationAllowed(true)
+                .setEnabled(false)
+                .build();
+
         mController.setZenMode(zenMode);
+
         assertThat(mController.isAvailable()).isFalse();
     }
 
     @Test
     public void isAvailable_appOptedIn_modeEnabled() {
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                        .setManualInvocationAllowed(true)
-                        .setEnabled(true)
-                        .build(), false);
+        ZenMode zenMode = new TestModeBuilder()
+                .setManualInvocationAllowed(true)
+                .setEnabled(true)
+                .build();
+
         mController.setZenMode(zenMode);
+
         assertThat(mController.isAvailable()).isTrue();
     }
 
@@ -116,15 +104,13 @@
         Button button = new Button(mContext);
         LayoutPreference pref = mock(LayoutPreference.class);
         when(pref.findViewById(anyInt())).thenReturn(button);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                        .setManualInvocationAllowed(true)
-                        .setEnabled(true)
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setManualInvocationAllowed(true)
+                .setActive(true)
+                .build();
+
         mController.updateZenMode(pref, zenMode);
+
         assertThat(button.getText().toString()).contains("off");
         assertThat(button.hasOnClickListeners()).isTrue();
     }
@@ -134,15 +120,13 @@
         Button button = new Button(mContext);
         LayoutPreference pref = mock(LayoutPreference.class);
         when(pref.findViewById(anyInt())).thenReturn(button);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                        .setManualInvocationAllowed(true)
-                        .setEnabled(true)
-                        .build(), false);
+        ZenMode zenMode = new TestModeBuilder()
+                .setManualInvocationAllowed(true)
+                .setActive(false)
+                .build();
+
         mController.updateZenMode(pref, zenMode);
+
         assertThat(button.getText().toString()).contains("on");
         assertThat(button.hasOnClickListeners()).isTrue();
     }
@@ -152,14 +136,11 @@
         Button button = new Button(mContext);
         LayoutPreference pref = mock(LayoutPreference.class);
         when(pref.findViewById(anyInt())).thenReturn(button);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                        .setManualInvocationAllowed(true)
-                        .setEnabled(true)
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setManualInvocationAllowed(true)
+                .setActive(true)
+                .build();
+
         mController.updateZenMode(pref, zenMode);
 
         button.callOnClick();
@@ -171,14 +152,11 @@
         Button button = new Button(mContext);
         LayoutPreference pref = mock(LayoutPreference.class);
         when(pref.findViewById(anyInt())).thenReturn(button);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                        .setManualInvocationAllowed(true)
-                        .setEnabled(true)
-                        .build(), false);
+        ZenMode zenMode = new TestModeBuilder()
+                .setManualInvocationAllowed(true)
+                .setActive(false)
+                .build();
+
         mController.updateZenMode(pref, zenMode);
 
         button.callOnClick();
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java
index 94c2d8a..058b2d7 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java
@@ -16,22 +16,19 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
-import android.service.notification.ZenPolicy;
 
 import androidx.preference.Preference;
 
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -67,13 +64,7 @@
     @EnableFlags(Flags.FLAG_MODES_UI)
     public void testHasSummary() {
         Preference pref = mock(Preference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                .setType(AutomaticZenRule.TYPE_DRIVING)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                .build(), true);
-        mController.updateZenMode(pref, zenMode);
+        mController.updateZenMode(pref, TestModeBuilder.EXAMPLE);
         verify(pref).setSummary(any());
     }
 }
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceControllerTest.java
index 1a62b75..a735cd9 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceControllerTest.java
@@ -16,22 +16,22 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-import static android.service.notification.ZenPolicy.STATE_ALLOW;
-import static android.service.notification.ZenPolicy.STATE_UNSET;
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenDeviceEffects;
-import android.service.notification.ZenPolicy;
+
 import androidx.preference.TwoStatePreference;
+
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -62,15 +62,11 @@
     @Test
     public void testUpdateState_grayscale() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
-                        .setDeviceEffects(new ZenDeviceEffects.Builder()
-                                .setShouldDisplayGrayscale(true)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(true)
+                        .build())
+                .build();
 
         ZenModeDisplayEffectPreferenceController controller =
                 new ZenModeDisplayEffectPreferenceController(
@@ -84,15 +80,11 @@
     @Test
     public void testOnPreferenceChange_grayscale() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAlarms(false).build())
-                        .setDeviceEffects(new ZenDeviceEffects.Builder()
-                                .setShouldDisplayGrayscale(true)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(true)
+                        .build())
+                .build();
 
         ZenModeDisplayEffectPreferenceController controller =
                 new ZenModeDisplayEffectPreferenceController(mContext, "effect_greyscale", mBackend);
@@ -103,22 +95,18 @@
 
         ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
         verify(mBackend).updateMode(captor.capture());
-        assertThat(captor.getValue().getRule().getDeviceEffects().shouldDisplayGrayscale())
+        assertThat(captor.getValue().getDeviceEffects().shouldDisplayGrayscale())
                 .isFalse();
     }
 
     @Test
     public void testUpdateState_aod() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowMedia(true).build())
-                        .setDeviceEffects(new ZenDeviceEffects.Builder()
-                                .setShouldSuppressAmbientDisplay(true)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldSuppressAmbientDisplay(true)
+                        .build())
+                .build();
 
         ZenModeDisplayEffectPreferenceController controller =
                 new ZenModeDisplayEffectPreferenceController(mContext, "effect_aod", mBackend);
@@ -131,15 +119,11 @@
     @Test
     public void testOnPreferenceChange_aod() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowMedia(false).build())
-                        .setDeviceEffects(new ZenDeviceEffects.Builder()
-                                .setShouldSuppressAmbientDisplay(true)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldSuppressAmbientDisplay(true)
+                        .build())
+                .build();
 
         ZenModeDisplayEffectPreferenceController controller =
                 new ZenModeDisplayEffectPreferenceController(mContext, "effect_aod", mBackend);
@@ -150,22 +134,18 @@
 
         ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
         verify(mBackend).updateMode(captor.capture());
-        assertThat(captor.getValue().getRule().getDeviceEffects().shouldSuppressAmbientDisplay())
+        assertThat(captor.getValue().getDeviceEffects().shouldSuppressAmbientDisplay())
                 .isFalse();
     }
 
     @Test
     public void testUpdateState_wallpaper() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowSystem(true).build())
-                        .setDeviceEffects(new ZenDeviceEffects.Builder()
-                                .setShouldDimWallpaper(true)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDimWallpaper(true)
+                        .build())
+                .build();
 
         ZenModeDisplayEffectPreferenceController controller =
                 new ZenModeDisplayEffectPreferenceController(
@@ -179,15 +159,11 @@
     @Test
     public void testOnPreferenceChange_wallpaper() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowSystem(false).build())
-                        .setDeviceEffects(new ZenDeviceEffects.Builder()
-                                .setShouldDimWallpaper(true)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDimWallpaper(true)
+                        .build())
+                .build();
 
         ZenModeDisplayEffectPreferenceController controller =
                 new ZenModeDisplayEffectPreferenceController(
@@ -199,21 +175,17 @@
 
         ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
         verify(mBackend).updateMode(captor.capture());
-        assertThat(captor.getValue().getRule().getDeviceEffects().shouldDimWallpaper()).isFalse();
+        assertThat(captor.getValue().getDeviceEffects().shouldDimWallpaper()).isFalse();
     }
 
     @Test
     public void testUpdateState_darkTheme() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowReminders(true).build())
-                        .setDeviceEffects(new ZenDeviceEffects.Builder()
-                                .setShouldUseNightMode(true)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldUseNightMode(true)
+                        .build())
+                .build();
 
         ZenModeDisplayEffectPreferenceController controller =
                 new ZenModeDisplayEffectPreferenceController(mContext, "effect_dark_theme",
@@ -227,15 +199,11 @@
     @Test
     public void testOnPreferenceChange_darkTheme() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowReminders(false).build())
-                        .setDeviceEffects(new ZenDeviceEffects.Builder()
-                                .setShouldUseNightMode(true)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldUseNightMode(true)
+                        .build())
+                .build();
 
         ZenModeDisplayEffectPreferenceController controller =
                 new ZenModeDisplayEffectPreferenceController(mContext, "effect_dark_theme",
@@ -247,6 +215,6 @@
 
         ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
         verify(mBackend).updateMode(captor.capture());
-        assertThat(captor.getValue().getRule().getDeviceEffects().shouldUseNightMode()).isFalse();
+        assertThat(captor.getValue().getDeviceEffects().shouldUseNightMode()).isFalse();
     }
 }
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceControllerTest.java
index 62aa046..3ccfb9f 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceControllerTest.java
@@ -16,22 +16,19 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
-import android.service.notification.ZenPolicy;
 
 import androidx.preference.Preference;
 
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -67,13 +64,7 @@
     @EnableFlags(Flags.FLAG_MODES_UI)
     public void testHasSummary() {
         Preference pref = mock(Preference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                .setType(AutomaticZenRule.TYPE_DRIVING)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                .build(), true);
-        mController.updateZenMode(pref, zenMode);
+        mController.updateZenMode(pref, TestModeBuilder.EXAMPLE);
         verify(pref).setSummary(any());
     }
 }
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceControllerTest.java
index c1c4d61..03c75fb 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeExitAtAlarmPreferenceControllerTest.java
@@ -21,13 +21,15 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.app.AutomaticZenRule;
 import android.content.Context;
 import android.service.notification.ZenModeConfig;
 
 import androidx.preference.TwoStatePreference;
 import androidx.test.core.app.ApplicationProvider;
 
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -65,10 +67,9 @@
         scheduleInfo.endHour = 2;
         scheduleInfo.exitAtAlarm = false;
 
-        ZenMode mode = new ZenMode("id",
-                new AutomaticZenRule.Builder("name",
-                        ZenModeConfig.toScheduleConditionId(scheduleInfo)).build(),
-                true);  // is active
+        ZenMode mode = new TestModeBuilder()
+                .setConditionId(ZenModeConfig.toScheduleConditionId(scheduleInfo))
+                .build();
 
         // need to call updateZenMode for the first call
         mPrefController.updateZenMode(preference, mode);
@@ -94,10 +95,9 @@
         scheduleInfo.endHour = 2;
         scheduleInfo.exitAtAlarm = true;
 
-        ZenMode mode = new ZenMode("id",
-                new AutomaticZenRule.Builder("name",
-                        ZenModeConfig.toScheduleConditionId(scheduleInfo)).build(),
-                true);  // is active
+        ZenMode mode = new TestModeBuilder()
+                .setConditionId(ZenModeConfig.toScheduleConditionId(scheduleInfo))
+                .build();
         mPrefController.updateZenMode(preference, mode);
 
         // turn off exit at alarm
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java
index ba9a6b8..5db7e92 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java
@@ -23,9 +23,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.AutomaticZenRule;
 import android.content.Context;
-import android.net.Uri;
 
 import androidx.annotation.NonNull;
 import androidx.preference.PreferenceScreen;
@@ -33,6 +31,8 @@
 
 import com.android.settings.R;
 import com.android.settings.dashboard.DashboardFragment;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.LayoutPreference;
 
 import com.google.common.collect.ImmutableList;
@@ -47,10 +47,7 @@
 @RunWith(RobolectricTestRunner.class)
 public class ZenModeIconPickerListPreferenceControllerTest {
 
-    private static final ZenMode ZEN_MODE = new ZenMode(
-            "mode_id",
-            new AutomaticZenRule.Builder("mode name", Uri.parse("mode")).build(),
-            /* isActive= */ false);
+    private static final ZenMode ZEN_MODE = TestModeBuilder.EXAMPLE;
 
     private ZenModesBackend mBackend;
     private ZenModeIconPickerListPreferenceController mController;
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java
index 9400f83..288359a 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java
@@ -16,22 +16,19 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
-import android.service.notification.ZenPolicy;
 
 import androidx.preference.Preference;
 
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -67,13 +64,7 @@
     @EnableFlags(Flags.FLAG_MODES_UI)
     public void testHasSummary() {
         Preference pref = mock(Preference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                .setType(AutomaticZenRule.TYPE_DRIVING)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                .build(), true);
-        mController.updateZenMode(pref, zenMode);
+        mController.updateZenMode(pref, TestModeBuilder.EXAMPLE);
         verify(pref).setSummary(any());
     }
 }
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceControllerTest.java
index 00a9fbe..ee7340b 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceControllerTest.java
@@ -16,22 +16,19 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
-import android.service.notification.ZenPolicy;
 
 import androidx.preference.Preference;
 
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -67,13 +64,7 @@
     @EnableFlags(Flags.FLAG_MODES_UI)
     public void testHasSummary() {
         Preference pref = mock(Preference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                .setType(AutomaticZenRule.TYPE_DRIVING)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                .build(), true);
-        mController.updateZenMode(pref, zenMode);
+        mController.updateZenMode(pref, TestModeBuilder.EXAMPLE);
         verify(pref).setSummary(any());
     }
 }
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceControllerTest.java
index 54edaf4..b23d946 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceControllerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 import static android.service.notification.ZenPolicy.STATE_ALLOW;
 import static android.service.notification.ZenPolicy.STATE_DISALLOW;
 import static android.service.notification.ZenPolicy.VISUAL_EFFECT_LIGHTS;
@@ -32,17 +31,18 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
 import android.content.res.Resources;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenPolicy;
 
 import androidx.preference.TwoStatePreference;
 
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -96,15 +96,12 @@
     @Test
     public void updateState_notChecked() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowAlarms(true)
-                                .showAllVisualEffects()
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowAlarms(true)
+                        .showAllVisualEffects()
+                        .build())
+                .build();
 
         mController.updateZenMode(preference, zenMode);
 
@@ -115,15 +112,12 @@
     @Test
     public void updateState_checked() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowAlarms(true)
-                                .showVisualEffect(VISUAL_EFFECT_PEEK, false)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowAlarms(true)
+                        .showVisualEffect(VISUAL_EFFECT_PEEK, false)
+                        .build())
+                .build();
 
         mController.updateZenMode(preference, zenMode);
 
@@ -138,16 +132,13 @@
                 "zen_effect_status", VISUAL_EFFECT_STATUS_BAR,
                 new int[]{VISUAL_EFFECT_NOTIFICATION_LIST}, mBackend);
 
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowAlarms(true)
-                                .showVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, false)
-                                .showVisualEffect(VISUAL_EFFECT_STATUS_BAR, true)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowAlarms(true)
+                        .showVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, false)
+                        .showVisualEffect(VISUAL_EFFECT_STATUS_BAR, true)
+                        .build())
+                .build();
 
         mController.updateZenMode(preference, zenMode);
 
@@ -168,15 +159,12 @@
                 "zen_effect_status", VISUAL_EFFECT_STATUS_BAR,
                 new int[]{VISUAL_EFFECT_NOTIFICATION_LIST}, mBackend);
 
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowAlarms(true)
-                                .showAllVisualEffects()
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowAlarms(true)
+                        .showAllVisualEffects()
+                        .build())
+                .build();
 
         mController.updateZenMode(preference, zenMode);
 
@@ -188,15 +176,12 @@
     @Test
     public void onPreferenceChanged_checkedFalse() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowAlarms(true)
-                                .hideAllVisualEffects()
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowAlarms(true)
+                        .hideAllVisualEffects()
+                        .build())
+                .build();
 
         mController.updateZenMode(preference, zenMode);
 
@@ -213,15 +198,12 @@
     @Test
     public void onPreferenceChanged_checkedTrue() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowAlarms(true)
-                                .showAllVisualEffects()
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowAlarms(true)
+                        .showAllVisualEffects()
+                        .build())
+                .build();
 
         mController.updateZenMode(preference, zenMode);
 
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java
index 699762e..c4d03fe 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java
@@ -16,22 +16,19 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
-import android.service.notification.ZenPolicy;
 
 import androidx.preference.Preference;
 
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -68,13 +65,7 @@
     @EnableFlags(Flags.FLAG_MODES_UI)
     public void testHasSummary() {
         Preference pref = mock(Preference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                .setType(AutomaticZenRule.TYPE_DRIVING)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                .build(), true);
-        mController.updateZenMode(pref, zenMode);
+        mController.updateZenMode(pref, TestModeBuilder.EXAMPLE);
         verify(pref).setSummary(any());
     }
 }
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherPreferenceControllerTest.java
index 4a4a6e4..c69a8a0 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherPreferenceControllerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 import static android.service.notification.ZenPolicy.STATE_ALLOW;
 import static android.service.notification.ZenPolicy.STATE_UNSET;
 
@@ -25,16 +24,17 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenPolicy;
 
 import androidx.preference.TwoStatePreference;
 
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -65,12 +65,9 @@
     @Test
     public void testUpdateState_alarms() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
+                .build();
 
         ZenModeOtherPreferenceController controller =
                 new ZenModeOtherPreferenceController(mContext, "modes_category_alarm", mBackend);
@@ -83,12 +80,9 @@
     @Test
     public void testOnPreferenceChange_alarms() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAlarms(false).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowAlarms(false).build())
+                .build();
 
         ZenModeOtherPreferenceController controller =
                 new ZenModeOtherPreferenceController(mContext, "modes_category_alarm", mBackend);
@@ -108,12 +102,9 @@
     @Test
     public void testUpdateState_media() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowMedia(true).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowMedia(true).build())
+                .build();
 
         ZenModeOtherPreferenceController controller =
                 new ZenModeOtherPreferenceController(mContext, "modes_category_media", mBackend);
@@ -126,12 +117,9 @@
     @Test
     public void testOnPreferenceChange_media() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowMedia(false).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowMedia(false).build())
+                .build();
 
         ZenModeOtherPreferenceController controller =
                 new ZenModeOtherPreferenceController(mContext, "modes_category_media", mBackend);
@@ -151,12 +139,9 @@
     @Test
     public void testUpdateState_system() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowSystem(true).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowSystem(true).build())
+                .build();
 
         ZenModeOtherPreferenceController controller =
                 new ZenModeOtherPreferenceController(mContext, "modes_category_system", mBackend);
@@ -169,12 +154,9 @@
     @Test
     public void testOnPreferenceChange_system() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowSystem(false).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowSystem(false).build())
+                .build();
 
         ZenModeOtherPreferenceController controller =
                 new ZenModeOtherPreferenceController(mContext, "modes_category_system", mBackend);
@@ -194,12 +176,9 @@
     @Test
     public void testUpdateState_reminders() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowReminders(true).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowReminders(true).build())
+                .build();
 
         ZenModeOtherPreferenceController controller =
                 new ZenModeOtherPreferenceController(mContext, "modes_category_reminders",
@@ -213,12 +192,9 @@
     @Test
     public void testOnPreferenceChange_reminders() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowReminders(false).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowReminders(false).build())
+                .build();
 
         ZenModeOtherPreferenceController controller =
                 new ZenModeOtherPreferenceController(mContext, "modes_category_reminders",
@@ -239,12 +215,9 @@
     @Test
     public void testUpdateState_events() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowEvents(true).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowEvents(true).build())
+                .build();
 
         ZenModeOtherPreferenceController controller =
                 new ZenModeOtherPreferenceController(mContext, "modes_category_events", mBackend);
@@ -257,12 +230,9 @@
     @Test
     public void testOnPreferenceChange_events() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowEvents(false).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowEvents(false).build())
+                .build();
 
         ZenModeOtherPreferenceController controller =
                 new ZenModeOtherPreferenceController(mContext, "modes_category_events", mBackend);
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java
index a331318..6591b72 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java
@@ -16,22 +16,19 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
-import android.service.notification.ZenPolicy;
 
 import androidx.preference.Preference;
 
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -69,13 +66,7 @@
     @EnableFlags(Flags.FLAG_MODES_UI)
     public void testHasSummary() {
         Preference pref = mock(Preference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                .setType(AutomaticZenRule.TYPE_DRIVING)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                .build(), true);
-        mController.updateZenMode(pref, zenMode);
+        mController.updateZenMode(pref, TestModeBuilder.EXAMPLE);
         verify(pref).setSummary(any());
     }
 }
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java
index 709af43..04df27e 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE;
@@ -40,11 +39,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
 import android.database.Cursor;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenPolicy;
@@ -54,6 +51,8 @@
 import androidx.preference.PreferenceManager;
 import androidx.preference.PreferenceScreen;
 
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.widget.SelectorWithWidgetPreference;
 
 import org.junit.Before;
@@ -439,20 +438,17 @@
 
     @Test
     public void testPreferenceClick_passesCorrectCheckedState_startingUnchecked_messages() {
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .disallowAllSounds()
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .disallowAllSounds()
+                        .build())
+                .build();
 
         mMessagesController.displayPreference(mPreferenceScreen);
         mMessagesController.updateZenMode(mMessagesPrefCategory, zenMode);
 
         assertThat(((SelectorWithWidgetPreference) mMessagesPrefCategory.findPreference(KEY_NONE))
-                .isChecked());
+                .isChecked()).isTrue();
 
         mMessagesPrefCategory.findPreference(KEY_STARRED).performClick();
 
@@ -464,14 +460,11 @@
 
     @Test
     public void testPreferenceClick_passesCorrectCheckedState_startingChecked_messages() {
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowAllSounds()
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowAllSounds()
+                        .build())
+                .build();
 
         mMessagesController.displayPreference(mPreferenceScreen);
         mMessagesController.updateZenMode(mMessagesPrefCategory, zenMode);
@@ -490,14 +483,11 @@
 
     @Test
     public void testPreferenceClick_passesCorrectCheckedState_startingUnchecked_calls() {
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .disallowAllSounds()
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .disallowAllSounds()
+                        .build())
+                .build();
 
         mCallsController.displayPreference(mPreferenceScreen);
         mCallsController.updateZenMode(mCallsPrefCategory, zenMode);
@@ -515,14 +505,11 @@
 
     @Test
     public void testPreferenceClick_passesCorrectCheckedState_startingChecked_calls() {
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .disallowAllSounds()
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .disallowAllSounds()
+                        .build())
+                .build();
 
         mCallsController.displayPreference(mPreferenceScreen);
         mCallsController.updateZenMode(mCallsPrefCategory, zenMode);
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceControllerTest.java
index 7bbb042..c1b99e5 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceControllerTest.java
@@ -16,24 +16,27 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
-import static android.service.notification.ZenPolicy.STATE_ALLOW;
 import static android.service.notification.ZenPolicy.STATE_DISALLOW;
 import static android.service.notification.ZenPolicy.STATE_UNSET;
+
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.ZenPolicy;
+
 import androidx.preference.TwoStatePreference;
+
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -64,14 +67,11 @@
     @Test
     public void testUpdateState_allCalls() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowCalls(PEOPLE_TYPE_ANYONE)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowCalls(PEOPLE_TYPE_ANYONE)
+                        .build())
+                .build();
 
         ZenModeRepeatCallersPreferenceController controller =
                 new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1);
@@ -85,15 +85,12 @@
     @Test
     public void testUpdateState_someCalls() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder()
-                                .allowCalls(PEOPLE_TYPE_STARRED)
-                                .allowRepeatCallers(true)
-                                .build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .allowCalls(PEOPLE_TYPE_STARRED)
+                        .allowRepeatCallers(true)
+                        .build())
+                .build();
 
         ZenModeRepeatCallersPreferenceController controller =
                 new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1);
@@ -107,12 +104,9 @@
     @Test
     public void testOnPreferenceChange() {
         TwoStatePreference preference = mock(TwoStatePreference.class);
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build())
-                        .build(), true);
+        ZenMode zenMode = new TestModeBuilder()
+                .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build())
+                .build();
 
         ZenModeRepeatCallersPreferenceController controller =
                 new ZenModeRepeatCallersPreferenceController(mContext, "repeat", mBackend, 1);
@@ -130,4 +124,4 @@
         assertThat(captor.getValue().getPolicy().getPriorityCallSenders())
                 .isEqualTo(STATE_UNSET);
     }
-}
\ No newline at end of file
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java
index 0ede058..cc6a497 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetCalendarPreferenceControllerTest.java
@@ -28,10 +28,8 @@
 
 import static org.mockito.Mockito.when;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.SystemZenRules;
@@ -41,6 +39,9 @@
 import androidx.preference.PreferenceCategory;
 import androidx.test.core.app.ApplicationProvider;
 
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -85,11 +86,9 @@
     @Test
     @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
     public void updateEventMode_updatesConditionAndTriggerDescription() {
-        ZenMode mode = new ZenMode("id",
-                new AutomaticZenRule.Builder("name", Uri.parse("condition"))
-                        .setPackage(SystemZenRules.PACKAGE_ANDROID)
-                        .build(),
-                true);  // is active
+        ZenMode mode = new TestModeBuilder()
+                .setPackage(SystemZenRules.PACKAGE_ANDROID)
+                .build();
 
         // Explicitly update preference controller with mode info first, which will also call
         // updateState()
@@ -115,10 +114,9 @@
         eventInfo.calName = "Definitely A Calendar";
         eventInfo.reply = REPLY_YES;
 
-        ZenMode mode = new ZenMode("id",
-                new AutomaticZenRule.Builder("name",
-                        ZenModeConfig.toEventConditionId(eventInfo)).build(),
-                true);  // is active
+        ZenMode mode = new TestModeBuilder()
+                .setConditionId(ZenModeConfig.toEventConditionId(eventInfo))
+                .build();
         mPrefController.updateZenMode(mPrefCategory, mode);
 
         // We should see mCalendar, mReply have their values set
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java
index 5f492b9..7dbc802 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetSchedulePreferenceControllerTest.java
@@ -23,10 +23,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
-import android.net.Uri;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.SystemZenRules;
@@ -38,6 +36,8 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.settings.R;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -81,11 +81,9 @@
     @Test
     @EnableFlags({Flags.FLAG_MODES_API, Flags.FLAG_MODES_UI})
     public void updateScheduleRule_updatesConditionAndTriggerDescription() {
-        ZenMode mode = new ZenMode("id",
-                new AutomaticZenRule.Builder("name", Uri.parse("condition"))
-                        .setPackage(SystemZenRules.PACKAGE_ANDROID)
-                        .build(),
-                true);  // is active
+        ZenMode mode = new TestModeBuilder()
+                .setPackage(SystemZenRules.PACKAGE_ANDROID)
+                .build();
 
         ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
         scheduleInfo.days = new int[] { Calendar.MONDAY };
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java
index ff4d4a3..31959e5 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeSetTriggerLinkPreferenceControllerTest.java
@@ -37,7 +37,6 @@
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.service.notification.SystemZenRules;
 import android.service.notification.ZenModeConfig;
-import android.service.notification.ZenPolicy;
 
 import androidx.preference.PreferenceCategory;
 import androidx.test.core.app.ApplicationProvider;
@@ -46,6 +45,8 @@
 import com.android.settings.SettingsActivity;
 import com.android.settings.dashboard.DashboardFragment;
 import com.android.settingslib.PrimarySwitchPreference;
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -100,26 +101,13 @@
         assertThat(mPrefController.isAvailable()).isFalse();
 
         // should be available for other modes
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
-                        .setEnabled(false)
-                        .build(), false);
-        mPrefController.updateZenMode(mPrefCategory, zenMode);
+        mPrefController.updateZenMode(mPrefCategory, TestModeBuilder.EXAMPLE);
         assertThat(mPrefController.isAvailable()).isTrue();
     }
 
     @Test
     public void testUpdateState() {
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
-                        .setEnabled(false)
-                        .build(), false);
+        ZenMode zenMode = new TestModeBuilder().setEnabled(false).build();
 
         // Update preference controller with a zen mode that is not enabled
         mPrefController.updateZenMode(mPrefCategory, zenMode);
@@ -133,13 +121,7 @@
 
     @Test
     public void testOnPreferenceChange() {
-        ZenMode zenMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                        .setType(AutomaticZenRule.TYPE_DRIVING)
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
-                        .setEnabled(false)
-                        .build(), false);
+        ZenMode zenMode = new TestModeBuilder().setEnabled(false).build();
 
         // start with disabled rule
         mPrefController.updateZenMode(mPrefCategory, zenMode);
@@ -158,13 +140,12 @@
         ZenModeConfig.EventInfo eventInfo = new ZenModeConfig.EventInfo();
         eventInfo.calendarId = 1L;
         eventInfo.calName = "My events";
-        ZenMode mode = new ZenMode("id", new AutomaticZenRule.Builder("name",
-                ZenModeConfig.toEventConditionId(eventInfo))
+        ZenMode mode = new TestModeBuilder()
                 .setPackage(SystemZenRules.PACKAGE_ANDROID)
+                .setConditionId(ZenModeConfig.toEventConditionId(eventInfo))
                 .setType(TYPE_SCHEDULE_CALENDAR)
                 .setTriggerDescription("My events")
-                .build(),
-                true);  // is active
+                .build();
         mPrefController.updateZenMode(mPrefCategory, mode);
 
         assertThat(mPreference.getTitle()).isNotNull();
@@ -186,13 +167,12 @@
         scheduleInfo.days = new int[] { Calendar.MONDAY, Calendar.TUESDAY, Calendar.THURSDAY };
         scheduleInfo.startHour = 1;
         scheduleInfo.endHour = 15;
-        ZenMode mode = new ZenMode("id", new AutomaticZenRule.Builder("name",
-                ZenModeConfig.toScheduleConditionId(scheduleInfo))
+        ZenMode mode = new TestModeBuilder()
+                .setConditionId(ZenModeConfig.toScheduleConditionId(scheduleInfo))
                 .setPackage(SystemZenRules.PACKAGE_ANDROID)
                 .setType(TYPE_SCHEDULE_TIME)
                 .setTriggerDescription("some schedule")
-                .build(),
-                true);  // is active
+                .build();
         mPrefController.updateZenMode(mPrefCategory, mode);
 
         assertThat(mPreference.getTitle()).isNotNull();
@@ -210,13 +190,12 @@
 
     @Test
     public void testRuleLink_manual() {
-        ZenMode mode = new ZenMode("id", new AutomaticZenRule.Builder("name",
-                ZenModeConfig.toCustomManualConditionId())
+        ZenMode mode = new TestModeBuilder()
+                .setConditionId(ZenModeConfig.toCustomManualConditionId())
                 .setPackage(SystemZenRules.PACKAGE_ANDROID)
                 .setType(TYPE_OTHER)
                 .setTriggerDescription("Will not be shown")
-                .build(),
-                true);  // is active
+                .build();
         mPrefController.updateZenMode(mPrefCategory, mode);
 
         assertThat(mPreference.getTitle()).isNotNull();
@@ -232,13 +211,12 @@
 
     @Test
     public void onScheduleChosen_updatesMode() {
-        ZenMode originalMode = new ZenMode("id",
-                new AutomaticZenRule.Builder("name", ZenModeConfig.toCustomManualConditionId())
-                        .setPackage(SystemZenRules.PACKAGE_ANDROID)
-                        .setType(TYPE_OTHER)
-                        .setTriggerDescription("")
-                        .build(),
-                false);
+        ZenMode originalMode = new TestModeBuilder()
+                .setConditionId(ZenModeConfig.toCustomManualConditionId())
+                .setPackage(SystemZenRules.PACKAGE_ANDROID)
+                .setType(TYPE_OTHER)
+                .setTriggerDescription("")
+                .build();
         mPrefController.updateZenMode(mPrefCategory, originalMode);
 
         ZenModeConfig.ScheduleInfo scheduleInfo = new ZenModeConfig.ScheduleInfo();
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeTest.java
deleted file mode 100644
index 37b71a5..0000000
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeTest.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.notification.modes;
-
-import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
-import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
-import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.app.AutomaticZenRule;
-import android.net.Uri;
-import android.service.notification.ZenPolicy;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-
-@RunWith(RobolectricTestRunner.class)
-public class ZenModeTest {
-
-    private static final ZenPolicy ZEN_POLICY = new ZenPolicy.Builder().allowAllSounds().build();
-
-    private static final AutomaticZenRule ZEN_RULE =
-            new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                    .setType(AutomaticZenRule.TYPE_DRIVING)
-                    .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                    .setZenPolicy(ZEN_POLICY)
-                    .build();
-
-    @Test
-    public void testBasicMethods() {
-        ZenMode zenMode = new ZenMode("id", ZEN_RULE, true);
-
-        assertThat(zenMode.getId()).isEqualTo("id");
-        assertThat(zenMode.getRule()).isEqualTo(ZEN_RULE);
-        assertThat(zenMode.isManualDnd()).isFalse();
-        assertThat(zenMode.canBeDeleted()).isTrue();
-        assertThat(zenMode.isActive()).isTrue();
-
-        ZenMode manualMode = ZenMode.manualDndMode(ZEN_RULE, false);
-        assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID);
-        assertThat(manualMode.isManualDnd()).isTrue();
-        assertThat(manualMode.canBeDeleted()).isFalse();
-        assertThat(manualMode.isActive()).isFalse();
-    }
-
-    @Test
-    public void getPolicy_interruptionFilterPriority_returnsZenPolicy() {
-        ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                .setZenPolicy(ZEN_POLICY)
-                .build(), false);
-
-        assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
-    }
-
-    @Test
-    public void getPolicy_interruptionFilterAlarms_returnsPolicyAllowingAlarms() {
-        ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
-                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
-                .setZenPolicy(ZEN_POLICY) // should be ignored
-                .build(), false);
-
-        assertThat(zenMode.getPolicy()).isEqualTo(
-                new ZenPolicy.Builder()
-                        .disallowAllSounds()
-                        .allowAlarms(true)
-                        .allowMedia(true)
-                        .allowPriorityChannels(false)
-                        .build());
-    }
-
-    @Test
-    public void getPolicy_interruptionFilterNone_returnsPolicyAllowingNothing() {
-        ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
-                .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
-                .setZenPolicy(ZEN_POLICY) // should be ignored
-                .build(), false);
-
-        assertThat(zenMode.getPolicy()).isEqualTo(
-                new ZenPolicy.Builder()
-                        .disallowAllSounds()
-                        .hideAllVisualEffects()
-                        .allowPriorityChannels(false)
-                        .build());
-    }
-
-    @Test
-    public void setPolicy_setsInterruptionFilterPriority() {
-        ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
-                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
-                .build(), false);
-
-        zenMode.setPolicy(ZEN_POLICY);
-
-        assertThat(zenMode.getRule().getInterruptionFilter()).isEqualTo(
-                INTERRUPTION_FILTER_PRIORITY);
-        assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
-        assertThat(zenMode.getRule().getZenPolicy()).isEqualTo(ZEN_POLICY);
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesBackendTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesBackendTest.java
deleted file mode 100644
index 9483683..0000000
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesBackendTest.java
+++ /dev/null
@@ -1,363 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.notification.modes;
-
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
-import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-import static android.provider.Settings.Global.ZEN_MODE_OFF;
-import static android.service.notification.Condition.SOURCE_UNKNOWN;
-import static android.service.notification.Condition.STATE_FALSE;
-import static android.service.notification.Condition.STATE_TRUE;
-import static android.service.notification.ZenPolicy.STATE_ALLOW;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.AutomaticZenRule;
-import android.app.Flags;
-import android.app.NotificationManager;
-import android.app.NotificationManager.Policy;
-import android.content.Context;
-import android.net.Uri;
-import android.platform.test.annotations.EnableFlags;
-import android.platform.test.flag.junit.SetFlagsRule;
-import android.provider.Settings;
-import android.service.notification.Condition;
-import android.service.notification.ZenAdapters;
-import android.service.notification.ZenDeviceEffects;
-import android.service.notification.ZenModeConfig;
-import android.service.notification.ZenPolicy;
-
-import com.android.settings.R;
-
-import com.google.common.collect.ImmutableMap;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
-import org.robolectric.shadows.ShadowApplication;
-
-import java.time.Duration;
-import java.util.List;
-
-@RunWith(RobolectricTestRunner.class)
-@EnableFlags(Flags.FLAG_MODES_UI)
-public class ZenModesBackendTest {
-
-    private static final String ZEN_RULE_ID = "rule";
-    private static final AutomaticZenRule ZEN_RULE =
-            new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
-                    .setType(AutomaticZenRule.TYPE_DRIVING)
-                    .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                    .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                    .build();
-
-    private static final AutomaticZenRule MANUAL_DND_RULE =
-            new AutomaticZenRule.Builder("Do Not Disturb", Uri.EMPTY)
-                    .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                    .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                    .build();
-
-    @Mock
-    private NotificationManager mNm;
-
-    private Context mContext;
-    private ZenModesBackend mBackend;
-
-    @Rule
-    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(
-            SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
-
-    // Helper methods to add active/inactive rule state to a config. Returns a copy.
-    private ZenModeConfig configWithManualRule(ZenModeConfig base, boolean active) {
-        ZenModeConfig out = base.copy();
-
-        if (active) {
-            out.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-            out.manualRule.condition =
-                    new Condition(out.manualRule.conditionId, "", STATE_TRUE, SOURCE_UNKNOWN);
-        } else {
-            out.manualRule.zenMode = ZEN_MODE_OFF;
-            out.manualRule.condition =
-                    new Condition(out.manualRule.conditionId, "", STATE_FALSE, SOURCE_UNKNOWN);
-        }
-        return out;
-    }
-
-    private ZenModeConfig configWithRule(ZenModeConfig base, String ruleId, AutomaticZenRule rule,
-            boolean active) {
-        ZenModeConfig out = base.copy();
-
-        // Note that there are many other fields of zenRule, but here we only set the ones
-        // relevant to determining whether or not it is active.
-        ZenModeConfig.ZenRule zenRule = new ZenModeConfig.ZenRule();
-        zenRule.pkg = "package";
-        zenRule.enabled = active;
-        zenRule.snoozing = false;
-        zenRule.condition = new Condition(rule.getConditionId(), "",
-                active ? Condition.STATE_TRUE : Condition.STATE_FALSE,
-                Condition.SOURCE_USER_ACTION);
-        out.automaticRules.put(ruleId, zenRule);
-
-        return out;
-    }
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        ShadowApplication shadowApplication = ShadowApplication.getInstance();
-        shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm);
-
-        mContext = RuntimeEnvironment.application;
-        mBackend = new ZenModesBackend(mContext);
-
-        // Default catch-all case with no data. This isn't realistic, but tests below that rely
-        // on the config to get data on rules active will create those individually.
-        when(mNm.getZenModeConfig()).thenReturn(new ZenModeConfig());
-    }
-
-    @Test
-    public void getModes_containsManualDndAndZenRules() {
-        AutomaticZenRule rule2 = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
-                .build();
-        Policy dndPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS,
-                Policy.PRIORITY_SENDERS_CONTACTS, Policy.PRIORITY_SENDERS_CONTACTS);
-        when(mNm.getAutomaticZenRules()).thenReturn(
-                ImmutableMap.of("rule1", ZEN_RULE, "rule2", rule2));
-        ZenModeConfig config = new ZenModeConfig();
-        config.applyNotificationPolicy(dndPolicy);
-        assertThat(config.manualRule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_ALLOW);
-        when(mNm.getZenModeConfig()).thenReturn(config);
-
-        List<ZenMode> modes = mBackend.getModes();
-
-        // all modes exist, but none of them are currently active
-        assertThat(modes).containsExactly(
-                ZenMode.manualDndMode(
-                        new AutomaticZenRule.Builder(
-                                mContext.getString(R.string.zen_mode_settings_title), Uri.EMPTY)
-                                .setType(AutomaticZenRule.TYPE_OTHER)
-                                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                                .setZenPolicy(ZenAdapters.notificationPolicyToZenPolicy(dndPolicy))
-                                .setManualInvocationAllowed(true)
-                                .build(),
-                        false),
-                new ZenMode("rule2", rule2, false),
-                new ZenMode("rule1", ZEN_RULE, false))
-                .inOrder();
-    }
-
-    @Test
-    public void getMode_manualDnd_returnsMode() {
-        Policy dndPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS,
-                Policy.PRIORITY_SENDERS_CONTACTS, Policy.PRIORITY_SENDERS_CONTACTS);
-        ZenModeConfig config = new ZenModeConfig();
-        config.applyNotificationPolicy(dndPolicy);
-        when(mNm.getZenModeConfig()).thenReturn(config);
-
-        ZenMode mode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
-
-        assertThat(mode).isEqualTo(
-                ZenMode.manualDndMode(
-                        new AutomaticZenRule.Builder(
-                                mContext.getString(R.string.zen_mode_settings_title), Uri.EMPTY)
-                                .setType(AutomaticZenRule.TYPE_OTHER)
-                                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                                .setZenPolicy(ZenAdapters.notificationPolicyToZenPolicy(dndPolicy))
-                                .setManualInvocationAllowed(true)
-                                .build(), false));
-    }
-
-    @Test
-    public void getMode_zenRule_returnsMode() {
-        when(mNm.getAutomaticZenRule(eq(ZEN_RULE_ID))).thenReturn(ZEN_RULE);
-
-        ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
-
-        assertThat(mode).isEqualTo(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
-    }
-
-    @Test
-    public void getMode_missingRule_returnsNull() {
-        when(mNm.getAutomaticZenRule(any())).thenReturn(null);
-
-        ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
-
-        assertThat(mode).isNull();
-        verify(mNm).getAutomaticZenRule(eq(ZEN_RULE_ID));
-    }
-
-    @Test
-    public void getMode_manualDnd_returnsCorrectActiveState() {
-        // Set up a base config with an active rule to make sure we're looking at the correct info
-        ZenModeConfig configWithActiveRule = configWithRule(new ZenModeConfig(), ZEN_RULE_ID,
-                ZEN_RULE, true);
-
-        // Equivalent to disallowAllSounds()
-        Policy dndPolicy = new Policy(0, 0, 0);
-        configWithActiveRule.applyNotificationPolicy(dndPolicy);
-        when(mNm.getZenModeConfig()).thenReturn(configWithActiveRule);
-
-        ZenMode mode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
-
-        // By default, manual rule is inactive
-        assertThat(mode.isActive()).isFalse();
-
-        // Now the returned config will represent the manual rule being active
-        when(mNm.getZenModeConfig()).thenReturn(configWithManualRule(configWithActiveRule, true));
-        ZenMode activeMode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
-        assertThat(activeMode.isActive()).isTrue();
-    }
-
-    @Test
-    public void getMode_zenRule_returnsCorrectActiveState() {
-        // Set up a base config that has an active manual rule and "rule2", to make sure we're
-        // looking at the correct rule's info.
-        ZenModeConfig configWithActiveRules = configWithRule(
-                configWithManualRule(new ZenModeConfig(), true),  // active manual rule
-                "rule2", ZEN_RULE, true);  // active rule 2
-
-        when(mNm.getAutomaticZenRule(eq(ZEN_RULE_ID))).thenReturn(ZEN_RULE);
-        when(mNm.getZenModeConfig()).thenReturn(
-                configWithRule(configWithActiveRules, ZEN_RULE_ID, ZEN_RULE, false));
-
-        // Round 1: the current config should indicate that the rule is not active
-        ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
-        assertThat(mode.isActive()).isFalse();
-
-        when(mNm.getZenModeConfig()).thenReturn(
-                configWithRule(configWithActiveRules, ZEN_RULE_ID, ZEN_RULE, true));
-        ZenMode activeMode = mBackend.getMode(ZEN_RULE_ID);
-        assertThat(activeMode.isActive()).isTrue();
-    }
-
-    @Test
-    public void updateMode_manualDnd_setsDeviceEffects() throws Exception {
-        ZenMode manualDnd = ZenMode.manualDndMode(
-                new AutomaticZenRule.Builder("DND", Uri.EMPTY)
-                        .setZenPolicy(new ZenPolicy())
-                        .setDeviceEffects(new ZenDeviceEffects.Builder()
-                                .setShouldDimWallpaper(true)
-                                .build())
-                        .build(), false);
-
-        mBackend.updateMode(manualDnd);
-
-        verify(mNm).setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder()
-                .setShouldDimWallpaper(true)
-                .build());
-    }
-
-    @Test
-    public void updateMode_manualDnd_setsNotificationPolicy() {
-        ZenMode manualDnd = ZenMode.manualDndMode(
-                new AutomaticZenRule.Builder("DND", Uri.EMPTY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                        .build(), false);
-
-        mBackend.updateMode(manualDnd);
-
-        verify(mNm).setNotificationPolicy(eq(new ZenModeConfig().toNotificationPolicy(
-                new ZenPolicy.Builder().allowAllSounds().build())), eq(true));
-    }
-
-    @Test
-    public void updateMode_zenRule_updatesRule() {
-        ZenMode ruleMode = new ZenMode("rule", ZEN_RULE, false);
-
-        mBackend.updateMode(ruleMode);
-
-        verify(mNm).updateAutomaticZenRule(eq("rule"), eq(ZEN_RULE), eq(true));
-    }
-
-    @Test
-    public void activateMode_manualDnd_setsZenModeImportant() {
-        mBackend.activateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false), null);
-
-        verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
-                any(), eq(true));
-    }
-
-    @Test
-    public void activateMode_manualDndWithDuration_setsZenModeImportantWithCondition() {
-        mBackend.activateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false),
-                Duration.ofMinutes(30));
-
-        verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS),
-                eq(ZenModeConfig.toTimeCondition(mContext, 30, 0, true).id),
-                any(),
-                eq(true));
-    }
-
-    @Test
-    public void activateMode_zenRule_setsRuleStateActive() {
-        mBackend.activateMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false), null);
-
-        verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_ID),
-                eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_TRUE,
-                        Condition.SOURCE_USER_ACTION)));
-    }
-
-    @Test
-    public void activateMode_zenRuleWithDuration_fails() {
-        assertThrows(IllegalArgumentException.class,
-                () -> mBackend.activateMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false),
-                        Duration.ofMinutes(30)));
-    }
-
-    @Test
-    public void deactivateMode_manualDnd_setsZenModeOff() {
-        mBackend.deactivateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, true));
-
-        verify(mNm).setZenMode(eq(ZEN_MODE_OFF), eq(null), any(), eq(true));
-    }
-
-    @Test
-    public void deactivateMode_zenRule_setsRuleStateInactive() {
-        mBackend.deactivateMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
-
-        verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_ID),
-                eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_FALSE,
-                        Condition.SOURCE_USER_ACTION)));
-    }
-
-    @Test
-    public void removeMode_zenRule_deletesRule() {
-        mBackend.removeMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
-
-        verify(mNm).removeAutomaticZenRule(ZEN_RULE_ID, true);
-    }
-
-    @Test
-    public void removeMode_manualDnd_fails() {
-        assertThrows(IllegalArgumentException.class,
-                () -> mBackend.removeMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false)));
-    }
-}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListItemPreferenceTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListItemPreferenceTest.java
new file mode 100644
index 0000000..495a24c
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListItemPreferenceTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.notification.modes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.service.notification.ZenModeConfig;
+
+import com.android.settingslib.notification.modes.ZenMode;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowLooper;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenModesListItemPreferenceTest {
+
+    private Context mContext;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+    }
+
+    @Test
+    public void constructor_setsMode() {
+        ZenModesListItemPreference preference = new ZenModesListItemPreference(mContext,
+                TestModeBuilder.EXAMPLE);
+
+        assertThat(preference.getKey()).isEqualTo(TestModeBuilder.EXAMPLE.getId());
+        assertThat(preference.getZenMode()).isEqualTo(TestModeBuilder.EXAMPLE);
+    }
+
+    @Test
+    public void setZenMode_modeEnabled() {
+        ZenMode mode = new TestModeBuilder()
+                .setName("Enabled mode")
+                .setTriggerDescription("When the thrush knocks")
+                .setEnabled(true)
+                .build();
+
+        ZenModesListItemPreference preference = new ZenModesListItemPreference(mContext, mode);
+        ShadowLooper.idleMainLooper(); // To load icon.
+
+        assertThat(preference.getTitle()).isEqualTo("Enabled mode");
+        assertThat(preference.getSummary()).isEqualTo("When the thrush knocks");
+        assertThat(preference.getIcon()).isNotNull();
+    }
+
+    @Test
+    public void setZenMode_modeActive() {
+        ZenMode mode = new TestModeBuilder()
+                .setName("Active mode")
+                .setTriggerDescription("When Birnam forest comes to Dunsinane")
+                .setEnabled(true)
+                .setActive(true)
+                .build();
+
+        ZenModesListItemPreference preference = new ZenModesListItemPreference(mContext, mode);
+        ShadowLooper.idleMainLooper();
+
+        assertThat(preference.getTitle()).isEqualTo("Active mode");
+        assertThat(preference.getSummary()).isEqualTo("ON • When Birnam forest comes to Dunsinane");
+        assertThat(preference.getIcon()).isNotNull();
+    }
+
+    @Test
+    public void setZenMode_modeDisabledByApp() {
+        ZenModeConfig.ZenRule configRule = new ZenModeConfig.ZenRule();
+        configRule.enabled = false;
+        configRule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_APP;
+        ZenMode mode = new TestModeBuilder()
+                .setName("Mode disabled by app")
+                .setTriggerDescription("When the cat's away")
+                .setEnabled(false)
+                .setConfigZenRule(configRule)
+                .build();
+
+        ZenModesListItemPreference preference = new ZenModesListItemPreference(mContext, mode);
+        ShadowLooper.idleMainLooper();
+
+        assertThat(preference.getTitle()).isEqualTo("Mode disabled by app");
+        assertThat(preference.getSummary()).isEqualTo("Tap to set up");
+        assertThat(preference.getIcon()).isNotNull();
+    }
+
+    @Test
+    public void setZenMode_modeDisabledByUser() {
+        ZenModeConfig.ZenRule configRule = new ZenModeConfig.ZenRule();
+        configRule.enabled = false;
+        configRule.disabledOrigin = ZenModeConfig.UPDATE_ORIGIN_USER;
+        ZenMode mode = new TestModeBuilder()
+                .setName("Mode disabled by user")
+                .setTriggerDescription("When the Levee Breaks")
+                .setEnabled(false)
+                .setConfigZenRule(configRule)
+                .build();
+
+        ZenModesListItemPreference preference = new ZenModesListItemPreference(mContext, mode);
+        ShadowLooper.idleMainLooper();
+
+        assertThat(preference.getTitle()).isEqualTo("Mode disabled by user");
+        assertThat(preference.getSummary()).isEqualTo("Paused");
+        assertThat(preference.getIcon()).isNotNull();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java
index 9a4de60..f2624ac 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesListPreferenceControllerTest.java
@@ -37,6 +37,8 @@
 import androidx.preference.PreferenceManager;
 import androidx.preference.PreferenceScreen;
 
+import com.android.settingslib.notification.modes.ZenMode;
+import com.android.settingslib.notification.modes.ZenModesBackend;
 import com.android.settingslib.search.SearchIndexableRaw;
 
 import com.google.common.collect.ImmutableList;
@@ -58,14 +60,15 @@
 public class ZenModesListPreferenceControllerTest {
     private static final String TEST_MODE_ID = "test_mode";
     private static final String TEST_MODE_NAME = "Test Mode";
-    private static final ZenMode TEST_MODE = new ZenMode(
-            TEST_MODE_ID,
-            new AutomaticZenRule.Builder(TEST_MODE_NAME, Uri.parse("test_uri"))
+
+    private static final ZenMode TEST_MODE = new TestModeBuilder()
+            .setId(TEST_MODE_ID)
+            .setAzr(new AutomaticZenRule.Builder(TEST_MODE_NAME, Uri.parse("test_uri"))
                     .setType(AutomaticZenRule.TYPE_BEDTIME)
                     .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
                     .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                    .build(),
-            false);
+                    .build())
+            .build();
 
     private static final ZenMode TEST_MANUAL_MODE = ZenMode.manualDndMode(
             new AutomaticZenRule.Builder("Do Not Disturb", Uri.EMPTY)
@@ -110,14 +113,9 @@
 
         assertThat(mPreference.getPreferenceCount()).isEqualTo(5);
         List<ZenModesListItemPreference> itemPreferences = getModeListItems(mPreference);
-        assertThat(itemPreferences.stream().map(pref -> pref.mZenMode).toList())
+        assertThat(itemPreferences.stream().map(ZenModesListItemPreference::getZenMode).toList())
                 .containsExactlyElementsIn(modes)
                 .inOrder();
-
-        for (int i = 0; i < modes.size(); i++) {
-            assertThat(((ZenModesListItemPreference) (mPreference.getPreference(i))).mZenMode)
-                    .isEqualTo(modes.get(i));
-        }
     }
 
     @Test
@@ -138,7 +136,7 @@
         mPrefController.updateState(mPreference);
 
         List<ZenModesListItemPreference> newPreferences = getModeListItems(mPreference);
-        assertThat(newPreferences.stream().map(pref -> pref.mZenMode).toList())
+        assertThat(newPreferences.stream().map(ZenModesListItemPreference::getZenMode).toList())
                 .containsExactlyElementsIn(updatedModes)
                 .inOrder();
 
@@ -194,7 +192,7 @@
         assertThat(newData).hasSize(1);
 
         SearchIndexableRaw newItem = newData.get(0);
-        assertThat(newItem.key).isEqualTo(ZenMode.MANUAL_DND_MODE_ID);
+        assertThat(newItem.key).isEqualTo(TEST_MANUAL_MODE.getId());
         assertThat(newItem.title).isEqualTo("Do Not Disturb");  // set above
     }
 
@@ -209,7 +207,7 @@
 
         // Should keep the order presented by getModes()
         SearchIndexableRaw item0 = data.get(0);
-        assertThat(item0.key).isEqualTo(ZenMode.MANUAL_DND_MODE_ID);
+        assertThat(item0.key).isEqualTo(TEST_MANUAL_MODE.getId());
         assertThat(item0.title).isEqualTo("Do Not Disturb");  // set above
 
         SearchIndexableRaw item1 = data.get(1);
@@ -218,13 +216,7 @@
     }
 
     private static ZenMode newMode(String id) {
-        return new ZenMode(
-                id,
-                new AutomaticZenRule.Builder("Mode " + id, Uri.parse("test_uri"))
-                        .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
-                        .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
-                        .build(),
-                false);
+        return new TestModeBuilder().setId(id).setName("Mode " + id).build();
     }
 
     /**
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java
index 51368c5..62b5ee0 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java
@@ -16,7 +16,6 @@
 
 package com.android.settings.notification.modes;
 
-import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE;
 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
@@ -25,12 +24,12 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import android.app.AutomaticZenRule;
 import android.content.Context;
-import android.net.Uri;
 import android.service.notification.ZenDeviceEffects;
 import android.service.notification.ZenPolicy;
 
+import com.android.settingslib.notification.modes.ZenMode;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -40,7 +39,6 @@
 
 import java.util.LinkedHashSet;
 import java.util.Set;
-
 @RunWith(RobolectricTestRunner.class)
 public class ZenModesSummaryHelperTest {
     private Context mContext;
@@ -58,50 +56,38 @@
 
     @Test
     public void getPeopleSummary_noOne() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("No one can interrupt");
     }
 
     @Test
     public void getPeopleSummary_some() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_CONTACTS).build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("Some people can interrupt");
     }
 
     @Test
     public void getPeopleSummary_all() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_ANYONE).
                         allowConversations(CONVERSATION_SENDERS_ANYONE)
                         .allowMessages(PEOPLE_TYPE_ANYONE).build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("All people can interrupt");
     }
 
     @Test
     public void getOtherSoundCategoriesSummary_single() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo(
                 "Alarms can interrupt");
@@ -109,12 +95,9 @@
 
     @Test
     public void getOtherSoundCategoriesSummary_duo() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder().allowAlarms(true).allowMedia(true).build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo(
                 "Alarms and media can interrupt");
@@ -122,16 +105,13 @@
 
     @Test
     public void getOtherSoundCategoriesSummary_trio() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .allowAlarms(true)
                         .allowMedia(true)
                         .allowSystem(true)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo(
                 "Alarms, media, and touch sounds can interrupt");
@@ -139,9 +119,7 @@
 
     @Test
     public void getOtherSoundCategoriesSummary_quad() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .allowAlarms(true)
                         .allowMedia(true)
@@ -149,7 +127,6 @@
                         .allowReminders(true)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo(
                 "Alarms, media, and 2 more can interrupt");
@@ -157,9 +134,7 @@
 
     @Test
     public void getOtherSoundCategoriesSummary_all() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .allowAlarms(true)
                         .allowMedia(true)
@@ -168,7 +143,6 @@
                         .allowEvents(true)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo(
                 "Alarms, media, and 3 more can interrupt");
@@ -176,61 +150,52 @@
 
     @Test
     public void getBlockedEffectsSummary_none() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .showAllVisualEffects()
                         .allowAlarms(true)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
+
         assertThat(mSummaryHelper.getBlockedEffectsSummary(zenMode))
                 .isEqualTo("Notifications shown");
     }
 
     @Test
     public void getBlockedEffectsSummary_some() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .allowAlarms(true)
                         .showAllVisualEffects()
                         .showVisualEffect(VISUAL_EFFECT_AMBIENT, false)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
+
         assertThat(mSummaryHelper.getBlockedEffectsSummary(zenMode))
                 .isEqualTo("Notifications partially hidden");
     }
 
     @Test
     public void getBlockedEffectsSummary_all() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .allowAlarms(true)
                         .hideAllVisualEffects()
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
+
         assertThat(mSummaryHelper.getBlockedEffectsSummary(zenMode))
                 .isEqualTo("Notifications hidden");
     }
 
     @Test
     public void getDisplayEffectsSummary_single_notifVis() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .showAllVisualEffects()
                         .showVisualEffect(VISUAL_EFFECT_AMBIENT, false)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo(
                 "Notifications partially hidden");
@@ -238,15 +203,12 @@
 
     @Test
     public void getDisplayEffectsSummary_single_notifVis_unusedEffect() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .showAllVisualEffects()
                         .showVisualEffect(VISUAL_EFFECT_LIGHTS, false)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo(
                 "Notifications shown");
@@ -254,15 +216,12 @@
 
     @Test
     public void getDisplayEffectsSummary_single_displayEffect() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder().showAllVisualEffects().build())
                 .setDeviceEffects(new ZenDeviceEffects.Builder()
                         .setShouldDimWallpaper(true)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo(
                 "Dim the wallpaper");
@@ -270,16 +229,13 @@
 
     @Test
     public void getDisplayEffectsSummary_duo() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder().showAllVisualEffects().build())
                 .setDeviceEffects(new ZenDeviceEffects.Builder()
                         .setShouldDimWallpaper(true)
                         .setShouldDisplayGrayscale(true)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo(
                 "Grayscale and dim the wallpaper");
@@ -287,9 +243,7 @@
 
     @Test
     public void getDisplayEffectsSummary_trio() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .hideAllVisualEffects()
                         .allowAlarms(true)
@@ -301,7 +255,6 @@
                         .setShouldDimWallpaper(true)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo(
                 "Notifications hidden, grayscale, and dim the wallpaper");
@@ -309,9 +262,7 @@
 
     @Test
     public void getDisplayEffectsSummary_quad() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .showAllVisualEffects()
                         .showVisualEffect(VISUAL_EFFECT_AMBIENT, false)
@@ -325,7 +276,6 @@
                         .setShouldUseNightMode(true)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo(
                 "Notifications partially hidden, grayscale, and 2 more");
@@ -333,28 +283,22 @@
 
     @Test
     public void getAppsSummary_none() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getAppsSummary(zenMode, new LinkedHashSet<>())).isEqualTo("None");
     }
 
     @Test
     public void getAppsSummary_priorityAppsNoList() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
 
         assertThat(mSummaryHelper.getAppsSummary(zenMode, null)).isEqualTo("Selected apps");
     }
@@ -396,19 +340,15 @@
 
     @Test
     public void getAppsSummary_priorityApps() {
-        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
-                .setType(AutomaticZenRule.TYPE_BEDTIME)
-                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+        ZenMode zenMode = new TestModeBuilder()
                 .setZenPolicy(new ZenPolicy.Builder()
                         .allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
                         .build())
                 .build();
-        ZenMode zenMode = new ZenMode("id", rule, true);
         Set<String> apps = Set.of("My App", "SecondApp", "ThirdApp", "FourthApp",
                 "FifthApp", "SixthApp");
 
         assertThat(mSummaryHelper.getAppsSummary(zenMode, apps)).isEqualTo("FifthApp, FourthApp, "
                 + "and 4 more can interrupt");
     }
-
 }
diff --git a/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java
index 44e1cc6..e035274 100644
--- a/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/users/UserDetailsSettingsTest.java
@@ -42,9 +42,13 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.UserInfo;
+import android.multiuser.Flags;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.telephony.TelephonyManager;
 
 import androidx.fragment.app.FragmentActivity;
@@ -63,6 +67,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -123,6 +128,8 @@
     private Bundle mArguments;
     private UserInfo mUserInfo;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -245,6 +252,19 @@
     }
 
     @Test
+    @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX})
+    public void onResume_UserSwitcherDisabled_shouldDisableSwitchPref() {
+        setupSelectedUser();
+        mUserCapabilities.mUserSwitcherEnabled = false;
+        mFragment.mSwitchUserPref = mSwitchUserPref;
+        mFragment.onAttach(mContext);
+
+        mFragment.onResume();
+
+        verify(mSwitchUserPref).setEnabled(false);
+    }
+
+    @Test
     public void onResume_switchDisallowed_shouldDisableSwitchPref() {
         setupSelectedUser();
         mUserManager.setSwitchabilityStatus(SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED);
diff --git a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java
index 5826ca2..85db0bd 100644
--- a/tests/robotests/src/com/android/settings/users/UserSettingsTest.java
+++ b/tests/robotests/src/com/android/settings/users/UserSettingsTest.java
@@ -34,6 +34,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 import static org.robolectric.Shadows.shadowOf;
 
 import android.app.settings.SettingsEnums;
@@ -46,10 +47,15 @@
 import android.content.pm.UserInfo;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
+import android.multiuser.Flags;
 import android.os.Bundle;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.os.UserManager;
+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.provider.Settings;
 import android.text.SpannableStringBuilder;
 import android.view.Menu;
@@ -75,6 +81,7 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.AdditionalMatchers;
@@ -142,6 +149,9 @@
     private UserSettings mFragment;
     private UserCapabilities mUserCapabilities;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -359,6 +369,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX})
     public void updateUserList_cannotSwitchUser_shouldDisableAddUser() {
         mUserCapabilities.mCanAddUser = true;
         doReturn(true).when(mUserManager).canAddMoreUsers(anyString());
@@ -375,6 +386,20 @@
     }
 
     @Test
+    @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX})
+    public void updateUserList_disallowAddUser_shouldDisableAddUserAndAddGuest() {
+        mUserCapabilities.mDisallowAddUserSetByAdmin = true;
+        doReturn(true).when(mUserManager).canAddMoreUsers(anyString());
+        doReturn(SWITCHABILITY_STATUS_OK)
+                .when(mUserManager).getUserSwitchability();
+
+        mFragment.updateUserList();
+
+        verify(mAddUserPreference).setVisible(true);
+        verify(mAddUserPreference).setDisabledByAdmin(any());
+    }
+
+    @Test
     public void updateUserList_canNotAddMoreUsers_shouldDisableAddUserWithSummary() {
         mUserCapabilities.mCanAddUser = true;
         doReturn(false).when(mUserManager).canAddMoreUsers(anyString());
@@ -392,6 +417,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX})
     public void updateUserList_cannotSwitchUser_shouldDisableAddGuest() {
         mUserCapabilities.mCanAddGuest = true;
         doReturn(true)
@@ -406,6 +432,54 @@
     }
 
     @Test
+    @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX})
+    public void updateUserList_cannotSwitchUser_shouldKeepPreferencesVisibleAndEnabled() {
+        givenUsers(getAdminUser(true));
+        mUserCapabilities.mCanAddGuest = true;
+        mUserCapabilities.mCanAddUser = true;
+        mUserCapabilities.mDisallowSwitchUser = true;
+        doReturn(true)
+                .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_GUEST));
+        doReturn(true)
+                .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_SECONDARY));
+
+        mFragment.updateUserList();
+
+        verify(mAddGuestPreference).setVisible(true);
+        verify(mAddGuestPreference).setEnabled(true);
+        verify(mAddUserPreference).setVisible(true);
+        verify(mAddUserPreference).setEnabled(true);
+    }
+
+    @Test
+    @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX})
+    public void updateUserList_disallowAddUser_shouldShowButDisableAddActions() {
+        givenUsers(getAdminUser(true));
+        mUserCapabilities.mCanAddGuest = true;
+        mUserCapabilities.mCanAddUser = false;
+        mUserCapabilities.mDisallowAddUser = true;
+        mUserCapabilities.mDisallowAddUserSetByAdmin = false;
+        List<UserManager.EnforcingUser> enforcingUsers = new ArrayList<>();
+        enforcingUsers.add(new UserManager.EnforcingUser(UserHandle.myUserId(),
+                UserManager.RESTRICTION_SOURCE_SYSTEM));
+        when(mUserManager.getUserRestrictionSources(UserManager.DISALLOW_ADD_USER,
+                UserHandle.of(UserHandle.myUserId()))).thenReturn(enforcingUsers);
+
+        doReturn(true)
+                .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_GUEST));
+        doReturn(true)
+                .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_SECONDARY));
+
+        mFragment.updateUserList();
+
+        verify(mAddGuestPreference).setVisible(true);
+        verify(mAddGuestPreference).setEnabled(false);
+        verify(mAddUserPreference).setVisible(true);
+        verify(mAddUserPreference).setEnabled(false);
+    }
+
+    @Test
+    @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX})
     public void updateUserList_addUserDisallowedByAdmin_shouldNotShowAddUser() {
         RestrictedLockUtils.EnforcedAdmin enforcedAdmin = mock(
                 RestrictedLockUtils.EnforcedAdmin.class);
@@ -421,6 +495,22 @@
     }
 
     @Test
+    @RequiresFlagsEnabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX})
+    public void updateUserList_addUserDisallowedByAdmin_shouldShowPrefDisabledByAdmin() {
+        RestrictedLockUtils.EnforcedAdmin enforcedAdmin = mock(
+                RestrictedLockUtils.EnforcedAdmin.class);
+
+        mUserCapabilities.mEnforcedAdmin = enforcedAdmin;
+        mUserCapabilities.mCanAddUser = false;
+        mUserCapabilities.mDisallowAddUser = true;
+        mUserCapabilities.mDisallowAddUserSetByAdmin = true;
+        doReturn(true).when(mAddUserPreference).isEnabled();
+
+        mFragment.updateUserList();
+
+        verify(mAddUserPreference).setDisabledByAdmin(enforcedAdmin);
+    }
+    @Test
     public void updateUserList_cannotAddUserButCanSwitchUser_shouldNotShowAddUser() {
         mUserCapabilities.mCanAddUser = false;
 
@@ -461,18 +551,31 @@
     }
 
     @Test
-    public void updateUserList_userSwitcherDisabled_shouldNotShowAddUser() {
+    public void updateUserList_userSwitcherDisabled_shouldShowAddUser() {
         givenUsers(getAdminUser(true));
         mUserCapabilities.mCanAddUser = true;
         mUserCapabilities.mUserSwitcherEnabled = false;
 
         mFragment.updateUserList();
 
-        verify(mAddUserPreference).setVisible(false);
+        verify(mAddUserPreference).setVisible(true);
     }
 
     @Test
-    public void updateUserList_userSwitcherDisabled_shouldNotShowAddGuest() {
+    public void updateUserList_userSwitcherDisabled_shouldShowAddGuest() {
+        givenUsers(getAdminUser(true));
+        mUserCapabilities.mCanAddGuest = true;
+        mUserCapabilities.mUserSwitcherEnabled = false;
+        doReturn(true)
+                .when(mUserManager).canAddMoreUsers(eq(UserManager.USER_TYPE_FULL_GUEST));
+
+        mFragment.updateUserList();
+
+        verify(mAddGuestPreference).setVisible(true);
+    }
+
+    @Test
+    public void updateUserList_userSwitcherDisabledCannotAddMoreGuests_shouldNotShowAddGuest() {
         givenUsers(getAdminUser(true));
         mUserCapabilities.mCanAddGuest = true;
         mUserCapabilities.mUserSwitcherEnabled = false;
@@ -533,18 +636,18 @@
     }
 
     @Test
-    public void updateUserList_existingSecondaryUser_shouldAddOnlyCurrUser_MultiUserIsDisabled() {
+    public void updateUserList_existingSecondaryUser_shouldAddAllUsers_MultiUserIsDisabled() {
         givenUsers(getAdminUser(true), getSecondaryUser(false));
         mUserCapabilities.mUserSwitcherEnabled = false;
 
         mFragment.updateUserList();
 
         ArgumentCaptor<UserPreference> captor = ArgumentCaptor.forClass(UserPreference.class);
-        verify(mFragment.mUserListCategory, times(1))
+        verify(mFragment.mUserListCategory, times(2))
                 .addPreference(captor.capture());
 
         List<UserPreference> userPrefs = captor.getAllValues();
-        assertThat(userPrefs.size()).isEqualTo(1);
+        assertThat(userPrefs.size()).isEqualTo(2);
         assertThat(userPrefs.get(0)).isSameInstanceAs(mMePreference);
     }
 
@@ -631,6 +734,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled({Flags.FLAG_NEW_MULTIUSER_SETTINGS_UX})
     public void updateUserList_uninitializedUserAndCanNotSwitchUser_shouldDisablePref() {
         UserInfo uninitializedUser = getSecondaryUser(false);
         removeFlag(uninitializedUser, UserInfo.FLAG_INITIALIZED);
diff --git a/tests/robotests/src/com/android/settings/wifi/calling/WifiCallingSettingsForSubTest.java b/tests/robotests/src/com/android/settings/wifi/calling/WifiCallingSettingsForSubTest.java
index 4497a0a..4440bc9 100644
--- a/tests/robotests/src/com/android/settings/wifi/calling/WifiCallingSettingsForSubTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/calling/WifiCallingSettingsForSubTest.java
@@ -48,6 +48,8 @@
 import android.telephony.ims.ImsMmTelManager;
 import android.view.View;
 
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceScreen;
 
@@ -56,10 +58,14 @@
 import com.android.settings.SettingsActivity;
 import com.android.settings.network.ims.MockWifiCallingQueryImsState;
 import com.android.settings.network.ims.WifiCallingQueryImsState;
+import com.android.settings.network.telephony.wificalling.IWifiCallingRepository;
 import com.android.settings.testutils.shadow.ShadowFragment;
 import com.android.settings.widget.SettingsMainSwitchBar;
 import com.android.settings.widget.SettingsMainSwitchPreference;
 
+import kotlin.Unit;
+import kotlin.jvm.functions.Function1;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -183,36 +189,25 @@
     }
 
     @Test
-    public void onResume_provisioningAllowed_shouldNotFinish() {
-        // Call onResume while provisioning is allowed.
-        mFragment.onResume();
+    public void onViewCreated_provisioningAllowed_shouldNotFinish() {
+        // Call onViewCreated while provisioning is allowed.
+        mFragment.onViewCreated(mView, null);
 
         // Verify that finish() is not called.
         verify(mFragment, never()).finish();
     }
 
     @Test
-    public void onResume_provisioningDisallowed_shouldFinish() {
-        // Call onResume while provisioning is disallowed.
-        mQueryImsState.setIsProvisionedOnDevice(false);
-        mFragment.onResume();
+    public void onViewCreated_provisioningDisallowed_shouldFinish() {
+        // Call onViewCreated while provisioning is disallowed.
+        mFragment.mIsWifiCallingReady = false;
+        mFragment.onViewCreated(mView, null);
 
         // Verify that finish() is called
         verify(mFragment).finish();
     }
 
     @Test
-    public void onResumeOnPause_provisioningCallbackRegistration() throws Exception {
-        // Verify that provisioning callback is registered after call to onResume().
-        mFragment.onResume();
-        verify(mFragment).registerProvisioningChangedCallback();
-
-        // Verify that provisioning callback is unregistered after call to onPause.
-        mFragment.onPause();
-        verify(mFragment).unregisterProvisioningChangedCallback();
-    }
-
-    @Test
     public void onResume_useWfcHomeModeConfigFalseAndEditable_shouldShowWfcRoaming() {
         // Call onResume to update the WFC roaming preference.
         mFragment.onResume();
@@ -377,6 +372,7 @@
 
     protected class TestFragment extends WifiCallingSettingsForSub {
         private SettingsMainSwitchPreference mSwitchPref;
+        protected boolean mIsWifiCallingReady = true;
 
         protected void setSwitchBar(SettingsMainSwitchPreference switchPref) {
             mSwitchPref = switchPref;
@@ -422,6 +418,25 @@
         }
 
         @Override
+        @NonNull
+        IWifiCallingRepository getWifiCallingRepository() {
+            return new IWifiCallingRepository() {
+                @Override
+                public void collectIsWifiCallingReadyFlow(
+                        @NonNull LifecycleOwner lifecycleOwner,
+                        @NonNull Function1<? super Boolean, Unit> action) {
+                    action.invoke(mIsWifiCallingReady);
+                }
+            };
+        }
+
+        @NonNull
+        @Override
+        LifecycleOwner getLifecycleOwner() {
+            return this;
+        }
+
+        @Override
         void showAlert(Intent intent) {
         }
     }
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index bc5824f..55df480 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -33,6 +33,7 @@
         "kotlinx_coroutines_test",
         "Settings-testutils2",
         "MediaDrmSettingsFlagsLib",
+        "servicestests-utils",
         // Don't add SettingsLib libraries here - you can use them directly as they are in the
         // instrumented Settings app.
     ],
diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java
new file mode 100644
index 0000000..019ade7
--- /dev/null
+++ b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayPreferenceFragmentTest.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.connecteddevice.display;
+
+
+import static android.view.Display.INVALID_DISPLAY;
+
+import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.PREVIOUSLY_SHOWN_LIST_KEY;
+import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.DISPLAYS_LIST_PREFERENCE_KEY;
+import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE;
+import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE;
+import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY;
+import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE;
+import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_ROTATION_KEY;
+import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE;
+import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_SETTINGS_RESOURCE;
+import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_USE_PREFERENCE_KEY;
+import static com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.EXTERNAL_DISPLAY_USE_TITLE_RESOURCE;
+import static com.android.settingslib.widget.FooterPreference.KEY_FOOTER;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Display;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.connecteddevice.display.ExternalDisplayPreferenceFragment.DisplayPreference;
+import com.android.settingslib.widget.FooterPreference;
+import com.android.settingslib.widget.MainSwitchPreference;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/** Unit tests for {@link ExternalDisplayPreferenceFragment}.  */
+@RunWith(AndroidJUnit4.class)
+public class ExternalDisplayPreferenceFragmentTest extends ExternalDisplayTestBase {
+    @Nullable
+    private ExternalDisplayPreferenceFragment mFragment;
+    private int mPreferenceIdFromResource;
+    private int mDisplayIdArg = INVALID_DISPLAY;
+    private int mResolutionSelectorDisplayId = INVALID_DISPLAY;
+    @Mock
+    private MetricsLogger mMockedMetricsLogger;
+
+    @Test
+    @UiThreadTest
+    public void testCreateAndStart() {
+        initFragment();
+        assertThat(mPreferenceIdFromResource).isEqualTo(EXTERNAL_DISPLAY_SETTINGS_RESOURCE);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testShowDisplayList() {
+        var fragment = initFragment();
+        var outState = new Bundle();
+        fragment.onSaveInstanceStateCallback(outState);
+        assertThat(outState.getBoolean(PREVIOUSLY_SHOWN_LIST_KEY)).isFalse();
+        assertThat(mHandler.getPendingMessages().size()).isEqualTo(1);
+        PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY);
+        assertThat(pref).isNull();
+        verify(mMockedInjector, never()).getAllDisplays();
+        mHandler.flush();
+        assertThat(mHandler.getPendingMessages().size()).isEqualTo(0);
+        verify(mMockedInjector).getAllDisplays();
+        pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY);
+        assertThat(pref).isNotNull();
+        assertThat(pref.getPreferenceCount()).isEqualTo(2);
+        fragment.onSaveInstanceStateCallback(outState);
+        assertThat(outState.getBoolean(PREVIOUSLY_SHOWN_LIST_KEY)).isTrue();
+    }
+
+    @Test
+    @UiThreadTest
+    public void testLaunchDisplaySettingFromList() {
+        initFragment();
+        mHandler.flush();
+        PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY);
+        assertThat(pref).isNotNull();
+        DisplayPreference display1Pref = (DisplayPreference) pref.getPreference(0);
+        DisplayPreference display2Pref = (DisplayPreference) pref.getPreference(1);
+        assertThat(display1Pref.getKey()).isEqualTo("display_id_" + 1);
+        assertThat("" + display1Pref.getTitle()).isEqualTo("HDMI");
+        assertThat("" + display1Pref.getSummary()).isEqualTo("1920 x 1080");
+        display1Pref.onPreferenceClick(display1Pref);
+        assertThat(mDisplayIdArg).isEqualTo(1);
+        verify(mMockedMetricsLogger).writePreferenceClickMetric(display1Pref);
+        assertThat(display2Pref.getKey()).isEqualTo("display_id_" + 2);
+        assertThat("" + display2Pref.getTitle()).isEqualTo("Overlay #1");
+        assertThat("" + display2Pref.getSummary()).isEqualTo("1240 x 780");
+        display2Pref.onPreferenceClick(display2Pref);
+        assertThat(mDisplayIdArg).isEqualTo(2);
+        verify(mMockedMetricsLogger).writePreferenceClickMetric(display2Pref);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testShowDisplayListForOnlyOneDisplay_PreviouslyShownList() {
+        var fragment = initFragment();
+        // Previously shown list of displays
+        fragment.onActivityCreatedCallback(createBundleForPreviouslyShownList());
+        // Only one display available
+        doReturn(new Display[] {mDisplays[1]}).when(mMockedInjector).getAllDisplays();
+        mHandler.flush();
+        PreferenceCategory pref = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY);
+        assertThat(pref).isNotNull();
+        assertThat(pref.getPreferenceCount()).isEqualTo(1);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testShowEnabledDisplay_OnlyOneDisplayAvailable() {
+        doReturn(true).when(mMockedInjector).isDisplayEnabled(any());
+        // Only one display available
+        doReturn(new Display[] {mDisplays[1]}).when(mMockedInjector).getAllDisplays();
+        // Init
+        initFragment();
+        mHandler.flush();
+        PreferenceCategory list = mPreferenceScreen.findPreference(DISPLAYS_LIST_PREFERENCE_KEY);
+        assertThat(list).isNull();
+        var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY);
+        assertThat(pref).isNotNull();
+        pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY);
+        assertThat(pref).isNotNull();
+        var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER);
+        assertThat(footerPref).isNotNull();
+        verify(footerPref).setTitle(EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testShowOneEnabledDisplay_FewAvailable() {
+        mDisplayIdArg = 1;
+        doReturn(true).when(mMockedInjector).isDisplayEnabled(any());
+        initFragment();
+        verify(mMockedInjector, never()).getDisplay(anyInt());
+        mHandler.flush();
+        verify(mMockedInjector).getDisplay(mDisplayIdArg);
+        var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY);
+        assertThat(pref).isNotNull();
+        pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY);
+        assertThat(pref).isNotNull();
+        var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER);
+        assertThat(footerPref).isNotNull();
+        verify(footerPref).setTitle(EXTERNAL_DISPLAY_CHANGE_RESOLUTION_FOOTER_RESOURCE);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testShowDisabledDisplay() {
+        mDisplayIdArg = 1;
+        initFragment();
+        verify(mMockedInjector, never()).getDisplay(anyInt());
+        mHandler.flush();
+        verify(mMockedInjector).getDisplay(mDisplayIdArg);
+        var mainPref = (MainSwitchPreference) mPreferenceScreen.findPreference(
+                EXTERNAL_DISPLAY_USE_PREFERENCE_KEY);
+        assertThat(mainPref).isNotNull();
+        assertThat("" + mainPref.getTitle()).isEqualTo(
+                getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE));
+        assertThat(mainPref.isChecked()).isFalse();
+        assertThat(mainPref.isEnabled()).isTrue();
+        assertThat(mainPref.getOnPreferenceChangeListener()).isNotNull();
+        var pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY);
+        assertThat(pref).isNull();
+        pref = mPreferenceScreen.findPreference(EXTERNAL_DISPLAY_ROTATION_KEY);
+        assertThat(pref).isNull();
+        var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER);
+        assertThat(footerPref).isNull();
+    }
+
+    @Test
+    @UiThreadTest
+    public void testNoDisplays() {
+        doReturn(new Display[0]).when(mMockedInjector).getAllDisplays();
+        initFragment();
+        mHandler.flush();
+        var mainPref = (MainSwitchPreference) mPreferenceScreen.findPreference(
+                EXTERNAL_DISPLAY_USE_PREFERENCE_KEY);
+        assertThat(mainPref).isNotNull();
+        assertThat("" + mainPref.getTitle()).isEqualTo(
+                getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE));
+        assertThat(mainPref.isChecked()).isFalse();
+        assertThat(mainPref.isEnabled()).isFalse();
+        assertThat(mainPref.getOnPreferenceChangeListener()).isNull();
+        var footerPref = (FooterPreference) mPreferenceScreen.findPreference(KEY_FOOTER);
+        assertThat(footerPref).isNotNull();
+        verify(footerPref).setTitle(EXTERNAL_DISPLAY_NOT_FOUND_FOOTER_RESOURCE);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testDisplayRotationPreference() {
+        mDisplayIdArg = 1;
+        doReturn(true).when(mMockedInjector).isDisplayEnabled(any());
+        var fragment = initFragment();
+        mHandler.flush();
+        var pref = fragment.getRotationPreference(mContext);
+        assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_ROTATION_KEY);
+        assertThat("" + pref.getTitle()).isEqualTo(
+                getText(EXTERNAL_DISPLAY_ROTATION_TITLE_RESOURCE));
+        assertThat(pref.getEntries().length).isEqualTo(4);
+        assertThat(pref.getEntryValues().length).isEqualTo(4);
+        assertThat(pref.getEntryValues()[0].toString()).isEqualTo("0");
+        assertThat(pref.getEntryValues()[1].toString()).isEqualTo("1");
+        assertThat(pref.getEntryValues()[2].toString()).isEqualTo("2");
+        assertThat(pref.getEntryValues()[3].toString()).isEqualTo("3");
+        assertThat(pref.getEntries()[0].length()).isGreaterThan(0);
+        assertThat(pref.getEntries()[1].length()).isGreaterThan(0);
+        assertThat("" + pref.getSummary()).isEqualTo(pref.getEntries()[0].toString());
+        assertThat(pref.getValue()).isEqualTo("0");
+        assertThat(pref.getOnPreferenceChangeListener()).isNotNull();
+        assertThat(pref.isEnabled()).isTrue();
+        var rotation = 1;
+        doReturn(true).when(mMockedInjector).freezeDisplayRotation(mDisplayIdArg, rotation);
+        assertThat(pref.getOnPreferenceChangeListener().onPreferenceChange(pref, rotation + ""))
+                .isTrue();
+        verify(mMockedInjector).freezeDisplayRotation(mDisplayIdArg, rotation);
+        assertThat(pref.getValue()).isEqualTo(rotation + "");
+        verify(mMockedMetricsLogger).writePreferenceClickMetric(pref);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testDisplayResolutionPreference() {
+        mDisplayIdArg = 1;
+        doReturn(true).when(mMockedInjector).isDisplayEnabled(any());
+        var fragment = initFragment();
+        mHandler.flush();
+        var pref = fragment.getResolutionPreference(mContext);
+        assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_RESOLUTION_PREFERENCE_KEY);
+        assertThat("" + pref.getTitle()).isEqualTo(
+                getText(EXTERNAL_DISPLAY_RESOLUTION_TITLE_RESOURCE));
+        assertThat("" + pref.getSummary()).isEqualTo("1920 x 1080");
+        assertThat(pref.isEnabled()).isTrue();
+        assertThat(pref.getOnPreferenceClickListener()).isNotNull();
+        assertThat(pref.getOnPreferenceClickListener().onPreferenceClick(pref)).isTrue();
+        assertThat(mResolutionSelectorDisplayId).isEqualTo(mDisplayIdArg);
+        verify(mMockedMetricsLogger).writePreferenceClickMetric(pref);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testUseDisplayPreference_EnabledDisplay() {
+        mDisplayIdArg = 1;
+        doReturn(true).when(mMockedInjector).isDisplayEnabled(any());
+        doReturn(true).when(mMockedInjector).enableConnectedDisplay(mDisplayIdArg);
+        doReturn(true).when(mMockedInjector).disableConnectedDisplay(mDisplayIdArg);
+        var fragment = initFragment();
+        mHandler.flush();
+        var pref = fragment.getUseDisplayPreference(mContext);
+        assertThat(pref.getKey()).isEqualTo(EXTERNAL_DISPLAY_USE_PREFERENCE_KEY);
+        assertThat("" + pref.getTitle()).isEqualTo(getText(EXTERNAL_DISPLAY_USE_TITLE_RESOURCE));
+        assertThat(pref.isEnabled()).isTrue();
+        assertThat(pref.isChecked()).isTrue();
+        assertThat(pref.getOnPreferenceChangeListener()).isNotNull();
+        assertThat(pref.getOnPreferenceChangeListener().onPreferenceChange(pref, false)).isTrue();
+        verify(mMockedInjector).disableConnectedDisplay(mDisplayIdArg);
+        assertThat(pref.isChecked()).isFalse();
+        assertThat(pref.getOnPreferenceChangeListener().onPreferenceChange(pref, true)).isTrue();
+        verify(mMockedInjector).enableConnectedDisplay(mDisplayIdArg);
+        assertThat(pref.isChecked()).isTrue();
+        verify(mMockedMetricsLogger, times(2)).writePreferenceClickMetric(pref);
+    }
+
+    @NonNull
+    private ExternalDisplayPreferenceFragment initFragment() {
+        if (mFragment != null) {
+            return mFragment;
+        }
+        mFragment = new TestableExternalDisplayPreferenceFragment();
+        mFragment.onCreateCallback(null);
+        mFragment.onActivityCreatedCallback(null);
+        mFragment.onStartCallback();
+        return mFragment;
+    }
+
+    @NonNull
+    private Bundle createBundleForPreviouslyShownList() {
+        var state = new Bundle();
+        state.putBoolean(PREVIOUSLY_SHOWN_LIST_KEY, true);
+        return state;
+    }
+
+    @NonNull
+    private String getText(int id) {
+        return mContext.getResources().getText(id).toString();
+    }
+
+    private class TestableExternalDisplayPreferenceFragment extends
+            ExternalDisplayPreferenceFragment {
+        private final View mMockedRootView;
+        private final TextView mEmptyView;
+        private final Activity mMockedActivity;
+        private final FooterPreference mMockedFooterPreference;
+        private final MetricsLogger mLogger;
+
+        TestableExternalDisplayPreferenceFragment() {
+            super(mMockedInjector);
+            mMockedActivity = mock(Activity.class);
+            mMockedRootView = mock(View.class);
+            mMockedFooterPreference = mock(FooterPreference.class);
+            doReturn(KEY_FOOTER).when(mMockedFooterPreference).getKey();
+            mEmptyView = new TextView(mContext);
+            doReturn(mEmptyView).when(mMockedRootView).findViewById(android.R.id.empty);
+            mLogger = mMockedMetricsLogger;
+        }
+
+        @Override
+        public PreferenceScreen getPreferenceScreen() {
+            return mPreferenceScreen;
+        }
+
+        @Override
+        protected Activity getCurrentActivity() {
+            return mMockedActivity;
+        }
+
+        @Override
+        public View getView() {
+            return mMockedRootView;
+        }
+
+        @Override
+        public void setEmptyView(View view) {
+            assertThat(view).isEqualTo(mEmptyView);
+        }
+
+        @Override
+        public View getEmptyView() {
+            return mEmptyView;
+        }
+
+        @Override
+        public void addPreferencesFromResource(int resource) {
+            mPreferenceIdFromResource = resource;
+        }
+
+        @Override
+        @NonNull
+        FooterPreference getFooterPreference(@NonNull Context context) {
+            return mMockedFooterPreference;
+        }
+
+        @Override
+        protected int getDisplayIdArg() {
+            return mDisplayIdArg;
+        }
+
+        @Override
+        protected void launchResolutionSelector(@NonNull Context context, int displayId) {
+            mResolutionSelectorDisplayId = displayId;
+        }
+
+        @Override
+        protected void launchDisplaySettings(final int displayId) {
+            mDisplayIdArg = displayId;
+        }
+
+        @Override
+        protected void writePreferenceClickMetric(Preference preference) {
+            mLogger.writePreferenceClickMetric(preference);
+        }
+    }
+
+    /**
+     * Interface allowing to mock and spy on log events.
+     */
+    public interface MetricsLogger {
+
+        /**
+         * On preference click metric
+         */
+        void writePreferenceClickMetric(Preference preference);
+    }
+}
diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayTestBase.java b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayTestBase.java
new file mode 100644
index 0000000..60b0342
--- /dev/null
+++ b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayTestBase.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.connecteddevice.display;
+
+import static com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY;
+import static com.android.settings.flags.Flags.FLAG_ROTATION_CONNECTED_DISPLAY_SETTING;
+import static com.android.settings.flags.Flags.FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.hardware.display.DisplayManagerGlobal;
+import android.hardware.display.IDisplayManager;
+import android.os.RemoteException;
+import android.view.Display;
+import android.view.DisplayAdjustments;
+import android.view.DisplayInfo;
+
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.testutils.TestHandler;
+import com.android.settings.connecteddevice.display.ExternalDisplaySettingsConfiguration.DisplayListener;
+import com.android.settings.flags.FakeFeatureFlagsImpl;
+
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class ExternalDisplayTestBase {
+    @Mock
+    ExternalDisplaySettingsConfiguration.Injector mMockedInjector;
+    @Mock
+    IDisplayManager mMockedIDisplayManager;
+    Resources mResources;
+    DisplayManagerGlobal mDisplayManagerGlobal;
+    FakeFeatureFlagsImpl mFlags = new FakeFeatureFlagsImpl();
+    Context mContext;
+    DisplayListener mListener;
+    TestHandler mHandler = new TestHandler(null);
+    PreferenceManager mPreferenceManager;
+    PreferenceScreen mPreferenceScreen;
+    Display[] mDisplays;
+
+    /**
+     * Setup.
+     */
+    @Before
+    public void setUp() throws RemoteException {
+        MockitoAnnotations.initMocks(this);
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        mResources = spy(mContext.getResources());
+        doReturn(mResources).when(mContext).getResources();
+        mPreferenceManager = new PreferenceManager(mContext);
+        mPreferenceScreen = mPreferenceManager.createPreferenceScreen(mContext);
+        doReturn(0).when(mMockedIDisplayManager).getPreferredWideGamutColorSpaceId();
+        mDisplayManagerGlobal = new DisplayManagerGlobal(mMockedIDisplayManager);
+        mFlags.setFlag(FLAG_ROTATION_CONNECTED_DISPLAY_SETTING, true);
+        mFlags.setFlag(FLAG_RESOLUTION_AND_ENABLE_CONNECTED_DISPLAY_SETTING, true);
+        mDisplays = new Display[] {
+                createDefaultDisplay(), createExternalDisplay(), createOverlayDisplay()};
+        doReturn(mDisplays).when(mMockedInjector).getAllDisplays();
+        doReturn(mDisplays).when(mMockedInjector).getEnabledDisplays();
+        for (var display : mDisplays) {
+            doReturn(display).when(mMockedInjector).getDisplay(display.getDisplayId());
+        }
+        doReturn(mFlags).when(mMockedInjector).getFlags();
+        doReturn(mHandler).when(mMockedInjector).getHandler();
+        doReturn("").when(mMockedInjector).getSystemProperty(
+                VIRTUAL_DISPLAY_PACKAGE_NAME_SYSTEM_PROPERTY);
+        doAnswer((arg) -> {
+            mListener = arg.getArgument(0);
+            return null;
+        }).when(mMockedInjector).registerDisplayListener(any());
+        doReturn(0).when(mMockedInjector).getDisplayUserRotation(anyInt());
+        doReturn(mContext).when(mMockedInjector).getContext();
+    }
+
+    Display createDefaultDisplay() throws RemoteException {
+        int displayId = 0;
+        var displayInfo = new DisplayInfo();
+        doReturn(displayInfo).when(mMockedIDisplayManager).getDisplayInfo(displayId);
+        displayInfo.displayId = displayId;
+        displayInfo.name = "Built-in";
+        displayInfo.type = Display.TYPE_INTERNAL;
+        displayInfo.supportedModes = new Display.Mode[]{
+                new Display.Mode(0, 2048, 1024, 60, 60, new float[0],
+                    new int[0])};
+        displayInfo.appsSupportedModes = displayInfo.supportedModes;
+        return createDisplay(displayInfo);
+    }
+
+    Display createExternalDisplay() throws RemoteException {
+        int displayId = 1;
+        var displayInfo = new DisplayInfo();
+        doReturn(displayInfo).when(mMockedIDisplayManager).getDisplayInfo(displayId);
+        displayInfo.displayId = displayId;
+        displayInfo.name = "HDMI";
+        displayInfo.type = Display.TYPE_EXTERNAL;
+        displayInfo.supportedModes = new Display.Mode[]{
+                new Display.Mode(0, 1920, 1080, 60, 60, new float[0], new int[0]),
+                new Display.Mode(1, 800, 600, 60, 60, new float[0], new int[0]),
+                new Display.Mode(2, 320, 240, 70, 70, new float[0], new int[0]),
+                new Display.Mode(3, 640, 480, 60, 60, new float[0], new int[0]),
+                new Display.Mode(4, 640, 480, 50, 60, new float[0], new int[0]),
+                new Display.Mode(5, 2048, 1024, 60, 60, new float[0], new int[0]),
+                new Display.Mode(6, 720, 480, 60, 60, new float[0], new int[0])};
+        displayInfo.appsSupportedModes = displayInfo.supportedModes;
+        return createDisplay(displayInfo);
+    }
+
+    Display createOverlayDisplay() throws RemoteException {
+        int displayId = 2;
+        var displayInfo = new DisplayInfo();
+        doReturn(displayInfo).when(mMockedIDisplayManager).getDisplayInfo(displayId);
+        displayInfo.displayId = displayId;
+        displayInfo.name = "Overlay #1";
+        displayInfo.type = Display.TYPE_OVERLAY;
+        displayInfo.supportedModes = new Display.Mode[]{
+                new Display.Mode(0, 1240, 780, 60, 60, new float[0],
+                    new int[0])};
+        displayInfo.appsSupportedModes = displayInfo.supportedModes;
+        return createDisplay(displayInfo);
+    }
+
+    Display createDisplay(DisplayInfo displayInfo) {
+        return new Display(mDisplayManagerGlobal, displayInfo.displayId, displayInfo,
+                (DisplayAdjustments) null);
+    }
+}
diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdaterTest.java b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdaterTest.java
new file mode 100644
index 0000000..824974a
--- /dev/null
+++ b/tests/unit/src/com/android/settings/connecteddevice/display/ExternalDisplayUpdaterTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.connecteddevice.display;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.RemoteException;
+import android.view.Display;
+
+import androidx.annotation.Nullable;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.connecteddevice.DevicePreferenceCallback;
+import com.android.settingslib.RestrictedLockUtils;
+import com.android.settingslib.RestrictedPreference;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/** Unit tests for {@link ExternalDisplayUpdater}.  */
+@RunWith(AndroidJUnit4.class)
+public class ExternalDisplayUpdaterTest extends ExternalDisplayTestBase {
+
+    private ExternalDisplayUpdater mUpdater;
+    @Mock
+    private DevicePreferenceCallback mMockedCallback;
+    @Mock
+    private Drawable mMockedDrawable;
+    private RestrictedPreference mPreferenceAdded;
+    private RestrictedPreference mPreferenceRemoved;
+
+    @Before
+    public void setUp() throws RemoteException {
+        super.setUp();
+        mUpdater = new TestableExternalDisplayUpdater(mMockedCallback, /*metricsCategory=*/ 0);
+    }
+
+    @Test
+    public void testPreferenceAdded() {
+        doAnswer((v) -> {
+            mPreferenceAdded = v.getArgument(0);
+            return null;
+        }).when(mMockedCallback).onDeviceAdded(any());
+        mUpdater.initPreference(mContext, mMockedInjector);
+        mUpdater.registerCallback();
+        mHandler.flush();
+        assertThat(mPreferenceAdded).isNotNull();
+        var summary = mPreferenceAdded.getSummary();
+        assertThat(summary).isNotNull();
+        assertThat(summary.length()).isGreaterThan(0);
+        var title = mPreferenceAdded.getTitle();
+        assertThat(title).isNotNull();
+        assertThat(title.length()).isGreaterThan(0);
+    }
+
+    @Test
+    public void testPreferenceRemoved() {
+        doAnswer((v) -> {
+            mPreferenceAdded = v.getArgument(0);
+            return null;
+        }).when(mMockedCallback).onDeviceAdded(any());
+        doAnswer((v) -> {
+            mPreferenceRemoved = v.getArgument(0);
+            return null;
+        }).when(mMockedCallback).onDeviceRemoved(any());
+        mUpdater.initPreference(mContext, mMockedInjector);
+        mUpdater.registerCallback();
+        mHandler.flush();
+        assertThat(mPreferenceAdded).isNotNull();
+        assertThat(mPreferenceRemoved).isNull();
+        // Remove display
+        doReturn(new Display[0]).when(mMockedInjector).getAllDisplays();
+        doReturn(new Display[0]).when(mMockedInjector).getEnabledDisplays();
+        mListener.onDisplayRemoved(1);
+        mHandler.flush();
+        assertThat(mPreferenceRemoved).isEqualTo(mPreferenceAdded);
+    }
+
+    class TestableExternalDisplayUpdater extends ExternalDisplayUpdater {
+        TestableExternalDisplayUpdater(
+                DevicePreferenceCallback callback,
+                int metricsCategory) {
+            super(callback, metricsCategory);
+        }
+
+        @Override
+        @Nullable
+        protected RestrictedLockUtils.EnforcedAdmin checkIfUsbDataSignalingIsDisabled(
+                Context context) {
+            // if null is returned - usb signalling is enabled
+            return null;
+        }
+
+        @Override
+        @Nullable
+        protected Drawable getDrawable(Context context) {
+            return mMockedDrawable;
+        }
+    }
+}
diff --git a/tests/unit/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragmentTest.java b/tests/unit/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragmentTest.java
new file mode 100644
index 0000000..ee38a1c
--- /dev/null
+++ b/tests/unit/src/com/android/settings/connecteddevice/display/ResolutionPreferenceFragmentTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.connecteddevice.display;
+
+import static android.view.Display.INVALID_DISPLAY;
+
+import static com.android.settings.connecteddevice.display.ResolutionPreferenceFragment.EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE;
+import static com.android.settings.connecteddevice.display.ResolutionPreferenceFragment.MORE_OPTIONS_KEY;
+import static com.android.settings.connecteddevice.display.ResolutionPreferenceFragment.TOP_OPTIONS_KEY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settingslib.widget.SelectorWithWidgetPreference;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/** Unit tests for {@link ResolutionPreferenceFragment}.  */
+@RunWith(AndroidJUnit4.class)
+public class ResolutionPreferenceFragmentTest extends ExternalDisplayTestBase {
+    @Nullable
+    private ResolutionPreferenceFragment mFragment;
+    private int mPreferenceIdFromResource;
+    private int mDisplayIdArg = INVALID_DISPLAY;
+    @Mock
+    private MetricsLogger mMockedMetricsLogger;
+
+    @Test
+    @UiThreadTest
+    public void testCreateAndStart() {
+        initFragment();
+        mHandler.flush();
+        assertThat(mPreferenceIdFromResource).isEqualTo(
+                EXTERNAL_DISPLAY_RESOLUTION_SETTINGS_RESOURCE);
+        var pref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY);
+        assertThat(pref).isNull();
+        pref = mPreferenceScreen.findPreference(MORE_OPTIONS_KEY);
+        assertThat(pref).isNull();
+    }
+
+    @Test
+    @UiThreadTest
+    public void testCreateAndStartDefaultDisplayNotAllowed() {
+        mDisplayIdArg = 0;
+        initFragment();
+        mHandler.flush();
+        var pref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY);
+        assertThat(pref).isNull();
+        pref = mPreferenceScreen.findPreference(MORE_OPTIONS_KEY);
+        assertThat(pref).isNull();
+    }
+
+    @Test
+    @UiThreadTest
+    public void testModePreferences() {
+        mDisplayIdArg = 1;
+        initFragment();
+        mHandler.flush();
+        PreferenceCategory topPref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY);
+        assertThat(topPref).isNotNull();
+        PreferenceCategory morePref = mPreferenceScreen.findPreference(MORE_OPTIONS_KEY);
+        assertThat(morePref).isNotNull();
+        assertThat(topPref.getPreferenceCount()).isEqualTo(3);
+        assertThat(morePref.getPreferenceCount()).isEqualTo(1);
+    }
+
+    @Test
+    @UiThreadTest
+    public void testModeChange() {
+        mDisplayIdArg = 1;
+        initFragment();
+        mHandler.flush();
+        PreferenceCategory topPref = mPreferenceScreen.findPreference(TOP_OPTIONS_KEY);
+        assertThat(topPref).isNotNull();
+        var modePref = (SelectorWithWidgetPreference) topPref.getPreference(1);
+        modePref.onClick();
+        var mode = mDisplays[mDisplayIdArg].getSupportedModes()[1];
+        verify(mMockedInjector).setUserPreferredDisplayMode(mDisplayIdArg, mode);
+    }
+
+    private void initFragment() {
+        if (mFragment != null) {
+            return;
+        }
+        mFragment = new TestableResolutionPreferenceFragment();
+        mFragment.onCreateCallback(null);
+        mFragment.onActivityCreatedCallback(null);
+        mFragment.onStartCallback();
+    }
+
+    private class TestableResolutionPreferenceFragment extends ResolutionPreferenceFragment {
+        private final View mMockedRootView;
+        private final TextView mEmptyView;
+        private final Resources mMockedResources;
+        private final MetricsLogger mLogger;
+        TestableResolutionPreferenceFragment() {
+            super(mMockedInjector);
+            mMockedResources = mock(Resources.class);
+            doReturn(61).when(mMockedResources).getInteger(
+                    com.android.internal.R.integer.config_externalDisplayPeakRefreshRate);
+            doReturn(1920).when(mMockedResources).getInteger(
+                    com.android.internal.R.integer.config_externalDisplayPeakWidth);
+            doReturn(1080).when(mMockedResources).getInteger(
+                    com.android.internal.R.integer.config_externalDisplayPeakHeight);
+            doReturn(true).when(mMockedResources).getBoolean(
+                    com.android.internal.R.bool.config_refreshRateSynchronizationEnabled);
+            mMockedRootView = mock(View.class);
+            mEmptyView = new TextView(mContext);
+            doReturn(mEmptyView).when(mMockedRootView).findViewById(android.R.id.empty);
+            mLogger = mMockedMetricsLogger;
+        }
+
+        @Override
+        public PreferenceScreen getPreferenceScreen() {
+            return mPreferenceScreen;
+        }
+
+        @Override
+        public View getView() {
+            return mMockedRootView;
+        }
+
+        @Override
+        public void setEmptyView(View view) {
+            assertThat(view).isEqualTo(mEmptyView);
+        }
+
+        @Override
+        public View getEmptyView() {
+            return mEmptyView;
+        }
+
+        @Override
+        public void addPreferencesFromResource(int resource) {
+            mPreferenceIdFromResource = resource;
+        }
+
+        @Override
+        protected int getDisplayIdArg() {
+            return mDisplayIdArg;
+        }
+
+        @Override
+        protected void writePreferenceClickMetric(Preference preference) {
+            mLogger.writePreferenceClickMetric(preference);
+        }
+
+        @Override
+        @NonNull
+        protected Resources getResources(@NonNull Context context) {
+            return mMockedResources;
+        }
+    }
+
+    /**
+     * Interface allowing to mock and spy on log events.
+     */
+    public interface MetricsLogger {
+        /**
+         * On preference click metric
+         */
+        void writePreferenceClickMetric(Preference preference);
+    }
+}