Merge "Extend SfpsEnrollmentFeature for text updating when progress changes" into main
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9ae77b2..14879e2 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -5192,6 +5192,18 @@
             android:theme="@style/Theme.SpaLib.Dialog">
         </activity>
 
+        <activity android:name="Settings$BluetoothDashboardActivity"
+            android:label="@string/bluetooth_settings_title"
+            android:permission="android.permission.BLUETOOTH_CONNECT"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.settings.BLUETOOTH_DASHBOARD_SETTINGS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
+                android:value="com.android.settings.connecteddevice.BluetoothDashboardFragment"/>
+        </activity>
+
         <activity
             android:name="com.android.settings.connecteddevice.audiosharing.AudioSharingActivity"
             android:label="@string/audio_sharing_title"
diff --git a/res/layout-land/fingerprint_v2_udfps_enroll_enrolling.xml b/res/layout-land/fingerprint_v2_udfps_enroll_enrolling.xml
index 86768d6..669c282 100644
--- a/res/layout-land/fingerprint_v2_udfps_enroll_enrolling.xml
+++ b/res/layout-land/fingerprint_v2_udfps_enroll_enrolling.xml
@@ -14,87 +14,65 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-
-<LinearLayout
+<!-- This is used to grab style attributes and apply them
+to this layout -->
+<com.google.android.setupdesign.GlifLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/udfps_layout"
+    android:id="@+id/glif_layout"
     style="?attr/fingerprint_layout_theme"
     android:layout_width="match_parent"
+    android:clipChildren="false"
+    android:clipToPadding="false"
     android:layout_height="match_parent"
-    android:orientation="horizontal">
-
-    <!-- This is used to grab style attributes and apply them
-    to this layout -->
-    <com.google.android.setupdesign.GlifLayout
-        android:id="@+id/dummy_glif_layout"
-        style="?attr/fingerprint_layout_theme"
-        android:layout_width="0dp"
-        android:layout_height="0dp"
-        android:visibility="gone"
-        />
+    >
 
     <LinearLayout
-        android:layout_width="300dp"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
 
-        <ImageView
-            android:id="@+id/sud_layout_icon"
-            style="@style/SudGlifIcon"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:scaleType="fitStart"
-            android:src="@drawable/ic_lock" />
-
-        <TextView
-            android:id="@+id/title"
-            style="@style/SudGlifHeaderTitle"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:ellipsize="end"
-            android:lines="2"
-            />
-
-        <TextView
-            android:id="@+id/description"
-            style="@style/SudDescription.Glif"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:ellipsize="end"
-            android:lines="3"
-            android:paddingLeft="10dp"
-            android:paddingRight="10dp"
-            />
-
-
-        <com.airbnb.lottie.LottieAnimationView
-            android:id="@+id/illustration_lottie"
-            android:layout_width="match_parent"
-            android:layout_height="0dp"
-            android:clipChildren="false"
-            android:clipToPadding="false"
-            android:paddingLeft="10dp"
-            android:paddingRight="10dp"
-            android:scaleType="centerInside"
-            android:visibility="gone"
-            app:lottie_autoPlay="true"
-            app:lottie_loop="true"
-            app:lottie_speed=".85"
-            />
-    </LinearLayout>
-
-    <FrameLayout
-        android:id="@+id/layout_container"
+        style="@style/SudContentFrame"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_gravity="center_horizontal|bottom"
+        android:clipChildren="false"
         android:clipToPadding="false"
+        android:orientation="vertical"
         >
 
-        <include layout="@layout/fingerprint_v2_udfps_enroll_view" />
-    </FrameLayout>
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:gravity="center|bottom"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:orientation="vertical">
 
+            <FrameLayout
+                android:id="@+id/layout_container"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_gravity="center_horizontal|bottom"
+                android:clipChildren="false"
+                android:clipToPadding="false"
+                >
 
-</LinearLayout>
+                <com.airbnb.lottie.LottieAnimationView
+                    android:id="@+id/illustration_lottie"
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    android:clipChildren="false"
+                    android:clipToPadding="false"
+                    android:scaleType="centerInside"
+                    android:visibility="gone"
+                    app:lottie_autoPlay="true"
+                    app:lottie_loop="true"
+                    app:lottie_speed=".85"
+                    />
+
+                <include layout="@layout/fingerprint_v2_udfps_enroll_view" />
+
+            </FrameLayout>
+        </LinearLayout>
+    </LinearLayout>
+</com.google.android.setupdesign.GlifLayout>
+
 
diff --git a/res/layout/data_usage_summary_preference.xml b/res/layout/data_usage_summary_preference.xml
index 4cbd958..e678b36 100644
--- a/res/layout/data_usage_summary_preference.xml
+++ b/res/layout/data_usage_summary_preference.xml
@@ -18,6 +18,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:minHeight="172dp"
     android:paddingTop="8dp"
     android:paddingBottom="16dp"
     android:paddingStart="?android:attr/listPreferredItemPaddingStart"
@@ -37,7 +38,6 @@
         android:id="@+id/usage_layout"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:paddingTop="12dp"
         android:orientation="horizontal">
 
         <TextView android:id="@+id/data_usage_view"
diff --git a/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml b/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml
index ab8fb2c..497e164 100644
--- a/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml
+++ b/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml
@@ -19,7 +19,6 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/udfps_layout"
-    style="?attr/fingerprint_layout_theme"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
@@ -27,65 +26,57 @@
     <!-- This is used to grab style attributes and apply them
     to this layout -->
     <com.google.android.setupdesign.GlifLayout
-        android:id="@+id/dummy_glif_layout"
+        android:id="@+id/glif_layout"
         style="?attr/fingerprint_layout_theme"
-        android:layout_width="0dp"
+        android:layout_width="match_parent"
         android:layout_height="0dp"
-        android:visibility="gone"
-        />
+        android:layout_weight="5"
+        >
 
-    <ImageView
-        android:id="@+id/sud_layout_icon"
-        style="@style/SudGlifIcon"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:scaleType="fitStart"
-        android:src="@drawable/ic_lock" />
+        <LinearLayout
+            style="@style/SudContentFrame"
+            android:id="@+id/sud_content_frame"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            >
 
-    <TextView
-        android:id="@+id/title"
-        style="@style/SudGlifHeaderTitle"
-        android:layout_width="match_parent"
-        android:layout_height="80dp"
-        android:ellipsize="end"
-        android:lines="2"
-        />
-
-    <TextView
-        android:id="@+id/description"
-        style="@style/SudDescription.Glif"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:ellipsize="end"
-        android:lines="3"
-        android:paddingLeft="10dp"
-        android:paddingRight="10dp"
-        />
-
-
-    <com.airbnb.lottie.LottieAnimationView
-        android:id="@+id/illustration_lottie"
-        android:layout_width="match_parent"
-        android:layout_height="200dp"
-        android:clipChildren="false"
-        android:clipToPadding="false"
-        android:paddingLeft="10dp"
-        android:paddingRight="10dp"
-        android:scaleType="centerInside"
-        app:lottie_autoPlay="true"
-        app:lottie_loop="true"
-        app:lottie_speed=".85" />
+            <com.airbnb.lottie.LottieAnimationView
+                android:id="@+id/illustration_lottie"
+                android:layout_width="match_parent"
+                android:layout_height="200dp"
+                android:clipChildren="false"
+                android:clipToPadding="false"
+                android:paddingLeft="10dp"
+                android:paddingRight="10dp"
+                android:scaleType="centerInside"
+                app:lottie_autoPlay="true"
+                app:lottie_loop="true"
+                app:lottie_speed=".85" />
+        </LinearLayout>
+    </com.google.android.setupdesign.GlifLayout>
 
     <FrameLayout
         android:id="@+id/layout_container"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_height="0dp"
         android:layout_gravity="center_horizontal|bottom"
+        android:layout_weight="4"
         android:clipToPadding="false"
+        android:clipChildren="false"
         >
 
         <include layout="@layout/fingerprint_v2_udfps_enroll_view" />
-    </FrameLayout>
 
+        <Button
+            android:id="@+id/skip"
+            style="@style/SudGlifButton.Secondary"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="bottom|left"
+            android:layout_marginStart="20dp"
+            android:layout_marginBottom="20dp"
+            android:text="@string/security_settings_fingerprint_enroll_enrolling_skip" />
+
+    </FrameLayout>
 
 </LinearLayout>
diff --git a/res/layout/fingerprint_v2_udfps_enroll_view.xml b/res/layout/fingerprint_v2_udfps_enroll_view.xml
index 20df6e1..9002eca 100644
--- a/res/layout/fingerprint_v2_udfps_enroll_view.xml
+++ b/res/layout/fingerprint_v2_udfps_enroll_view.xml
@@ -21,6 +21,8 @@
     android:id="@+id/udfps_animation_view"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:clipChildren="false"
+    android:clipToPadding="false"
     android:orientation="vertical">
 
     <ImageView
diff --git a/res/raw/udfps_left_edge_hint_lottie.json b/res/raw/udfps_left_edge_hint_lottie.json
index 9e26dfe..e3c4ce1 100644
--- a/res/raw/udfps_left_edge_hint_lottie.json
+++ b/res/raw/udfps_left_edge_hint_lottie.json
@@ -1 +1 @@
-{}
\ No newline at end of file
+{"v":"5.8.1","fr":60,"ip":0,"op":211,"w":412,"h":200,"nm":"FPS Left Side","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".blue200","cl":"blue200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":55,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":85,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":115,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":140,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":165,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":170,"s":[100]},{"t":195,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[169.175,161.377,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[7.894,8.852],[0,0]],"v":[[-7.497,-6.019],[7.497,6.019]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-13.066,-25.141],[-1.414,-2.132]],"o":[[0,0],[1.326,2.55],[0,0]],"v":[[-25.707,-58.269],[-17.526,-20.998],[-13.399,-13.981]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[-10.063,-10.595],[0,0]],"o":[[0.312,1.13],[11.84,12.467],[0,0]],"v":[[-8.095,-38.352],[4.748,-14.866],[28.345,-2.398]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.68235296011,0.796078443527,0.980392158031,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Fingerprint Side","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[169.175,161.377,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[7.894,8.852],[0,0]],"v":[[-7.497,-6.019],[7.497,6.019]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-13.066,-25.141],[-1.414,-2.132]],"o":[[0,0],[1.326,2.55],[0,0]],"v":[[-25.707,-58.269],[-17.526,-20.998],[-13.399,-13.981]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[-10.063,-10.595],[0,0]],"o":[[0.312,1.13],[11.84,12.467],[0,0]],"v":[[-8.095,-38.352],[4.748,-14.866],[28.345,-2.398]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[3.585,11.306],[0,0]],"v":[[90.35,-61.769],[93.054,-42.731]],"c":false},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0,0],[-14.956,-24.065],[-1.112,-2.304]],"o":[[0,0],[1.517,2.441],[0,0]],"v":[[54.72,-104.215],[82.601,-78.164],[86.53,-71.035]],"c":false},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0,0],[-3.978,-14.061],[0,0]],"o":[[0.813,0.844],[4.681,16.544],[0,0]],"v":[[63.033,-78.092],[76.68,-55.064],[75.364,-28.408]],"c":false},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.596069335938,0.321563720703,0.239196777344,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":7,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Fingerprint","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[176.139,66.987,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.408,-0.232],[1.017,0.68],[-0.959,1.434],[-6.841,3.896],[-7.562,0.41],[-0.093,-1.722],[1.724,-0.094],[6.003,-3.419],[3.674,-5.493]],"o":[[-0.995,0.567],[-1.433,-0.959],[4.21,-6.294],[6.841,-3.896],[1.722,-0.093],[0.094,1.722],[-6.599,0.357],[-6.003,3.418],[-0.279,0.417]],"v":[[-16.693,13.1],[-19.976,12.983],[-20.836,8.652],[-3.944,-6.925],[18.072,-13.506],[21.359,-10.556],[18.409,-7.27],[-0.853,-1.498],[-15.645,12.124]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[7.044,-4.012],[6.96,0.908],[-0.223,1.71],[-1.711,-0.224],[-5.298,3.017],[-2.065,5.824],[-1.624,-0.577],[0.576,-1.626]],"o":[[-6.489,3.695],[-1.71,-0.223],[0.223,-1.71],[5.601,0.73],[5.742,-3.27],[0.577,-1.625],[1.625,0.576],[-2.566,7.236]],"v":[[44.597,78.169],[24.039,82.43],[21.346,78.93],[24.846,76.237],[41.507,72.742],[53.614,58.64],[57.601,56.74],[59.5,60.727]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.323,-0.184],[1.029,0.844],[0.13,0.229],[-15.469,8.81],[-8.28,-14.539],[1.498,-0.853],[0.853,1.498],[12.426,-7.076],[-6.547,-11.496],[-4.085,-3.35],[1.094,-1.333]],"o":[[-1.091,0.621],[-5.004,-4.103],[-8.28,-14.539],[15.469,-8.81],[0.853,1.498],[-1.498,0.853],[-6.547,-11.496],[-12.426,7.077],[0.024,0.042],[1.333,1.093],[-0.25,0.305]],"v":[[10.147,71.146],[6.621,70.847],[-1.636,61.086],[11.185,19.448],[53.537,29.665],[52.369,33.924],[48.111,32.756],[14.276,24.874],[3.791,57.996],[10.581,66.018],[11.016,70.413]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[1.183,-0.674],[0.287,-0.071],[3.505,8.779],[-6.102,3.475],[-3.887,-4.766],[0,0],[-3.067,1.747],[1.97,3.459],[0,0],[-1.498,0.853],[-0.853,-1.498],[0,0],[6.451,-3.674],[4.175,5.108],[0,0],[2.675,-1.523],[-1.489,-3.73],[-5.283,1.305],[-0.414,-1.674]],"o":[[-0.244,0.139],[-9.188,2.268],[-2.649,-6.633],[5.335,-3.038],[0,0],[2.235,2.735],[3.459,-1.97],[0,0],[-0.853,-1.498],[1.498,-0.853],[0,0],[3.674,6.452],[-5.725,3.26],[0,0],[-1.949,-2.391],[-3.377,1.923],[2.674,6.697],[1.674,-0.413],[0.342,1.387]],"v":[[36.276,64.961],[35.477,65.279],[11.151,49.991],[16.667,33.848],[32.582,36.831],[36.686,41.876],[45.842,43.586],[48.542,33.739],[47.994,32.777],[49.163,28.518],[53.421,29.686],[53.969,30.648],[48.932,49.012],[31.846,45.823],[27.739,40.774],[19.758,39.275],[16.951,47.675],[33.98,59.216],[37.761,61.499]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.501,-0.285],[0.978,0.515],[11.066,-6.302],[0.514,-9.958],[1.721,0.088],[-0.089,1.722],[-13.068,7.442],[-10.828,-5.692],[0.802,-1.526]],"o":[[-0.895,0.51],[-8.827,-4.639],[-11.066,6.302],[-0.089,1.722],[-1.722,-0.089],[0.63,-12.217],[13.068,-7.442],[1.527,0.802],[-0.288,0.548]],"v":[[42.238,8.912],[39.237,8.964],[6.49,11.701],[-12.571,38.468],[-15.851,41.426],[-18.808,38.146],[3.4,6.274],[42.142,3.436],[43.453,7.653]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.596078455448,0.321568638086,0.239215686917,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":6,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Thumb","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[225.438,108.028,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[42.783,10.613],[3.8,0.302],[0,0],[0.105,0.008],[0.484,0.019],[6.105,-2.438],[0,0],[0.032,-0.013],[0.85,-0.396],[0,0],[0,0],[0,0],[0,0],[-26.827,-45.427],[0,0],[0,0]],"o":[[-28.424,-53.851],[-3.608,-1.195],[0,0],[-0.106,-0.009],[-0.482,-0.035],[-6.685,-0.314],[0,0],[-0.032,0.013],[-0.853,0.343],[-20.172,9.126],[0,0],[-6.217,10.813],[0,0],[-5.276,37.379],[0,0],[0,0],[0,0]],"v":[[76.157,43.103],[-34.58,-89.69],[-45.717,-91.957],[-45.708,-91.966],[-46.022,-91.983],[-47.471,-92.063],[-66.474,-88.859],[-66.467,-88.866],[-66.56,-88.826],[-69.115,-87.715],[-94.124,-66.879],[-94.112,-66.885],[-101.514,-44.23],[-101.507,-44.234],[-53.003,92.116],[30.37,92.116],[102.173,92.116]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.678431391716,0.403921574354,0.305882364511,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".grey900","cl":"grey900","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[206,100,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[412,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":38,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.1254902035,0.129411771894,0.141176477075,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/res/raw/udfps_right_edge_hint_lottie.json b/res/raw/udfps_right_edge_hint_lottie.json
index 9e26dfe..93a75b3 100644
--- a/res/raw/udfps_right_edge_hint_lottie.json
+++ b/res/raw/udfps_right_edge_hint_lottie.json
@@ -1 +1 @@
-{}
\ No newline at end of file
+{"v":"5.8.1","fr":60,"ip":0,"op":211,"w":412,"h":200,"nm":"FPS Right Side","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".blue200","cl":"blue200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":55,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":85,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":115,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":140,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":165,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":170,"s":[100]},{"t":195,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[169.175,161.377,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[3.585,11.306],[0,0]],"v":[[90.35,-61.769],[93.054,-42.731]],"c":false},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-14.956,-24.065],[-1.112,-2.304]],"o":[[0,0],[1.517,2.441],[0,0]],"v":[[54.72,-104.215],[82.601,-78.164],[86.53,-71.035]],"c":false},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[-3.978,-14.061],[0,0]],"o":[[0.813,0.844],[4.681,16.544],[0,0]],"v":[[63.033,-78.092],[76.68,-55.064],[75.364,-28.408]],"c":false},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.68235296011,0.796078443527,0.980392158031,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Fingerprint Side","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[169.175,161.377,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[7.894,8.852],[0,0]],"v":[[-7.497,-6.019],[7.497,6.019]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-13.066,-25.141],[-1.414,-2.132]],"o":[[0,0],[1.326,2.55],[0,0]],"v":[[-25.707,-58.269],[-17.526,-20.998],[-13.399,-13.981]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[-10.063,-10.595],[0,0]],"o":[[0.312,1.13],[11.84,12.467],[0,0]],"v":[[-8.095,-38.352],[4.748,-14.866],[28.345,-2.398]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[3.585,11.306],[0,0]],"v":[[90.35,-61.769],[93.054,-42.731]],"c":false},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0,0],[-14.956,-24.065],[-1.112,-2.304]],"o":[[0,0],[1.517,2.441],[0,0]],"v":[[54.72,-104.215],[82.601,-78.164],[86.53,-71.035]],"c":false},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ind":5,"ty":"sh","ix":6,"ks":{"a":0,"k":{"i":[[0,0],[-3.978,-14.061],[0,0]],"o":[[0.813,0.844],[4.681,16.544],[0,0]],"v":[[63.033,-78.092],[76.68,-55.064],[75.364,-28.408]],"c":false},"ix":2},"nm":"Path 6","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.596069335938,0.321563720703,0.239196777344,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":7,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Fingerprint","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[176.139,66.987,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.408,-0.232],[1.017,0.68],[-0.959,1.434],[-6.841,3.896],[-7.562,0.41],[-0.093,-1.722],[1.724,-0.094],[6.003,-3.419],[3.674,-5.493]],"o":[[-0.995,0.567],[-1.433,-0.959],[4.21,-6.294],[6.841,-3.896],[1.722,-0.093],[0.094,1.722],[-6.599,0.357],[-6.003,3.418],[-0.279,0.417]],"v":[[-16.693,13.1],[-19.976,12.983],[-20.836,8.652],[-3.944,-6.925],[18.072,-13.506],[21.359,-10.556],[18.409,-7.27],[-0.853,-1.498],[-15.645,12.124]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[7.044,-4.012],[6.96,0.908],[-0.223,1.71],[-1.711,-0.224],[-5.298,3.017],[-2.065,5.824],[-1.624,-0.577],[0.576,-1.626]],"o":[[-6.489,3.695],[-1.71,-0.223],[0.223,-1.71],[5.601,0.73],[5.742,-3.27],[0.577,-1.625],[1.625,0.576],[-2.566,7.236]],"v":[[44.597,78.169],[24.039,82.43],[21.346,78.93],[24.846,76.237],[41.507,72.742],[53.614,58.64],[57.601,56.74],[59.5,60.727]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.323,-0.184],[1.029,0.844],[0.13,0.229],[-15.469,8.81],[-8.28,-14.539],[1.498,-0.853],[0.853,1.498],[12.426,-7.076],[-6.547,-11.496],[-4.085,-3.35],[1.094,-1.333]],"o":[[-1.091,0.621],[-5.004,-4.103],[-8.28,-14.539],[15.469,-8.81],[0.853,1.498],[-1.498,0.853],[-6.547,-11.496],[-12.426,7.077],[0.024,0.042],[1.333,1.093],[-0.25,0.305]],"v":[[10.147,71.146],[6.621,70.847],[-1.636,61.086],[11.185,19.448],[53.537,29.665],[52.369,33.924],[48.111,32.756],[14.276,24.874],[3.791,57.996],[10.581,66.018],[11.016,70.413]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[1.183,-0.674],[0.287,-0.071],[3.505,8.779],[-6.102,3.475],[-3.887,-4.766],[0,0],[-3.067,1.747],[1.97,3.459],[0,0],[-1.498,0.853],[-0.853,-1.498],[0,0],[6.451,-3.674],[4.175,5.108],[0,0],[2.675,-1.523],[-1.489,-3.73],[-5.283,1.305],[-0.414,-1.674]],"o":[[-0.244,0.139],[-9.188,2.268],[-2.649,-6.633],[5.335,-3.038],[0,0],[2.235,2.735],[3.459,-1.97],[0,0],[-0.853,-1.498],[1.498,-0.853],[0,0],[3.674,6.452],[-5.725,3.26],[0,0],[-1.949,-2.391],[-3.377,1.923],[2.674,6.697],[1.674,-0.413],[0.342,1.387]],"v":[[36.276,64.961],[35.477,65.279],[11.151,49.991],[16.667,33.848],[32.582,36.831],[36.686,41.876],[45.842,43.586],[48.542,33.739],[47.994,32.777],[49.163,28.518],[53.421,29.686],[53.969,30.648],[48.932,49.012],[31.846,45.823],[27.739,40.774],[19.758,39.275],[16.951,47.675],[33.98,59.216],[37.761,61.499]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.501,-0.285],[0.978,0.515],[11.066,-6.302],[0.514,-9.958],[1.721,0.088],[-0.089,1.722],[-13.068,7.442],[-10.828,-5.692],[0.802,-1.526]],"o":[[-0.895,0.51],[-8.827,-4.639],[-11.066,6.302],[-0.089,1.722],[-1.722,-0.089],[0.63,-12.217],[13.068,-7.442],[1.527,0.802],[-0.288,0.548]],"v":[[42.238,8.912],[39.237,8.964],[6.49,11.701],[-12.571,38.468],[-15.851,41.426],[-18.808,38.146],[3.4,6.274],[42.142,3.436],[43.453,7.653]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.596078455448,0.321568638086,0.239215686917,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":6,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Thumb","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[225.438,108.028,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[42.783,10.613],[3.8,0.302],[0,0],[0.105,0.008],[0.484,0.019],[6.105,-2.438],[0,0],[0.032,-0.013],[0.85,-0.396],[0,0],[0,0],[0,0],[0,0],[-26.827,-45.427],[0,0],[0,0]],"o":[[-28.424,-53.851],[-3.608,-1.195],[0,0],[-0.106,-0.009],[-0.482,-0.035],[-6.685,-0.314],[0,0],[-0.032,0.013],[-0.853,0.343],[-20.172,9.126],[0,0],[-6.217,10.813],[0,0],[-5.276,37.379],[0,0],[0,0],[0,0]],"v":[[76.157,43.103],[-34.58,-89.69],[-45.717,-91.957],[-45.708,-91.966],[-46.022,-91.983],[-47.471,-92.063],[-66.474,-88.859],[-66.467,-88.866],[-66.56,-88.826],[-69.115,-87.715],[-94.124,-66.879],[-94.112,-66.885],[-101.514,-44.23],[-101.507,-44.234],[-53.003,92.116],[30.37,92.116],[102.173,92.116]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.678431391716,0.403921574354,0.305882364511,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".grey900","cl":"grey900","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[206,100,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[412,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":38,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.1254902035,0.129411771894,0.141176477075,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/res/raw/udfps_tip_hint_lottie.json b/res/raw/udfps_tip_hint_lottie.json
index 9e26dfe..1e45382 100644
--- a/res/raw/udfps_tip_hint_lottie.json
+++ b/res/raw/udfps_tip_hint_lottie.json
@@ -1 +1 @@
-{}
\ No newline at end of file
+{"v":"5.7.6","fr":60,"ip":0,"op":211,"w":412,"h":200,"nm":"FPS Tip","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".blue200","cl":"blue200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":30,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":55,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":85,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":110,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":115,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":140,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":165,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":170,"s":[100]},{"t":195,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[187.598,42.31,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[14.087,-6.682],[0,0]],"v":[[-13.068,2.034],[13.068,-1.085]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-22.142,12.611]],"o":[[0,0],[0,0]],"v":[[-49.515,34.761],[-23.485,7.239]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[-17.076,8.616],[-6.271,0.508]],"o":[[0,0],[7.731,-3.901],[0,0]],"v":[[-49.303,7.104],[-26.763,-9.607],[-5.197,-15.604]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.68235296011,0.796078443527,0.980392158031,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Fingerprint Tip","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[187.598,42.31,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[14.087,-6.682],[0,0]],"v":[[-13.068,2.034],[13.068,-1.085]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[-22.142,12.611]],"o":[[0,0],[0,0]],"v":[[-49.515,34.761],[-23.485,7.239]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[-17.076,8.616],[-6.271,0.508]],"o":[[0,0],[7.731,-3.901],[0,0]],"v":[[-49.303,7.104],[-26.763,-9.607],[-5.197,-15.604]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.596069335938,0.321563720703,0.239196777344,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Fingerprint","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[176.139,66.987,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.408,-0.232],[1.017,0.68],[-0.959,1.434],[-6.841,3.896],[-7.562,0.41],[-0.093,-1.722],[1.724,-0.094],[6.003,-3.419],[3.674,-5.493]],"o":[[-0.995,0.567],[-1.433,-0.959],[4.21,-6.294],[6.841,-3.896],[1.722,-0.093],[0.094,1.722],[-6.599,0.357],[-6.003,3.418],[-0.279,0.417]],"v":[[-16.693,13.1],[-19.976,12.983],[-20.836,8.652],[-3.944,-6.925],[18.072,-13.506],[21.359,-10.556],[18.409,-7.27],[-0.853,-1.498],[-15.645,12.124]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[7.044,-4.012],[6.96,0.908],[-0.223,1.71],[-1.711,-0.224],[-5.298,3.017],[-2.065,5.824],[-1.624,-0.577],[0.576,-1.626]],"o":[[-6.489,3.695],[-1.71,-0.223],[0.223,-1.71],[5.601,0.73],[5.742,-3.27],[0.577,-1.625],[1.625,0.576],[-2.566,7.236]],"v":[[44.597,78.169],[24.039,82.43],[21.346,78.93],[24.846,76.237],[41.507,72.742],[53.614,58.64],[57.601,56.74],[59.5,60.727]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0.323,-0.184],[1.029,0.844],[0.13,0.229],[-15.469,8.81],[-8.28,-14.539],[1.498,-0.853],[0.853,1.498],[12.426,-7.076],[-6.547,-11.496],[-4.085,-3.35],[1.094,-1.333]],"o":[[-1.091,0.621],[-5.004,-4.103],[-8.28,-14.539],[15.469,-8.81],[0.853,1.498],[-1.498,0.853],[-6.547,-11.496],[-12.426,7.077],[0.024,0.042],[1.333,1.093],[-0.25,0.305]],"v":[[10.147,71.146],[6.621,70.847],[-1.636,61.086],[11.185,19.448],[53.537,29.665],[52.369,33.924],[48.111,32.756],[14.276,24.874],[3.791,57.996],[10.581,66.018],[11.016,70.413]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[1.183,-0.674],[0.287,-0.071],[3.505,8.779],[-6.102,3.475],[-3.887,-4.766],[0,0],[-3.067,1.747],[1.97,3.459],[0,0],[-1.498,0.853],[-0.853,-1.498],[0,0],[6.451,-3.674],[4.175,5.108],[0,0],[2.675,-1.523],[-1.489,-3.73],[-5.283,1.305],[-0.414,-1.674]],"o":[[-0.244,0.139],[-9.188,2.268],[-2.649,-6.633],[5.335,-3.038],[0,0],[2.235,2.735],[3.459,-1.97],[0,0],[-0.853,-1.498],[1.498,-0.853],[0,0],[3.674,6.452],[-5.725,3.26],[0,0],[-1.949,-2.391],[-3.377,1.923],[2.674,6.697],[1.674,-0.413],[0.342,1.387]],"v":[[36.276,64.961],[35.477,65.279],[11.151,49.991],[16.667,33.848],[32.582,36.831],[36.686,41.876],[45.842,43.586],[48.542,33.739],[47.994,32.777],[49.163,28.518],[53.421,29.686],[53.969,30.648],[48.932,49.012],[31.846,45.823],[27.739,40.774],[19.758,39.275],[16.951,47.675],[33.98,59.216],[37.761,61.499]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ind":4,"ty":"sh","ix":5,"ks":{"a":0,"k":{"i":[[0.501,-0.285],[0.978,0.515],[11.066,-6.302],[0.514,-9.958],[1.721,0.088],[-0.089,1.722],[-13.068,7.442],[-10.828,-5.692],[0.802,-1.526]],"o":[[-0.895,0.51],[-8.827,-4.639],[-11.066,6.302],[-0.089,1.722],[-1.722,-0.089],[0.63,-12.217],[13.068,-7.442],[1.527,0.802],[-0.288,0.548]],"v":[[42.238,8.912],[39.237,8.964],[6.49,11.701],[-12.571,38.468],[-15.851,41.426],[-18.808,38.146],[3.4,6.274],[42.142,3.436],[43.453,7.653]],"c":true},"ix":2},"nm":"Path 5","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.596078455448,0.321568638086,0.239215686917,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":6,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Thumb","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[225.438,108.028,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[42.783,10.613],[3.8,0.302],[0,0],[0.105,0.008],[0.484,0.019],[6.105,-2.438],[0,0],[0.032,-0.013],[0.85,-0.396],[0,0],[0,0],[0,0],[0,0],[-26.827,-45.427],[0,0],[0,0]],"o":[[-28.424,-53.851],[-3.608,-1.195],[0,0],[-0.106,-0.009],[-0.482,-0.035],[-6.685,-0.314],[0,0],[-0.032,0.013],[-0.853,0.343],[-20.172,9.126],[0,0],[-6.217,10.813],[0,0],[-5.276,37.379],[0,0],[0,0],[0,0]],"v":[[76.157,43.103],[-34.58,-89.69],[-45.717,-91.957],[-45.708,-91.966],[-46.022,-91.983],[-47.471,-92.063],[-66.474,-88.859],[-66.467,-88.866],[-66.56,-88.826],[-69.115,-87.715],[-94.124,-66.879],[-94.112,-66.885],[-101.514,-44.23],[-101.507,-44.234],[-53.003,92.116],[30.37,92.116],[102.173,92.116]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.678431391716,0.403921574354,0.305882364511,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".grey900","cl":"grey900","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[206,100,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[412,200],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":38,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.1254902035,0.129411771894,0.141176477075,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":960,"st":0,"bm":0}],"markers":[]}
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5da429a..fa90d07 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1240,6 +1240,28 @@
     <string name="private_space_fingerprint_unlock_title">Fingerprint Unlock for private space</string>
     <!-- Title for the Face unlock for private space preference. [CHAR LIMIT=60] -->
     <string name="private_space_face_unlock_title">Face Unlock for private space</string>
+    <!-- Title for the Face and Fingerprint preference for private space. [CHAR LIMIT=60] -->
+    <string name="private_space_biometric_unlock_title">Face &amp; Fingerprint Unlock for private space</string>
+    <!-- Introduction title shown in private space fingerprint enrollment [CHAR LIMIT=90] -->
+    <string name="private_space_fingerprint_enroll_introduction_title">Set up Fingerprint Unlock for private space</string>
+    <!-- Introduction detail message shown in private space fingerprint enrollment dialog [CHAR LIMIT=NONE]-->
+    <string name ="private_space_fingerprint_enroll_introduction_message">Use your fingerprint to unlock your private space or verify it\u2019s you, like when you sign in to apps or approve a purchase</string>
+    <!-- Introduction description message shown in private space fingerprint enrollment introduction screen. [CHAR LIMIT=NONE] -->
+    <string name="private_space_fingerprint_enroll_introduction_footer_message">Your private space can be unlocked when you don\u2019t intend to, like if someone holds up your phone to your finger.</string>
+    <!-- Message shown in fingerprint enrollment dialog once enrollment is completed (private space) [CHAR LIMIT=NONE] -->
+    <string name="private_space_fingerprint_enroll_finish_message">Use your fingerprint to unlock your private space or to approve purchases</string>
+    <!-- Introduction title shown in private space face enrollment [CHAR LIMIT=90] -->
+    <string name="private_space_face_enroll_introduction_title">Set up Face Unlock for private space</string>
+    <!-- Introduction detail message shown in private space face enrollment dialog [CHAR LIMIT=NONE]-->
+    <string name ="private_space_face_enroll_introduction_message">Use your face to unlock your private space or verify it\u2019s you, like when you sign in to apps or approve a purchase</string>
+    <!-- Message on the face enrollment introduction page for private space that provides information about what could cause the phone to unlock. [CHAR LIMIT=NONE] -->
+    <string name="private_space_face_enroll_introduction_info_looking">Looking at the phone can unlock private space even when you don\u2019t intend to. Your private space can also be unlocked by someone who looks a lot like you, like an identical sibling, or if someone holds the device up to your face.</string>
+    <!-- Message on the face enrollment introduction page for private space that provides information about the relative security of face for unlocking the phone. [CHAR LIMIT=NONE] -->
+    <string name="private_space_face_enroll_introduction_info_less_secure">Using your face to unlock your private space may be less secure than a strong pattern, PIN, or password</string>
+    <!-- Text shown on the details of a toggle which disables/enables face unlock for private space, depending if the user's eyes are open. [CHAR LIMIT=NONE] -->
+    <string name="private_space_face_settings_require_attention_details">To unlock private space, your eyes must be open. For best results, take off sunglasses.</string>
+    <!-- Text shown in face settings in private space explaining what your face can be used for. [CHAR LIMIT=NONE] -->
+    <string name="private_space_face_settings_footer">Use your face to unlock your private space.\n\nKeep in mind:\nYou can only have one face set up at a time. To add another face, delete the current one.\n\nLooking at the phone can unlock it when you don\u2019t intend to.\n\nYour private space can be unlocked by someone else if your device is held up to your face.\n\nYour private space can be unlocked by someone who looks a lot like you, like an identical sibling.</string>
     <!-- Biometric category title - biometric options for unlocking the device. [CHAR LIMIT=50] -->
     <string name="private_space_category_ways_to_unlock">Ways to unlock</string>
     <!-- Summary for one lock when device screen lock is used as private profile lock. [CHAR LIMIT=40] -->
@@ -3408,6 +3430,8 @@
     <string name="error_name_empty">The Name field can\u2019t be empty.</string>
     <!-- APN error dialog messages: -->
     <string name="error_apn_empty">The APN can\u2019t be empty.</string>
+    <!-- APN error message about APN type cannot be empty -->
+    <string name="error_apn_type_empty">The APN type can\u2019t be empty.</string>
     <!-- APN error dialog messages: -->
     <string name="error_mcc_not3">MCC field must be 3 digits.</string>
     <!-- APN error dialog messages: -->
@@ -7948,8 +7972,55 @@
     <!--  Do not disturb: Subtitle for the Visual signals option to toggle on/off visual signals/alerts when the screen is on/when screen is off. [CHAR LIMIT=30] -->
     <string name="zen_mode_visual_signals_settings_subtitle">Allow visual signals</string>
 
+    <!-- Do not disturb: mode page section title [CHAR LIMIT=80] -->
+    <string name="mode_interruption_filter_title">Notifications that can reach you</string>
+    <!-- Do not disturb: mode page section title [CHAR LIMIT=80] -->
+    <string name="mode_device_effects_title">Additional actions</string>
+
+
+    <!-- Do not disturb: display settings title [CHAR LIMIT=80] -->
+    <string name="mode_display_settings_title">Display settings</string>
+    <!-- Do not disturb: display options section header [CHAR LIMIT=80] -->
+    <string name="mode_display_options_section">Display options</string>
+    <!-- Do not disturb: device effect option, title or first in list [CHAR LIMIT=80] -->
+    <string name="mode_grayscale_title">Grayscale</string>
+    <!-- Do not disturb: device effect option, not first in list [CHAR LIMIT=80] -->
+    <string name="mode_grayscale_title_secondary_list">grayscale</string>
+    <!-- Do not disturb: device effect summary [CHAR LIMIT=NONE] -->
+    <string name="mode_grayscale_summary">Change the screen to black and white</string>
+    <!-- Do not disturb: device effect option, title or first in list [CHAR LIMIT=80] -->
+    <string name="mode_aod_title">Keep the screen dark</string>
+    <!-- Do not disturb: device effect option, not first in list [CHAR LIMIT=80] -->
+    <string name="mode_aod_title_secondary_list">keep the screen dark</string>
+    <!-- Do not disturb: device effect summary [CHAR LIMIT=NONE] -->
+    <string name="mode_aod_summary">Disable always on display</string>
+    <!-- Do not disturb: device effect option , title or first in list[CHAR LIMIT=80] -->
+    <string name="mode_wallpaper_title">Dim the wallpaper</string>
+    <!-- Do not disturb: device effect option, not first in list [CHAR LIMIT=80] -->
+    <string name="mode_wallpaper_title_secondary_list">dim the wallpaper</string>
+    <!-- Do not disturb: device effect summary [CHAR LIMIT=NONE] -->
+    <string name="mode_wallpaper_summary">Filter the brightness of the wallpaper</string>
+    <!-- Do not disturb: device effect option, title or first in list [CHAR LIMIT=80] -->
+    <string name="mode_dark_theme_title">Enable dark theme</string>
+    <!-- Do not disturb: device effect option, not first in list [CHAR LIMIT=80] -->
+    <string name="mode_dark_theme_title_secondary_list">enable dark theme</string>
+    <!-- Do not disturb: device effect summary [CHAR LIMIT=NONE] -->
+    <string name="mode_dark_theme_summary">Switch the OS and apps to prefer light text on a dark
+        background, which may be easier on the eyes and confers significant battery savings on some devices</string>
+    <!-- [CHAR LIMIT=NONE] Zen mode settings: Summary for sound interruption settings -->
+    <string name="mode_display_settings_summary">
+        {count, plural, offset:2
+        =0    {No display changes}
+        =1    {{effect_1}}
+        =2    {{effect_1} and {effect_2}}
+        =3    {{effect_1}, {effect_2}, and {effect_3}}
+        other {{effect_1}, {effect_2}, and # more}
+        }
+    </string>
+
     <!-- Do not disturb: restrict notifications settings title [CHAR LIMIT=80] -->
-    <string name="zen_mode_restrict_notifications_title">Display options for hidden notifications</string>
+    <string name="zen_mode_restrict_notifications_title">Display options for filtered
+        notifications</string>
     <!-- Do not disturb: Hide notifications screen category title [CHAR LIMIT=100] -->
     <string name="zen_mode_restrict_notifications_category">When Do Not Disturb is on</string>
     <!-- Do not disturb: Mute notifications option [CHAR LIMIT=60] -->
@@ -7971,11 +8042,12 @@
     <!-- Do not disturb: restrict notifications page, menu option [CHAR LIMIT=60] -->
     <string name="zen_mode_restrict_notifications_disable_custom">Remove custom setting</string>
     <!-- Do not disturb: restrict notifications page, menu option [CHAR LIMIT=60] -->
-    <string name="zen_mode_restrict_notifications_summary_muted">No sound from notifications</string>
+    <string name="zen_mode_restrict_notifications_summary_muted">Notifications shown</string>
     <!-- Do not disturb: restrict notifications page, menu option [CHAR LIMIT=60] -->
-    <string name="zen_mode_restrict_notifications_summary_custom">Partially hidden</string>
+    <string name="zen_mode_restrict_notifications_summary_custom">Notifications partially
+        hidden</string>
     <!-- Do not disturb: restrict notifications page, menu option [CHAR LIMIT=100] -->
-    <string name="zen_mode_restrict_notifications_summary_hidden">No visuals or sound from notifications</string>
+    <string name="zen_mode_restrict_notifications_summary_hidden">Notifications hidden</string>
 
     <!-- Do not disturb: what to block title [CHAR LIMIT = 60] -->
     <string name="zen_mode_what_to_block_title">Custom restrictions</string>
@@ -11358,7 +11430,7 @@
     <string name="media_output_default_summary">This device</string>
 
     <!-- Summary for media output when audio sharing. [CHAR LIMIT=NONE] -->
-    <string name="media_output_audio_sharing">Audio sharing</string>
+    <string name="media_output_audio_sharing">Sharing audio</string>
 
     <!-- Summary for media output settings when device is in ongoing call state. -->
     <string name="media_out_summary_ongoing_call_state">Unavailable during calls</string>
@@ -11848,6 +11920,10 @@
     <string name="sim_onboarding_progressbar_turning_sim_on">Turning on <xliff:g id="carrier_name" example="Google Fi">%1$s</xliff:g>&#8230;</string>
     <!-- Title of service provider name(SPN) at mobile network settings page. [CHAR LIMIT=30] -->
     <string name="mobile_network_spn_title">Mobile network</string>
+    <!-- At the mobile network page, title for primary IMEI for multi-sim devices -->
+    <string name="imei_primary">IMEI (primary)</string>
+    <!-- At the mobile network page, title for primary MEID for multi-sim devices -->
+    <string name="meid_primary">MEID (primary)</string>
     <!-- Title of phone number at mobile network settings page. [CHAR LIMIT=30] -->
     <string name="mobile_network_phone_number_title">Phone number</string>
     <!-- Title of SIM label and color editor dialog at mobile network settings page. [CHAR LIMIT=30] -->
diff --git a/res/xml/bluetooth_le_audio_sharing.xml b/res/xml/bluetooth_le_audio_sharing.xml
index 8ba6c07..15c38b3 100644
--- a/res/xml/bluetooth_le_audio_sharing.xml
+++ b/res/xml/bluetooth_le_audio_sharing.xml
@@ -34,7 +34,7 @@
         android:key="calls_and_alarms"
         android:summary=""
         android:title="@string/audio_sharing_call_audio_title"
-        settings:controller="com.android.settings.connecteddevice.audiosharing.CallsAndAlarmsPreferenceController" />
+        settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingCallAudioPreferenceController" />
 
     <Preference
         android:icon="@drawable/ic_audio_play_sample"
@@ -68,14 +68,14 @@
 
     <PreferenceCategory
         android:key="audio_streams_settings_category"
-        android:title="@string/audio_streams_category_title"
+        android:title="@string/audio_sharing_nearby_audio_title"
         settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController">
 
         <Preference
             android:fragment="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsDashboardFragment"
             android:icon="@drawable/ic_chevron_right_24dp"
             android:key="audio_streams_settings"
-            android:title="@string/audio_streams_pref_title" />
+            android:title="@string/audio_streams_main_page_title" />
 
     </PreferenceCategory>
 </PreferenceScreen>
\ No newline at end of file
diff --git a/res/xml/modes_display_settings.xml b/res/xml/modes_display_settings.xml
new file mode 100644
index 0000000..53268ec
--- /dev/null
+++ b/res/xml/modes_display_settings.xml
@@ -0,0 +1,45 @@
+<?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/mode_display_settings_title">
+
+    <PreferenceCategory
+        android:key="display_options"
+        android:title="@string/mode_display_options_section">
+        <Preference
+                android:key="notification_visibility"
+                android:title="@string/zen_mode_restrict_notifications_title" />
+        <SwitchPreferenceCompat
+                android:key="effect_greyscale"
+                android:title="@string/mode_grayscale_title"
+                android:summary="@string/mode_grayscale_summary"/>
+        <SwitchPreferenceCompat
+                android:key="effect_aod"
+                android:title="@string/mode_aod_title"
+                android:summary="@string/mode_aod_summary"/>
+        <SwitchPreferenceCompat
+                android:key="effect_wallpaper"
+                android:title="@string/mode_wallpaper_title"
+                android:summary="@string/mode_wallpaper_summary"/>
+        <SwitchPreferenceCompat
+                android:key="effect_dark_theme"
+                android:title="@string/mode_dark_theme_title"
+                android:summary="@string/mode_dark_theme_summary"/>
+    </PreferenceCategory>
+</PreferenceScreen>
diff --git a/res/xml/modes_notif_vis_settings.xml b/res/xml/modes_notif_vis_settings.xml
new file mode 100644
index 0000000..551c704
--- /dev/null
+++ b/res/xml/modes_notif_vis_settings.xml
@@ -0,0 +1,59 @@
+<?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:key="zen_mode_block_settings_page"
+    android:title="@string/zen_mode_restrict_notifications_title">
+
+   <PreferenceCategory
+       android:title="@string/zen_mode_block_effects_screen_off"
+       android:key="zen_mode_block_screen_off">
+
+       <com.android.settings.widget.DisabledCheckBoxPreference
+           android:key="zen_effect_intent"
+           android:title="@string/zen_mode_block_effect_intent" />
+
+       <com.android.settings.widget.DisabledCheckBoxPreference
+           android:key="zen_effect_light"
+           android:title="@string/zen_mode_block_effect_light" />
+
+       <com.android.settings.widget.DisabledCheckBoxPreference
+           android:key="zen_effect_ambient"
+           android:title="@string/zen_mode_block_effect_ambient" />
+
+   </PreferenceCategory>
+    <PreferenceCategory
+        android:title="@string/zen_mode_block_effects_screen_on"
+        android:key="zen_mode_block_screen_on">
+       <com.android.settings.widget.DisabledCheckBoxPreference
+           android:key="zen_effect_badge"
+           android:title="@string/zen_mode_block_effect_badge" />
+
+        <com.android.settings.widget.DisabledCheckBoxPreference
+            android:key="zen_effect_status"
+            android:title="@string/zen_mode_block_effect_status" />
+
+        <com.android.settings.widget.DisabledCheckBoxPreference
+            android:key="zen_effect_peek"
+            android:title="@string/zen_mode_block_effect_peek" />
+
+       <com.android.settings.widget.DisabledCheckBoxPreference
+           android:key="zen_effect_list"
+           android:title="@string/zen_mode_block_effect_list" />
+   </PreferenceCategory>
+</PreferenceScreen>
diff --git a/res/xml/modes_rule_settings.xml b/res/xml/modes_rule_settings.xml
index 7a08a68..a380987 100644
--- a/res/xml/modes_rule_settings.xml
+++ b/res/xml/modes_rule_settings.xml
@@ -22,12 +22,23 @@
             android:key="header"
             android:layout="@layout/settings_entity_header" />
 
-    <Preference
-            android:key="zen_mode_people"
-            android:title="@string/zen_category_people"/>
+    <PreferenceCategory
+            android:title="@string/mode_interruption_filter_title"
+            android:key="modes_filters">
+        <Preference
+                android:key="zen_mode_people"
+                android:title="@string/zen_category_people"/>
 
-    <Preference
-            android:key="zen_other_settings"
-            android:title="@string/zen_category_exceptions" />
+        <Preference
+                android:key="zen_other_settings"
+                android:title="@string/zen_category_exceptions" />
+    </PreferenceCategory>
 
+    <PreferenceCategory
+            android:title="@string/mode_device_effects_title"
+            android:key="modes_additional_actions">
+        <Preference
+                android:key="mode_display_settings"
+                android:title="@string/mode_display_settings_title" />
+    </PreferenceCategory>
 </PreferenceScreen>
\ No newline at end of file
diff --git a/res/xml/private_space_biometric_settings.xml b/res/xml/private_space_biometric_settings.xml
index 6135b5f..38ec09a 100644
--- a/res/xml/private_space_biometric_settings.xml
+++ b/res/xml/private_space_biometric_settings.xml
@@ -17,7 +17,7 @@
 <PreferenceScreen
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:settings="http://schemas.android.com/apk/res-auto"
-    android:title="@string/private_space_biometric_title"
+    android:title="@string/private_space_biometric_unlock_title"
     settings:searchable="false">
 
     <com.android.settingslib.widget.TopIntroPreference
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index 9ebc124..3367bf1 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -47,6 +47,7 @@
 
     public static class MemtagPageActivity extends SettingsActivity { /* empty */}
     public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }
+    public static class BluetoothDashboardActivity extends SettingsActivity { /* empty */ }
     public static class CreateShortcutActivity extends SettingsActivity { /* empty */ }
     public static class FaceSettingsActivity extends SettingsActivity { /* empty */ }
     /** Container for {@link FaceSettings} to use with a pre-defined task affinity. */
diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java
index f76ea27..145f3a4 100644
--- a/src/com/android/settings/Utils.java
+++ b/src/com/android/settings/Utils.java
@@ -1401,6 +1401,16 @@
     }
 
     /**
+     * Returns true if the userId is a private profile, false otherwise.
+     */
+    public static boolean isPrivateProfile(int userId, @NonNull Context context) {
+        final UserManager userManager = context.getSystemService(UserManager.class);
+        UserInfo userInfo = userManager.getUserInfo(userId);
+        return Flags.allowPrivateProfile() && android.multiuser.Flags.enablePrivateSpaceFeatures()
+                && userInfo.isPrivateProfile();
+    }
+
+    /**
      * Enable new edge to edge feature.
      *
      * @param activity the Activity need to setup the edge to edge feature.
diff --git a/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java b/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java
index 53de7c5..1d80099 100644
--- a/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java
+++ b/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java
@@ -559,7 +559,8 @@
         if (android.app.Flags.appRestrictionsApi()) {
             am.noteAppRestrictionEnabled(pkgName, mAppEntry.info.uid,
                     ActivityManager.RESTRICTION_LEVEL_FORCE_STOPPED, true,
-                    ActivityManager.RESTRICTION_REASON_USER, "settings", 0L);
+                    ActivityManager.RESTRICTION_REASON_USER,
+                    "settings", ActivityManager.RESTRICTION_SOURCE_USER, 0L);
         }
         am.forceStopPackage(pkgName);
         int userId = UserHandle.getUserId(mAppEntry.info.uid);
diff --git a/src/com/android/settings/biometrics/combination/CombinedBiometricStatusUtils.java b/src/com/android/settings/biometrics/combination/CombinedBiometricStatusUtils.java
index 2cd239e..92b6d60 100644
--- a/src/com/android/settings/biometrics/combination/CombinedBiometricStatusUtils.java
+++ b/src/com/android/settings/biometrics/combination/CombinedBiometricStatusUtils.java
@@ -93,7 +93,10 @@
     public String getTitle() {
         UserManager userManager = mContext.getSystemService(UserManager.class);
         if (userManager != null && userManager.isProfile()) {
-            return mContext.getString(R.string.security_settings_work_biometric_preference_title);
+            return mContext.getString(
+                    Utils.isPrivateProfile(mUserId, mContext)
+                            ? R.string.private_space_biometric_unlock_title
+                            : R.string.security_settings_work_biometric_preference_title);
         } else {
             return mContext.getString(R.string.security_settings_biometric_preference_title);
         }
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
index 414c545..4888388 100644
--- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
@@ -365,7 +365,9 @@
 
     @StringRes
     protected int getInfoMessageLooking() {
-        return R.string.security_settings_face_enroll_introduction_info_looking;
+        return isPrivateProfile()
+                ? R.string.private_space_face_enroll_introduction_info_looking
+                : R.string.security_settings_face_enroll_introduction_info_looking;
     }
 
     @StringRes
@@ -390,7 +392,10 @@
 
     @StringRes
     protected int getLessSecureMessage() {
-        return R.string.security_settings_face_enroll_introduction_info_less_secure;
+        return isPrivateProfile()
+                ? R.string.private_space_face_enroll_introduction_info_less_secure
+                : R.string.security_settings_face_enroll_introduction_info_less_secure;
+
     }
 
     @Override
@@ -411,6 +416,9 @@
 
     @Override
     protected int getHeaderResDefault() {
+        if (isPrivateProfile()) {
+            return R.string.private_space_face_enroll_introduction_title;
+        }
         return R.string.security_settings_face_enroll_introduction_title;
     }
 
@@ -577,7 +585,10 @@
 
     @Override
     protected void updateDescriptionText() {
-        if (mIsFaceStrong) {
+        if (isPrivateProfile()) {
+            setDescriptionText(getString(
+                    R.string.private_space_face_enroll_introduction_message));
+        } else if (mIsFaceStrong) {
             setDescriptionText(getString(
                     R.string.security_settings_face_enroll_introduction_message_class3));
         }
@@ -608,4 +619,8 @@
         }
         updateDescriptionText();
     }
+
+    private boolean isPrivateProfile() {
+        return Utils.isPrivateProfile(mUserId, getApplicationContext());
+    }
 }
diff --git a/src/com/android/settings/biometrics/face/FaceSettings.java b/src/com/android/settings/biometrics/face/FaceSettings.java
index 24df51f..061487a 100644
--- a/src/com/android/settings/biometrics/face/FaceSettings.java
+++ b/src/com/android/settings/biometrics/face/FaceSettings.java
@@ -211,6 +211,8 @@
                 ((FaceSettingsPreferenceController) controller).setUserId(mUserId);
             } else if (controller instanceof FaceSettingsEnrollButtonPreferenceController) {
                 ((FaceSettingsEnrollButtonPreferenceController) controller).setUserId(mUserId);
+            } else if (controller instanceof FaceSettingsFooterPreferenceController) {
+                ((FaceSettingsFooterPreferenceController) controller).setUserId(mUserId);
             }
         }
         mRemoveController.setUserId(mUserId);
@@ -367,6 +369,7 @@
         controllers.add(new FaceSettingsRemoveButtonPreferenceController(context));
         controllers.add(new FaceSettingsConfirmPreferenceController(context));
         controllers.add(new FaceSettingsEnrollButtonPreferenceController(context));
+        controllers.add(new FaceSettingsFooterPreferenceController(context));
         return controllers;
     }
 
diff --git a/src/com/android/settings/biometrics/face/FaceSettingsAttentionPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsAttentionPreferenceController.java
index 77a160d..71c4678 100644
--- a/src/com/android/settings/biometrics/face/FaceSettingsAttentionPreferenceController.java
+++ b/src/com/android/settings/biometrics/face/FaceSettingsAttentionPreferenceController.java
@@ -24,10 +24,12 @@
 import android.hardware.face.FaceManager.SetFeatureCallback;
 import android.provider.Settings;
 
+import androidx.annotation.Nullable;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceScreen;
 import androidx.preference.TwoStatePreference;
 
+import com.android.settings.R;
 import com.android.settings.Utils;
 
 /**
@@ -99,6 +101,18 @@
     }
 
     @Override
+    public void updateState(@Nullable Preference preference) {
+        if (preference == null) {
+            return;
+        }
+        super.updateState(preference);
+        if (Utils.isPrivateProfile(getUserId(), mContext)) {
+            preference.setSummary(mContext.getString(
+                    R.string.private_space_face_settings_require_attention_details));
+        }
+    }
+
+    @Override
     public boolean isChecked() {
         if (!FaceSettings.isFaceHardwareDetected(mContext)) {
             return true;
diff --git a/src/com/android/settings/biometrics/face/FaceSettingsFooterPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsFooterPreferenceController.java
index e2123f0..d56c9b3 100644
--- a/src/com/android/settings/biometrics/face/FaceSettingsFooterPreferenceController.java
+++ b/src/com/android/settings/biometrics/face/FaceSettingsFooterPreferenceController.java
@@ -30,6 +30,7 @@
 import androidx.preference.PreferenceScreen;
 
 import com.android.settings.R;
+import com.android.settings.Utils;
 import com.android.settings.core.BasePreferenceController;
 import com.android.settings.overlay.FeatureFactory;
 import com.android.settings.utils.AnnotationSpan;
@@ -41,12 +42,17 @@
  * Footer for face settings showing the help text and help link.
  */
 public class FaceSettingsFooterPreferenceController extends BasePreferenceController {
+    private static final String KEY = "security_face_footer";
     private static final String TAG = "FaceSettingsFooterPreferenceController";
     private static final String ANNOTATION_URL = "url";
     private final FaceFeatureProvider mProvider;
     private Preference mPreference;
     private boolean mIsFaceStrong;
+    private int mUserId;
 
+    public FaceSettingsFooterPreferenceController(@NonNull Context context) {
+        this(context, KEY);
+    }
     public FaceSettingsFooterPreferenceController(Context context, String preferenceKey) {
         super(context, preferenceKey);
         mProvider = FeatureFactory.getFeatureFactory().getFaceFeatureProvider();
@@ -79,7 +85,9 @@
 
         int footerRes;
         boolean isAttentionSupported = mProvider.isAttentionSupported(mContext);
-        if (mIsFaceStrong) {
+        if (Utils.isPrivateProfile(mUserId, mContext)) {
+            footerRes = R.string.private_space_face_settings_footer;
+        } else if (mIsFaceStrong) {
             footerRes = isAttentionSupported
                     ? R.string.security_settings_face_settings_footer_class3
                     : R.string.security_settings_face_settings_footer_attention_not_supported;
@@ -92,6 +100,10 @@
                 mContext.getText(footerRes), linkInfo));
     }
 
+    public void setUserId(int userId) {
+        mUserId = userId;
+    }
+
     private void addAuthenticatorsRegisteredCallback(Context context) {
         final FaceManager faceManager = context.getSystemService(FaceManager.class);
         faceManager.addAuthenticatorsRegisteredCallback(
diff --git a/src/com/android/settings/biometrics/face/FaceStatusUtils.java b/src/com/android/settings/biometrics/face/FaceStatusUtils.java
index d02e115..5af0a8a 100644
--- a/src/com/android/settings/biometrics/face/FaceStatusUtils.java
+++ b/src/com/android/settings/biometrics/face/FaceStatusUtils.java
@@ -67,7 +67,10 @@
     public String getTitle() {
         UserManager userManager = mContext.getSystemService(UserManager.class);
         if (userManager != null && userManager.isProfile()) {
-            return mContext.getString(R.string.security_settings_face_profile_preference_title);
+            return mContext.getString(
+                    Utils.isPrivateProfile(mUserId, mContext)
+                            ? R.string.private_space_face_unlock_title
+                            : R.string.security_settings_face_profile_preference_title);
         } else {
             return mContext.getString(R.string.security_settings_face_preference_title);
         }
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java
index 9c89f24..013e3d7 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java
@@ -78,7 +78,10 @@
             setContentView(R.layout.fingerprint_enroll_finish);
         }
         setHeaderText(R.string.security_settings_fingerprint_enroll_finish_title);
-        setDescriptionText(R.string.security_settings_fingerprint_enroll_finish_v2_message);
+        setDescriptionText(Utils.isPrivateProfile(mUserId, getApplicationContext())
+                ? R.string.private_space_fingerprint_enroll_finish_message
+                : R.string.security_settings_fingerprint_enroll_finish_v2_message);
+
         final String sfpsDescription = mSfpsRestToUnlockFeature != null
                 ? mSfpsRestToUnlockFeature.getDescriptionForSfps(this)
                 : null;
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
index a1f4eff..f6626b2 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollIntroduction.java
@@ -176,7 +176,9 @@
     @Override
     protected void initViews() {
         setDescriptionText(getString(
-                R.string.security_settings_fingerprint_enroll_introduction_v3_message,
+                isPrivateProfile()
+                        ? R.string.private_space_fingerprint_enroll_introduction_message
+                        : R.string.security_settings_fingerprint_enroll_introduction_v3_message,
                 DeviceHelper.getDeviceName(this)));
 
         super.initViews();
@@ -261,6 +263,9 @@
 
     @StringRes
     protected int getFooterMessage5() {
+        if (isPrivateProfile()) {
+            return R.string.private_space_fingerprint_enroll_introduction_footer_message;
+        }
         return R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_5;
     }
 
@@ -292,6 +297,9 @@
 
     @Override
     protected int getHeaderResDefault() {
+        if (isPrivateProfile()) {
+            return R.string.private_space_fingerprint_enroll_introduction_title;
+        }
         return R.string.security_settings_fingerprint_enroll_introduction_title;
     }
 
@@ -477,4 +485,8 @@
         data.putExtra(MultiBiometricEnrollHelper.EXTRA_SKIP_PENDING_ENROLL, true);
         return data;
     }
+
+    private boolean isPrivateProfile() {
+        return Utils.isPrivateProfile(mUserId, getApplicationContext());
+    }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
index 63499a5..7651176 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
@@ -510,8 +510,9 @@
                 mFooterColumns.add(column2);
             } else {
                 final FooterColumn column = new FooterColumn();
-                column.mTitle = getString(
-                        R.string.security_settings_fingerprint_enroll_introduction_v3_message,
+                column.mTitle = getString(isPrivateProfile()
+                        ? R.string.private_space_fingerprint_enroll_introduction_message
+                        : R.string.security_settings_fingerprint_enroll_introduction_v3_message,
                         DeviceHelper.getDeviceName(getActivity()));
                 column.mLearnMoreClickListener = learnMoreClickListener;
                 column.mLearnMoreOverrideText = getText(
@@ -1130,6 +1131,10 @@
             }
         };
 
+        private boolean isPrivateProfile() {
+            return Utils.isPrivateProfile(mUserId, getContext());
+        }
+
         public static class DeleteFingerprintDialog extends InstrumentedDialogFragment
                 implements DialogInterface.OnClickListener {
 
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtils.java b/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtils.java
index d6e6498..a7c9e9d 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtils.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintStatusUtils.java
@@ -68,7 +68,10 @@
     public String getTitle() {
         UserManager userManager = mContext.getSystemService(UserManager.class);
         if (userManager != null && userManager.isProfile()) {
-            return mContext.getString(R.string.security_settings_work_fingerprint_preference_title);
+            return mContext.getString(
+                    Utils.isPrivateProfile(mUserId, mContext)
+                            ? R.string.private_space_fingerprint_unlock_title
+                            : R.string.security_settings_work_fingerprint_preference_title);
         } else {
             return mContext.getString(R.string.security_settings_fingerprint_preference_title);
         }
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java
index 8d1113e..f292aae 100644
--- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java
@@ -184,6 +184,7 @@
      */
     public void onAcquired(boolean isAcquiredGood) {
         if (mListener != null) {
+            Log.e("JRM", "OnaCquired " + isAcquiredGood + " lastStepIsGood" + animateIfLastStep());
             mListener.onAcquired(isAcquiredGood && animateIfLastStep());
         }
     }
diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/DebuggingRepository.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/DebuggingRepository.kt
index e99f85b..d007e1a 100644
--- a/src/com/android/settings/biometrics/fingerprint2/data/repository/DebuggingRepository.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/DebuggingRepository.kt
@@ -34,7 +34,7 @@
    */
   private val isBuildDebuggable = Build.IS_DEBUGGABLE
   /** This flag indicates if udfps should use debug repos to supply data to its various views. */
-  private val udfpsEnrollmentDebugEnabled = true
+  private val udfpsEnrollmentDebugEnabled = false
 
   override fun isDebuggingEnabled(): Boolean {
     return isBuildDebuggable
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt
index e683cb8..32a1763 100644
--- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/EnrollStageInteractor.kt
@@ -34,10 +34,10 @@
     flowOf(
       mapOf(
         0.0f to EnrollStageModel.Center,
-        0.25f to EnrollStageModel.Guided,
-        0.5f to EnrollStageModel.Fingertip,
-        0.75f to EnrollStageModel.LeftEdge,
-        0.875f to EnrollStageModel.RightEdge,
+        0.065f to EnrollStageModel.Guided,
+        0.48f to EnrollStageModel.Fingertip,
+        0.584f to EnrollStageModel.LeftEdge,
+        0.792f to EnrollStageModel.RightEdge,
       )
     )
 }
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintEnrollInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintEnrollInteractor.kt
index e4180d3..f967e04 100644
--- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintEnrollInteractor.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintEnrollInteractor.kt
@@ -33,6 +33,7 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.transform
 import kotlinx.coroutines.flow.update
 
 /** This repository is responsible for collecting all state related to the enroll API. */
@@ -106,6 +107,30 @@
           streamEnded = true
           enrollRequestOutstanding.update { false }
         }
+
+        override fun onUdfpsPointerDown(sensorId: Int) {
+          trySend(FingerEnrollState.PointerDown(sensorId)).onFailure { error ->
+            Log.d(TAG, "onUdfpsPointerDown failed to send, due to $error")
+          }
+        }
+
+        override fun onUdfpsPointerUp(sensorId: Int) {
+          trySend(FingerEnrollState.PointerUp(sensorId)).onFailure { error ->
+            Log.d(TAG, "onUdfpsPointerUp failed to send, due to $error")
+          }
+        }
+
+        override fun onUdfpsOverlayShown() {
+          trySend(FingerEnrollState.OverlayShown).onFailure { error ->
+            Log.d(TAG, "OverlayShown failed to send, due to $error")
+          }
+        }
+
+        override fun onAcquired(isAcquiredGood: Boolean) {
+          trySend(FingerEnrollState.Acquired(isAcquiredGood)).onFailure { error ->
+            Log.d(TAG, "Acquired failed to send, due to $error")
+          }
+        }
       }
 
     val cancellationSignal = CancellationSignal()
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt
index f9276e6..3ecf312 100644
--- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/OrientationInteractor.kt
@@ -17,17 +17,12 @@
 package com.android.settings.biometrics.fingerprint2.domain.interactor
 
 import android.content.Context
-import android.util.Log
 import android.view.OrientationEventListener
 import com.android.internal.R
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
-import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.flow.transform
 
 /** Interactor which provides information about orientation */
@@ -35,7 +30,9 @@
   /** A flow that contains the information about the orientation changing */
   val orientation: Flow<Int>
   /**
-   * A flow that contains the rotation info
+   * This indicates the surface rotation that hte view is currently in. For instance its possible to
+   * rotate a view to 90 degrees but for it to still be portrait mode. In this case, this flow
+   * should emit that we are in rotation 0 (SurfaceView.Rotation_0)
    */
   val rotation: Flow<Int>
   /**
@@ -50,8 +47,7 @@
   fun getRotationFromDefault(rotation: Int): Int
 }
 
-class OrientationInteractorImpl(private val context: Context, activityScope: CoroutineScope) :
-  OrientationInteractor {
+class OrientationInteractorImpl(private val context: Context) : OrientationInteractor {
 
   override val orientation: Flow<Int> = callbackFlow {
     val orientationEventListener =
@@ -62,9 +58,12 @@
       }
     orientationEventListener.enable()
     awaitClose { orientationEventListener.disable() }
-  }.shareIn(activityScope, SharingStarted.Eagerly, replay = 1)
+  }
 
-  override val rotation: Flow<Int> = orientation.transform { emit(context.display!!.rotation) }
+  override val rotation: Flow<Int> =
+    orientation.transform {
+      emit(context.display!!.rotation)
+    }
 
   override val rotationFromDefault: Flow<Int> = rotation.map { getRotationFromDefault(it) }
 
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UdfpsEnrollInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UdfpsEnrollInteractor.kt
index ec09ffd..107d1f6 100644
--- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UdfpsEnrollInteractor.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/UdfpsEnrollInteractor.kt
@@ -17,6 +17,7 @@
 package com.android.settings.biometrics.fingerprint2.domain.interactor
 
 import android.graphics.PointF
+import android.util.Log
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
@@ -69,6 +70,7 @@
 
   override fun onEnrollmentStep(stepsRemaining: Int, totalStep: Int) {
     val index = (totalStep - stepsRemaining) % guidedEnrollmentPoints.size
+    Log.e("JRM", "guided enroll step $index")
     _guidedEnrollment.update { guidedEnrollmentPoints[index] }
   }
 
diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/model/FingerEnrollState.kt b/src/com/android/settings/biometrics/fingerprint2/lib/model/FingerEnrollState.kt
index 24a9a86..e087304 100644
--- a/src/com/android/settings/biometrics/fingerprint2/lib/model/FingerEnrollState.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/lib/model/FingerEnrollState.kt
@@ -47,10 +47,10 @@
   data class Acquired(val acquiredGood: Boolean) : FingerEnrollState()
 
   /** Indicates a pointer down event has occurred */
-  data object PointerDown : FingerEnrollState()
+  data class PointerDown(val fingerId: Int) : FingerEnrollState()
 
   /** Indicates a pointer up event has occurred */
-  data object PointerUp : FingerEnrollState()
+  data class PointerUp(val fingerId: Int) : FingerEnrollState()
 
   /** Indicates the overlay has shown */
   data object OverlayShown : FingerEnrollState()
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt
index 6d353a4..d25fcd0 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt
@@ -72,6 +72,7 @@
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.fragment.RFPSEnrollFragment
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSViewModel
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.fragment.UdfpsEnrollFragment
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsLastStepViewModel
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsViewModel
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintAction
@@ -126,6 +127,7 @@
   private lateinit var fingerprintFlowViewModel: FingerprintFlowViewModel
   private lateinit var fingerprintEnrollConfirmationViewModel:
     FingerprintEnrollConfirmationViewModel
+  private lateinit var udfpsLastStepViewModel: UdfpsLastStepViewModel
   private lateinit var udfpsViewModel: UdfpsViewModel
   private lateinit var enrollStageInteractor: EnrollStageInteractor
   private val coroutineDispatcher = Dispatchers.Default
@@ -320,7 +322,7 @@
     foldStateInteractor = FoldStateInteractorImpl(context)
     foldStateInteractor.onConfigurationChange(resources.configuration)
 
-    orientationInteractor = OrientationInteractorImpl(context, lifecycleScope)
+    orientationInteractor = OrientationInteractorImpl(context)
     vibrationInteractor =
       VibrationInteractorImpl(context.getSystemService(Vibrator::class.java)!!, context)
 
@@ -373,11 +375,15 @@
         fingerprintEnrollEnrollingViewModel,
         navigationViewModel,
         orientationInteractor,
+        fingerprintManagerInteractor,
       ),
     )[RFPSViewModel::class.java]
 
     enrollStageInteractor = EnrollStageInteractorImpl()
 
+    udfpsLastStepViewModel =
+      UdfpsLastStepViewModel(fingerprintEnrollEnrollingViewModel, vibrationInteractor)
+
     udfpsViewModel =
       ViewModelProvider(
         this,
@@ -393,6 +399,9 @@
           backgroundViewModel,
           fingerprintSensorRepo,
           udfpsEnrollInteractor,
+          fingerprintManagerInteractor,
+          udfpsLastStepViewModel,
+          accessibilityInteractor,
         ),
       )[UdfpsViewModel::class.java]
 
@@ -456,7 +465,7 @@
                 step.exitTransition.toAnimation(),
               )
               .setReorderingAllowed(true)
-              .add(R.id.fragment_container_view, theClass::class.java, null)
+              .replace(R.id.fragment_container_view, theClass::class.java, null)
               .commit()
             navigationViewModel.update(
               FingerprintAction.TRANSITION_FINISHED,
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt
index 0645081..8abcf1a 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/viewmodel/RFPSViewModel.kt
@@ -20,15 +20,18 @@
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.viewModelScope
 import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor
+import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor
 import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintAction
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep.Enrollment
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationViewModel
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
@@ -41,6 +44,7 @@
   private val fingerprintEnrollViewModel: FingerprintEnrollEnrollingViewModel,
   private val navigationViewModel: FingerprintNavigationViewModel,
   orientationInteractor: OrientationInteractor,
+  private val fingerprintManager: FingerprintManagerInteractor,
 ) : ViewModel() {
 
   private val _textViewIsVisible = MutableStateFlow(false)
@@ -52,7 +56,16 @@
   /** Indicates if the icon should be animating or not */
   val shouldAnimateIcon = _shouldAnimateIcon
 
-  private var enrollFlow: Flow<FingerEnrollState?> = fingerprintEnrollViewModel.enrollFlow
+  private var enrollFlow: Flow<FingerEnrollState?> =
+    fingerprintManager.sensorPropertiesInternal.filterNotNull().combine(
+      fingerprintEnrollViewModel.enrollFlow
+    ) { props, enroll ->
+      if (props.sensorType == FingerprintSensorType.REAR) {
+        enroll
+      } else {
+        null
+      }
+    }
 
   /**
    * Enroll progress message with a replay of size 1 allowing for new subscribers to get the most
@@ -149,6 +162,7 @@
     private val fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel,
     private val navigationViewModel: FingerprintNavigationViewModel,
     private val orientationInteractor: OrientationInteractor,
+    private val fingerprintManager: FingerprintManagerInteractor,
   ) : ViewModelProvider.Factory {
 
     @Suppress("UNCHECKED_CAST")
@@ -157,6 +171,7 @@
         fingerprintEnrollEnrollingViewModel,
         navigationViewModel,
         orientationInteractor,
+        fingerprintManager,
       )
         as T
     }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt
index a2e5232..4588a07 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt
@@ -21,8 +21,10 @@
 import android.view.MotionEvent
 import android.view.MotionEvent.ACTION_HOVER_MOVE
 import android.view.View
+import android.view.ViewGroup
 import android.view.WindowManager
-import android.widget.TextView
+import android.widget.Button
+import android.widget.FrameLayout
 import androidx.annotation.VisibleForTesting
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.Lifecycle
@@ -40,7 +42,6 @@
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.EducationAnimationModel
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsViewModel
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget.UdfpsEnrollViewV2
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
 import com.google.android.setupdesign.GlifLayout
 import kotlinx.coroutines.launch
 
@@ -71,10 +72,8 @@
     val fragment = this
     lottie = view.findViewById(R.id.illustration_lottie)!!
     udfpsEnrollView = view.findViewById(R.id.udfps_animation_view)!!
-    val titleTextView = view.findViewById<TextView>(R.id.title)!!
-    val descriptionTextView = view.findViewById<TextView>(R.id.description)!!
+    val glifLayout: GlifLayout = view.findViewById(R.id.glif_layout)!!
 
-    val glifLayout = view.findViewById<GlifLayout>(R.id.dummy_glif_layout)!!
     val backgroundColor = glifLayout.backgroundBaseColor
     val window = requireActivity().window
     window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
@@ -83,6 +82,10 @@
     window.statusBarColor = color
     view.setBackgroundColor(color)
 
+    view.findViewById<Button>(R.id.skip)?.apply {
+      setOnClickListener { viewModel.negativeButtonClicked() }
+    }
+
     udfpsEnrollView.setFinishAnimationCompleted { viewModel.finishedSuccessfully() }
 
     viewLifecycleOwner.lifecycleScope.launch {
@@ -92,32 +95,41 @@
             udfpsEnrollView.setSensorRect(sensor.sensorBounds, sensor.sensorType)
           }
         }
-        viewLifecycleOwner.lifecycleScope.launch {
-          viewModel.headerText.collect { titleTextView.setText(it.toResource()) }
-        }
 
-        viewLifecycleOwner.lifecycleScope.launch {
-          viewModel.descriptionText.collect {
-            if (it != null) {
-              it.toResource()?.let { text -> descriptionTextView.setText(text) }
-            } else {
-              descriptionTextView.text = ""
+        launch { viewModel.overlayShown.collect { udfpsEnrollView.overlayShown() } }
+        launch { viewModel.headerText.collect { glifLayout.setHeaderText(it.toResource()) } }
+        launch {
+          viewModel.userInteractedWithSensor.collect {
+            if (!it) {
+              glifLayout.setHeaderText(R.string.security_settings_fingerprint_enroll_udfps_title)
+              glifLayout.setDescriptionText(R.string.security_settings_udfps_enroll_start_message)
             }
           }
         }
-        viewLifecycleOwner.lifecycleScope.launch {
+
+        launch {
+          viewModel.descriptionText.collect {
+            if (it != null) {
+              it.toResource()?.let { text -> glifLayout.setDescriptionText(text) }
+            } else {
+              glifLayout.descriptionText = ""
+            }
+          }
+        }
+        launch {
           viewModel.shouldShowLottie.collect {
             lottie.visibility = if (it) View.VISIBLE else View.GONE
           }
         }
 
-        viewLifecycleOwner.lifecycleScope.launch {
+        launch {
           viewModel.lottie.collect { lottieModel ->
             if (lottie.visibility == View.GONE) {
               return@collect
             }
             val resource = lottieModel.toResource()
             if (resource != null) {
+              glifLayout.descriptionTextView.visibility = View.GONE
               LottieCompositionFactory.fromRawRes(requireContext(), resource).addListener { comp ->
                 comp?.let { composition ->
                   lottie.setComposition(composition)
@@ -126,27 +138,24 @@
                 }
               }
             } else {
+              glifLayout.descriptionTextView.visibility = View.VISIBLE
               lottie.visibility = View.INVISIBLE
             }
           }
         }
-
-        viewLifecycleOwner.lifecycleScope.launch {
-          repeatOnLifecycle(Lifecycle.State.DESTROYED) { viewModel.stopEnrollment() }
-        }
-
-        viewLifecycleOwner.lifecycleScope.launch {
+        launch {
           viewModel.accessibilityEnabled.collect { enabled ->
             udfpsEnrollView.setAccessibilityEnabled(enabled)
           }
         }
 
-        viewLifecycleOwner.lifecycleScope.launch {
+        launch {
           viewModel.enrollState.collect {
             Log.d(TAG, "EnrollEvent $it")
             if (it is FingerEnrollState.EnrollError) {
               try {
                 FingerprintErrorDialog.showInstance(it, fragment)
+                viewModel.errorDialogShown(it)
               } catch (exception: Exception) {
                 Log.e(TAG, "Exception occurred $exception")
               }
@@ -156,19 +165,40 @@
           }
         }
 
-        viewLifecycleOwner.lifecycleScope.launch {
-          viewModel.progressSaved.collect { udfpsEnrollView.onEnrollProgressSaved(it) }
+        launch { viewModel.progressSaved.collect { udfpsEnrollView.onEnrollProgressSaved(it) } }
+
+        launch { viewModel.guidedEnrollment.collect { udfpsEnrollView.updateGuidedEnrollment(it) } }
+        launch {
+          viewModel.guidedEnrollmentSaved.collect { udfpsEnrollView.onGuidedPointSaved(it) }
         }
 
-        viewLifecycleOwner.lifecycleScope.launch {
-          viewModel.guidedEnrollment.collect {
-            glifLayout.post { udfpsEnrollView.updateGuidedEnrollment(it) }
+        launch { viewModel.shouldDrawIcon.collect { udfpsEnrollView.shouldDrawIcon(it) } }
+
+        // Hack to get the final step of enroll progress to animate.
+        launch {
+          viewModel.udfpsLastStepViewModel.shouldAnimateCompletion.collect {
+            Log.d(TAG, "Sending fake last enroll event $it")
+            udfpsEnrollView.onUdfpsEvent(it)
           }
         }
-        viewLifecycleOwner.lifecycleScope.launch {
-          viewModel.guidedEnrollmentSaved.collect {
-            glifLayout.post { udfpsEnrollView.onGuidedPointSaved(it) }
-          }
+      }
+    }
+
+    viewLifecycleOwner.lifecycleScope.launch {
+      repeatOnLifecycle(Lifecycle.State.DESTROYED) { viewModel.stopEnrollment() }
+    }
+
+    viewLifecycleOwner.lifecycleScope.launch {
+      viewModel.isLandscape.collect {
+        if (it) {
+          changeViewToLandscape()
+        }
+      }
+    }
+    viewLifecycleOwner.lifecycleScope.launch {
+      viewModel.isReverseLandscape.collect {
+        if (it) {
+          changeViewToReverseLandscape()
         }
       }
     }
@@ -183,12 +213,70 @@
     viewModel.readyForEnrollment()
   }
 
+  private fun changeViewToReverseLandscape() {
+    Log.d(TAG, "changeViewToReverseLandscape")
+    val glifContainer = requireView().findViewById<GlifLayout>(R.id.glif_layout)!!
+    val headerView =
+      glifContainer.findViewById<View>(
+        com.google.android.setupdesign.R.id.sud_landscape_header_area
+      )
+    // The landscape_header_area nad landscape_content_area should have the same parent
+    val parent = headerView!!.parent as ViewGroup
+    val sudContentFrame =
+      glifContainer.findViewById<View>(
+        com.google.android.setupdesign.R.id.sud_landscape_content_area
+      )!!
+    val udfpsContainer = requireView().findViewById<FrameLayout>(R.id.layout_container)!!
+
+    parent.removeView(headerView)
+    parent.removeView(sudContentFrame)
+    parent.addView(sudContentFrame)
+    parent.addView(headerView)
+
+    unclipSubviewsFromParent(udfpsContainer)
+    udfpsEnrollView.requestLayout()
+  }
+
+  private fun changeViewToLandscape() {
+    Log.d(TAG, "changeViewToLandscape")
+
+    val glifContainer = requireView().findViewById<GlifLayout>(R.id.glif_layout)!!
+    val headerView =
+      glifContainer.findViewById<View>(
+        com.google.android.setupdesign.R.id.sud_landscape_header_area
+      )
+    // The landscape_header_area nad landscape_content_area should have the same parent
+    val parent = headerView!!.parent as ViewGroup
+    val sudContentFrame =
+      glifContainer.findViewById<View>(
+        com.google.android.setupdesign.R.id.sud_landscape_content_area
+      )!!
+
+    parent.removeView(headerView)
+    parent.removeView(sudContentFrame)
+    parent.addView(headerView)
+    parent.addView(sudContentFrame)
+
+    val udfpsContainer = requireView().findViewById<FrameLayout>(R.id.layout_container)!!
+    unclipSubviewsFromParent(udfpsContainer)
+    udfpsEnrollView.requestLayout()
+  }
+
+  private fun unclipSubviewsFromParent(view: View) {
+    var currParent = view.parent
+    while (currParent is ViewGroup) {
+      currParent.clipChildren = false
+      currParent.clipToPadding = false
+      currParent = currParent.parent
+    }
+  }
+
   private fun HeaderText.toResource(): Int {
     return when (this.enrollStageModel) {
       EnrollStageModel.Center,
-      EnrollStageModel.Guided,
-      EnrollStageModel.Fingertip,
-      EnrollStageModel.Unknown -> R.string.security_settings_udfps_enroll_fingertip_title
+      EnrollStageModel.Guided -> R.string.security_settings_fingerprint_enroll_repeat_title
+      EnrollStageModel.Fingertip -> R.string.security_settings_udfps_enroll_fingertip_title
+      EnrollStageModel.Unknown -> R.string.security_settings_fingerprint_enroll_udfps_title
       EnrollStageModel.LeftEdge -> R.string.security_settings_udfps_enroll_left_edge_title
       EnrollStageModel.RightEdge -> R.string.security_settings_udfps_enroll_right_edge_title
     }
@@ -218,6 +306,5 @@
 
   companion object {
     private const val TAG = "UDFPSEnrollFragment"
-    private val navStep = FingerprintNavigationStep.Enrollment::class
   }
 }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/UdfpsSensorLocation.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/UdfpsSensorLocation.kt
new file mode 100644
index 0000000..6eb29f9
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/model/UdfpsSensorLocation.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model
+
+import android.graphics.PointF
+import com.android.systemui.biometrics.shared.model.FingerprintSensor
+
+data class UdfpsSensorLocation(val sensorLocation: FingerprintSensor, val offset: PointF?)
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsLastStepViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsLastStepViewModel.kt
new file mode 100644
index 0000000..6a45ec4
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsLastStepViewModel.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintVibrationEffects
+import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor
+import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.flow.transform
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+/**
+ * This class is responsible for determining if we should animate the last step of an enrollment. It
+ * seems to be due to poor hardware implementation that the last step of a UDFPS enrollment takes
+ * much longer then normal (likely due to to saving the whole enrollment/or some kind of
+ * verification)
+ *
+ * Because of this, we will use the information of if a fingerprint was acquired(good) + enrollment
+ * has reached the last step
+ */
+class UdfpsLastStepViewModel(
+  private val fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel,
+  private val vibrationInteractor: VibrationInteractor,
+) : ViewModel() {
+
+  private val lastStep: MutableStateFlow<FingerEnrollState.EnrollProgress?> = MutableStateFlow(null)
+  private val stepSize: MutableStateFlow<Int?> = MutableStateFlow(null)
+
+  init {
+    viewModelScope.launch {
+      val steps =
+        fingerprintEnrollEnrollingViewModel.enrollFlow
+          .filterIsInstance<FingerEnrollState.EnrollProgress>()
+          .distinctUntilChanged()
+          .take(2)
+          .toList()
+      stepSize.update { steps[0].remainingSteps - steps[1].remainingSteps }
+      lastStep.update { FingerEnrollState.EnrollProgress(0, steps[0].totalStepsRequired) }
+    }
+  }
+
+  private val enrollProgress =
+    fingerprintEnrollEnrollingViewModel.enrollFlow.filterIsInstance<
+      FingerEnrollState.EnrollProgress
+    >()
+
+  /** Indicates if we should animate the completion of udfps enrollment. */
+  val shouldAnimateCompletion =
+    enrollProgress
+      .transform { it ->
+        if (it.remainingSteps == stepSize.value) {
+          fingerprintEnrollEnrollingViewModel.enrollFlow
+            .filterIsInstance<FingerEnrollState.Acquired>()
+            .filter { acquired -> acquired.acquiredGood }
+            .collect {
+              vibrationInteractor.vibrate(
+                FingerprintVibrationEffects.UdfpsSuccess,
+                "UdfpsLastStepViewModel",
+              )
+
+              emit(lastStep.value)
+            }
+        }
+      }
+      .filterNotNull()
+
+  class UdfpsLastStepViewModelFactory(
+    private val fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel,
+    private val vibrationInteractor: VibrationInteractor,
+  ) : ViewModelProvider.Factory {
+
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : ViewModel> create(modelClass: Class<T>): T {
+      return UdfpsLastStepViewModel(fingerprintEnrollEnrollingViewModel, vibrationInteractor) as T
+    }
+  }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt
index a22f680..508084e 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt
@@ -22,9 +22,10 @@
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.viewModelScope
-import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository
 import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
+import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository
 import com.android.settings.biometrics.fingerprint2.data.repository.SimulatedTouchEventsRepository
+import com.android.settings.biometrics.fingerprint2.domain.interactor.AccessibilityInteractor
 import com.android.settings.biometrics.fingerprint2.domain.interactor.DebuggingInteractor
 import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractor
 import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractor
@@ -32,6 +33,7 @@
 import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor
 import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor
 import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor
+import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor
 import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.DescriptionText
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.HeaderText
@@ -41,6 +43,7 @@
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
 import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationViewModel
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.combineTransform
@@ -51,6 +54,8 @@
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.transform
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 
 /** ViewModel used to drive UDFPS Enrollment through [UdfpsEnrollFragment] */
@@ -66,13 +71,25 @@
   backgroundViewModel: BackgroundViewModel,
   sensorRepository: FingerprintSensorRepository,
   udfpsEnrollInteractor: UdfpsEnrollInteractor,
+  fingerprintManager: FingerprintManagerInteractor,
+  val udfpsLastStepViewModel: UdfpsLastStepViewModel,
+  accessibilityInteractor: AccessibilityInteractor,
 ) : ViewModel() {
 
   private val isSetupWizard = flowOf(false)
   private var shouldResetErollment = false
 
   private var _enrollState: Flow<FingerEnrollState?> =
-    fingerprintEnrollEnrollingViewModel.enrollFlow
+    fingerprintManager.sensorPropertiesInternal.filterNotNull().combine(
+      fingerprintEnrollEnrollingViewModel.enrollFlow
+    ) { props, enroll ->
+      if (props.sensorType.isUdfps()) {
+        enroll
+      } else {
+        null
+      }
+    }
+
   /** The current state of the enrollment. */
   var enrollState: Flow<FingerEnrollState> =
     combine(fingerprintEnrollEnrollingViewModel.enrollFlowShouldBeRunning, _enrollState) {
@@ -86,48 +103,59 @@
       }
       .filterNotNull()
 
+  /** Indicates that overlay has been shown */
+  val overlayShown =
+    enrollState
+      .filterIsInstance<FingerEnrollState.OverlayShown>()
+      .shareIn(viewModelScope, SharingStarted.Eagerly, 1)
+
+  private var _userInteractedWithSensor = MutableStateFlow(false)
+
+  /**
+   * This indicates whether the user has interacted with the sensor or not. This indicates if we are
+   * in the initial state of the UI.
+   */
+  val userInteractedWithSensor: Flow<Boolean> =
+    enrollState.transform {
+      val interactiveMessage =
+        when (it) {
+          is FingerEnrollState.Acquired,
+          is FingerEnrollState.EnrollError,
+          is FingerEnrollState.EnrollHelp,
+          is FingerEnrollState.EnrollProgress,
+          is FingerEnrollState.PointerDown,
+          is FingerEnrollState.PointerUp -> true
+          else -> false
+        }
+      val hasPreviouslyInteracted = _userInteractedWithSensor.value or interactiveMessage
+      _userInteractedWithSensor.update { hasPreviouslyInteracted }
+      emit(hasPreviouslyInteracted)
+    }
+
   /**
    * Forwards the property sensor information. This is typically used to recreate views that must be
    * aligned with the sensor.
    */
   val sensorLocation = sensorRepository.fingerprintSensor
 
-  /** Indicates if accessibility is enabled */
-  val accessibilityEnabled = flowOf(true).shareIn(viewModelScope, SharingStarted.Eagerly, 1)
-
-  init {
-    viewModelScope.launch {
-      enrollState
-        .combine(accessibilityEnabled) { event, isEnabled -> Pair(event, isEnabled) }
-        .collect {
-          if (
-            when (it.first) {
-              is FingerEnrollState.EnrollError -> true
-              is FingerEnrollState.EnrollHelp -> it.second
-              is FingerEnrollState.EnrollProgress -> true
-              else -> false
-            }
-          ) {
-            vibrate(it.first)
-          }
-        }
-    }
-
-    viewModelScope.launch {
-      backgroundViewModel.background.filter { it }.collect { didGoToBackground() }
-    }
-  }
-
-  /**
-   * This indicates at which point the UI should offset the fingerprint sensor icon for guided
-   * enrollment.
-   */
+  /** Indicates a step of guided enrollment, the ui should animate the icon to the new location. */
   val guidedEnrollment: Flow<PointF> =
-    udfpsEnrollInteractor.guidedEnrollmentOffset.distinctUntilChanged()
+    udfpsEnrollInteractor.guidedEnrollmentOffset
+      .distinctUntilChanged()
+      .shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 0)
 
-  /** The saved version of [guidedEnrollment] */
-  val guidedEnrollmentSaved: Flow<PointF> =
-    guidedEnrollment.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
+  private var _lastOrientation: Int? = null
+
+  /** In case of rotations we should ensure the UI does not re-animate the last state. */
+  private val shouldReplayLastEvent =
+    orientationInteractor.rotation.transform {
+      if (_lastOrientation != null && _lastOrientation!! != it) {
+        emit(true)
+      } else {
+        emit(false)
+      }
+      _lastOrientation = it
+    }
 
   /**
    * This is the saved progress, this is for when views are recreated and need saved state for the
@@ -136,8 +164,32 @@
   var progressSaved: Flow<FingerEnrollState.EnrollProgress> =
     enrollState
       .filterIsInstance<FingerEnrollState.EnrollProgress>()
-      .filterNotNull()
-      .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
+      .combineTransform(shouldReplayLastEvent) { enroll, shouldReplay ->
+        if (shouldReplay) {
+          emit(enroll)
+        }
+      }
+      .shareIn(viewModelScope, SharingStarted.Eagerly, 1)
+
+  /** Indicates if accessibility is enabled */
+  val accessibilityEnabled =
+    accessibilityInteractor.isAccessibilityEnabled.shareIn(
+      this.viewModelScope,
+      SharingStarted.Eagerly,
+      replay = 1,
+    )
+
+  /** Indicates if we are in reverse landscape orientation. */
+  val isReverseLandscape =
+    orientationInteractor.rotation
+      .transform { emit(it == Surface.ROTATION_270) }
+      .distinctUntilChanged()
+
+  /** Indicates if we are in the landscape orientation */
+  val isLandscape =
+    orientationInteractor.rotation
+      .transform { emit(it == Surface.ROTATION_90) }
+      .distinctUntilChanged()
 
   /** This sends touch exploration events only used for debugging purposes. */
   val touchExplorationDebug: Flow<Point> =
@@ -170,6 +222,18 @@
       .filterNotNull()
       .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
 
+  /** The saved version of [guidedEnrollment] */
+  val guidedEnrollmentSaved: Flow<PointF> =
+    combineTransform(guidedEnrollment, shouldReplayLastEvent, enrollStage) {
+        point,
+        shouldReplay,
+        stage ->
+        if (shouldReplay && stage is EnrollStageModel.Guided) {
+          emit(point)
+        }
+      }
+      .shareIn(viewModelScope, SharingStarted.Eagerly, 1)
+
   init {
     viewModelScope.launch {
       enrollState
@@ -200,7 +264,7 @@
     }
 
     viewModelScope.launch {
-      backgroundViewModel.background.filter { true }.collect { didGoToBackground() }
+      backgroundViewModel.background.filter { it }.collect { didGoToBackground() }
     }
   }
 
@@ -210,20 +274,12 @@
         displayDensityInteractor.displayDensity,
         displayDensityInteractor.defaultDisplayDensity,
         displayDensityInteractor.fontScale,
-        orientationInteractor.rotation,
-      ) { currDisplayDensity, defaultDisplayDensity, fontScale, rotation ->
-        val canShowLottieForRotation =
-          when (rotation) {
-            Surface.ROTATION_0 -> true
-            else -> false
-          }
-
-        canShowLottieForRotation &&
-          if (fontScale > 1.0f) {
-            false
-          } else {
-            defaultDisplayDensity == currDisplayDensity
-          }
+      ) { currDisplayDensity, defaultDisplayDensity, fontScale ->
+        if (fontScale > 1.0f) {
+          false
+        } else {
+          defaultDisplayDensity == currDisplayDensity
+        }
       }
       .shareIn(viewModelScope, SharingStarted.Eagerly, 1)
 
@@ -234,6 +290,18 @@
       }
       .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
 
+  /** Indicates if we should or shold not draw the fingerprint icon */
+  val shouldDrawIcon: Flow<Boolean> =
+    enrollState.transform { state ->
+      when (state) {
+        is FingerEnrollState.EnrollProgress,
+        is FingerEnrollState.EnrollError,
+        is FingerEnrollState.PointerUp -> emit(true)
+        is FingerEnrollState.PointerDown -> emit(false)
+        else -> {}
+      }
+    }
+
   private val shouldClearDescriptionText = enrollStage.map { it is EnrollStageModel.Unknown }
 
   /** The description text for UDFPS enrollment */
@@ -267,12 +335,12 @@
 
   /** Indicates the negative button has been clicked */
   fun negativeButtonClicked() {
-    doReset()
     navigationViewModel.update(
       FingerprintAction.NEGATIVE_BUTTON_PRESSED,
       navStep,
       "$TAG#negativeButtonClicked",
     )
+    doReset()
   }
 
   /** Indicates that an enrollment was completed */
@@ -282,7 +350,7 @@
   }
 
   /** Indicates that the application went to the background. */
-  private fun didGoToBackground() {
+  fun didGoToBackground() {
     navigationViewModel.update(
       FingerprintAction.DID_GO_TO_BACKGROUND,
       navStep,
@@ -293,11 +361,7 @@
 
   private fun doReset() {
     _enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow
-    progressSaved =
-      enrollState
-        .filterIsInstance<FingerEnrollState.EnrollProgress>()
-        .filterNotNull()
-        .shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
+    _userInteractedWithSensor.update { false }
   }
 
   /** The lottie that should be shown for UDFPS Enrollment */
@@ -320,6 +384,15 @@
     vibrationInteractor.vibrate(vibrationEvent, "UdfpsEnrollFragment")
   }
 
+  /** Indicates an error sent by the HAL has been acknowledged by the user */
+  fun errorDialogShown(it: FingerEnrollState.EnrollError) {
+    navigationViewModel.update(
+      FingerprintAction.USER_CLICKED_FINISH,
+      navStep,
+      "${TAG}#userClickedStopEnrollingDialog",
+    )
+  }
+
   class UdfpsEnrollmentFactory(
     private val vibrationInteractor: VibrationInteractor,
     private val displayDensityInteractor: DisplayDensityInteractor,
@@ -332,6 +405,9 @@
     private val backgroundViewModel: BackgroundViewModel,
     private val sensorRepository: FingerprintSensorRepository,
     private val udfpsEnrollInteractor: UdfpsEnrollInteractor,
+    private val fingerprintManager: FingerprintManagerInteractor,
+    private val udfpsLastStepViewModel: UdfpsLastStepViewModel,
+    private val accessibilityInteractor: AccessibilityInteractor,
   ) : ViewModelProvider.Factory {
 
     @Suppress("UNCHECKED_CAST")
@@ -348,6 +424,9 @@
         backgroundViewModel,
         sensorRepository,
         udfpsEnrollInteractor,
+        fingerprintManager,
+        udfpsLastStepViewModel,
+        accessibilityInteractor,
       )
         as T
     }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt
index c209c55..1eec046 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt
@@ -36,8 +36,8 @@
 import android.view.animation.AccelerateDecelerateInterpolator
 import androidx.core.animation.addListener
 import androidx.core.graphics.toRect
-import androidx.core.graphics.toRectF
 import com.android.settings.R
+import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
 import kotlin.math.sin
 
 /**
@@ -45,7 +45,6 @@
  * various stages of enrollment
  */
 class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeSet?) : Drawable() {
-  private var targetAnimationDuration: Long = TARGET_ANIM_DURATION_LONG
   private var targetAnimatorSet: AnimatorSet? = null
   private val movingTargetFpIcon: Drawable
   private val fingerprintDrawable: ShapeDrawable
@@ -55,7 +54,7 @@
   @ColorInt private var movingTargetFill = 0
   private var currentScale = 1.0f
   private var alpha = 0
-  private var guidedEnrollmentOffset: PointF? = null
+  private var stopDrawing = false
 
   /**
    * This is the physical location of the sensor. This rect will be updated by [drawSensorRectAt]
@@ -63,15 +62,12 @@
   private val sensorRectBounds: Rect = Rect()
 
   /**
-   * The following values are used to describe where the icon should be drawn. [currX] and [currY]
-   * are changed based on the current guided enrollment step which is given by the
-   * [UdfpsEnrollHelperV2]
+   * The following values are used to describe where the icon should be drawn. [sensorLeftOffset]
+   * and [sensorTopOffset] are changed based on the current guided enrollment step which is given by
+   * the [UdfpsEnrollHelperV2]
    */
-  private var currX = 0f
-  private var currY = 0f
-
-  private var sensorWidth = 0f
-  private var sensorHeight = 0f
+  private var sensorLeftOffset = 0f
+  private var sensorTopOffset = 0f
 
   init {
     fingerprintDrawable = createUdfpsIcon(context)
@@ -131,29 +127,35 @@
    * The [sensorRect] coordinates for the sensor area. The [sensorRect] should be the coordinates
    * with respect to its root frameview
    */
-  fun drawSensorRectAt(sensorRect: Rect) {
-    Log.e(TAG, "UdfpsEnrollIcon#drawSensorRect($sensorRect)")
+  fun drawSensorRectAt(overlayParams: UdfpsOverlayParams) {
+    Log.e(TAG, "UdfpsEnrollIcon#drawSensorRect(${overlayParams.sensorBounds})")
+    val sensorRect = overlayParams.sensorBounds
     sensorRectBounds.set(sensorRect)
     fingerprintDrawable.bounds = sensorRect
     movingTargetFpIcon.bounds = sensorRect
-    currX = sensorRect.left.toFloat()
-    currY = sensorRect.top.toFloat()
-    sensorWidth = (sensorRect.right - sensorRect.left).toFloat()
-    sensorHeight = (sensorRect.bottom - sensorRect.top).toFloat()
+
+    // End existing animation if we get an update of the sensor rect.
+    targetAnimatorSet?.end()
+
     invalidateSelf()
   }
 
   /** Stop drawing the fingerprint icon. */
   fun stopDrawing() {
-    alpha = 0
+    stopDrawing = true
+    invalidateSelf()
   }
 
   /** Resume drawing the fingerprint icon */
   fun startDrawing() {
-    alpha = 255
+    stopDrawing = false
+    invalidateSelf()
   }
 
   override fun draw(canvas: Canvas) {
+    if (stopDrawing) {
+      return
+    }
     movingTargetFpIcon.alpha = alpha
     fingerprintDrawable.setAlpha(alpha)
     sensorOutlinePaint.setAlpha(alpha)
@@ -165,23 +167,28 @@
     fingerprintDrawable.draw(canvas)
   }
 
-  private fun getCurrLocation(): RectF =
-    RectF(currX, currY, currX + sensorWidth, currY + sensorHeight)
+  private fun getCurrLocation(): RectF {
+    val x = sensorRectBounds.left + sensorLeftOffset
+    val y = sensorRectBounds.top + sensorTopOffset
+    return RectF(x, y, x + sensorRectBounds.width(), y + sensorRectBounds.height())
+  }
 
-  private fun animateMovement(currentBounds: Rect, offsetRect: RectF, scaleMovement: Boolean) {
-    if (currentBounds.equals(offsetRect)) {
+  private fun animateMovement(leftOffset: Float, topOffset: Float, scaleMovement: Boolean) {
+    if (leftOffset == sensorLeftOffset && topOffset == sensorTopOffset) {
       return
     }
 
-    val xAnimator = ValueAnimator.ofFloat(currentBounds.left.toFloat(), offsetRect.left)
+    val currLocation = getCurrLocation()
+
+    val xAnimator = ValueAnimator.ofFloat(currLocation.left - sensorRectBounds.left, leftOffset)
     xAnimator.addUpdateListener {
-      currX = it.animatedValue as Float
+      sensorLeftOffset = it.animatedValue as Float
       invalidateSelf()
     }
 
-    val yAnimator = ValueAnimator.ofFloat(currentBounds.top.toFloat(), offsetRect.top)
+    val yAnimator = ValueAnimator.ofFloat(currLocation.top - sensorRectBounds.top, topOffset)
     yAnimator.addUpdateListener {
-      currY = it.animatedValue as Float
+      sensorTopOffset = it.animatedValue as Float
       invalidateSelf()
     }
     val animators = mutableListOf(xAnimator, yAnimator)
@@ -199,6 +206,7 @@
       animators.add(scaleAnimator)
     }
 
+    targetAnimatorSet?.cancel()
     targetAnimatorSet = AnimatorSet()
 
     targetAnimatorSet?.let {
@@ -210,50 +218,13 @@
   }
 
   /**
-   * This sets animation time to 0. This typically happens after an activity recreation, we don't
-   * want to re-animate the progress/success animation with the default timer
-   */
-  private fun setAnimationTimeToZero() {
-    targetAnimationDuration = 0
-  }
-
-  /** This sets animation timers back to normal, this happens after we have */
-  private fun restoreAnimationTime() {
-    targetAnimationDuration = TARGET_ANIM_DURATION_LONG
-  }
-
-  /**
    * Indicates a change to guided enrollment has occurred. Also indicates if we are recreating the
    * view, in which case their is no need to animate the icon to whatever position it was in.
    */
-  fun updateGuidedEnrollment(point: PointF,  isRecreating: Boolean) {
-    guidedEnrollmentOffset = point
-    if (isRecreating) {
-      setAnimationTimeToZero()
-    } else {
-      restoreAnimationTime()
-    }
-
-    val currentBounds = getCurrLocation().toRect()
-    val offset = guidedEnrollmentOffset
-    if (offset?.x != 0f && offset?.y != 0f) {
-      val targetRect = Rect(sensorRectBounds).toRectF()
-      // This is the desired location of the sensor rect, the [EnrollHelper]
-      // offsets the initial sensor rect by a bit to get the user to move their finger a bit more.
-      targetRect.offset(offset!!.x, offset!!.y)
-      val shouldAnimateMovement = !currentBounds.equals(targetRect)
-      if (shouldAnimateMovement) {
-        targetAnimatorSet?.cancel()
-        animateMovement(currentBounds, targetRect, true)
-      } else {
-        // If we are not offsetting the sensor, move it back to its original place
-        animateMovement(currentBounds, sensorRectBounds.toRectF(), false)
-      }
-    } else {
-      // If we are not offsetting the sensor, move it back to its original place
-      animateMovement(currentBounds, sensorRectBounds.toRectF(), false)
-    }
-    invalidateSelf()
+  fun updateGuidedEnrollment(point: PointF, isRecreating: Boolean) {
+    val pointIsZero = point.x == 0f && point.y == 0f
+    val shouldAnimateMovement = pointIsZero || !isRecreating
+    animateMovement(point?.x ?: 0f, point?.y ?: 0f, shouldAnimateMovement)
   }
 
   companion object {
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt
index bf2f026..c3adc87 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt
@@ -27,16 +27,15 @@
 import android.graphics.drawable.Drawable
 import android.util.AttributeSet
 import android.util.DisplayMetrics
-import android.util.Log
 import android.view.animation.DecelerateInterpolator
 import android.view.animation.Interpolator
 import android.view.animation.OvershootInterpolator
 import androidx.annotation.ColorInt
-import androidx.core.animation.addListener
 import androidx.core.animation.doOnEnd
 import androidx.core.graphics.toRectF
 import com.android.internal.annotations.VisibleForTesting
 import com.android.settings.R
+import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
 import kotlin.math.cos
 import kotlin.math.max
 import kotlin.math.sin
@@ -145,7 +144,6 @@
 
   /** Indicates enrollment progress has occurred. */
   fun onEnrollmentProgress(remaining: Int, totalSteps: Int, isRecreating: Boolean = false) {
-
     afterFirstTouch = true
     updateProgress(remaining, totalSteps, isRecreating)
   }
@@ -216,8 +214,8 @@
    * Draws the progress with locations [sensorLocationX] [sensorLocationY], note these must be with
    * respect to the parent framelayout.
    */
-  fun drawProgressAt(sensorRect: Rect) {
-    this.sensorRect.set(sensorRect)
+  fun drawProgressAt(overlayParams: UdfpsOverlayParams) {
+    this.sensorRect.set(overlayParams.sensorBounds)
     invalidateSelf()
   }
 
@@ -251,8 +249,6 @@
 
     this.remainingSteps = remainingSteps
     this.totalSteps = totalSteps
-    this.remainingSteps = remainingSteps
-    this.totalSteps = totalSteps
     val targetProgress = (totalSteps - remainingSteps).toFloat().div(max(1, totalSteps))
 
     if (progressAnimator != null && progressAnimator!!.isRunning) {
@@ -290,12 +286,8 @@
         checkMarkDrawable.bounds = newBounds
         checkMarkDrawable.setVisible(true, false)
       }
-      doOnEnd {
-        onFinishedCompletionAnimation?.let{
-          it()
-        }
+      doOnEnd { onFinishedCompletionAnimation?.let { it() } }
 
-      }
       start()
     }
   }
@@ -356,6 +348,7 @@
   private fun flashHelpFillColor() {
     if (fillColorAnimator != null && fillColorAnimator!!.isRunning) {
       fillColorAnimator!!.end()
+      fillColorAnimator = null
     }
     @ColorInt val targetColor = helpColor
     fillColorAnimator =
@@ -375,7 +368,6 @@
    * want to re-animate the progress/success animation with the default timer
    */
   private fun setAnimationTimeToZero() {
-    fillColorAnimationDuration = 0
     animateArcDuration = 0
     checkmarkAnimationDelayDuration = 0
     checkmarkAnimationDuration = 0
@@ -383,7 +375,6 @@
 
   /** This sets animation timers back to normal, this happens after we have */
   private fun restoreAnimationTime() {
-    fillColorAnimationDuration = FILL_COLOR_ANIMATION_DURATION_MS
     animateArcDuration = PROGRESS_ANIMATION_DURATION_MS
     checkmarkAnimationDelayDuration = CHECKMARK_ANIMATION_DELAY_MS
     checkmarkAnimationDuration = CHECKMARK_ANIMATION_DURATION_MS
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt
index 586408f..d9593c1 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt
@@ -70,27 +70,11 @@
   fun setSensorRect(rect: Rect, sensorType: FingerprintSensorType) {
     this.sensorRect = rect
     this.fingerprintSensorType = sensorType
-    findViewById<ImageView?>(R.id.udfps_enroll_animation_fp_progress_view)?.also {
-      it.setImageDrawable(fingerprintProgressDrawable)
-    }
-    findViewById<ImageView>(R.id.udfps_enroll_animation_fp_view)?.also {
-      it.setImageDrawable(fingerprintIcon)
-    }
 
-    val rotation = display.rotation
     var displayInfo = DisplayInfo()
     context.display.getDisplayInfo(displayInfo)
+    val rotation = displayInfo.rotation
     val scaleFactor = udfpsUtils.getScaleFactor(displayInfo)
-    val overlayParams =
-      UdfpsOverlayParams(
-        sensorRect,
-        fingerprintProgressDrawable.bounds,
-        displayInfo.naturalWidth,
-        displayInfo.naturalHeight,
-        scaleFactor,
-        rotation,
-        sensorType.toInt(),
-      )
     val parentView = parent as ViewGroup
     val coords = parentView.getLocationOnScreen()
     val parentLeft = coords[0]
@@ -99,22 +83,44 @@
     // If the view has been rotated, we need to translate the sensor coordinates
     // to the new rotated view.
     when (rotation) {
-      Surface.ROTATION_90,
-      Surface.ROTATION_270 -> {
+      Surface.ROTATION_90 -> {
         sensorRectOffset.set(
           sensorRectOffset.top,
           sensorRectOffset.left,
           sensorRectOffset.bottom,
           sensorRectOffset.right,
         )
+        sensorRectOffset.offset(-parentLeft, -parentTop)
       }
-      else -> {}
-    }
-    // Translate the sensor position into UdfpsEnrollView's view space.
-    sensorRectOffset.offset(-parentLeft, -parentTop)
+      // When the view is rotated 270 degrees, 0,0 is the top corner left
+      Surface.ROTATION_270 -> {
+        sensorRectOffset.set(
+          (displayInfo.naturalHeight - sensorRectOffset.bottom) - parentLeft,
+          sensorRectOffset.left - parentTop,
+          (displayInfo.naturalHeight - sensorRectOffset.top) - parentLeft,
+          sensorRectOffset.right - parentTop,
+        )
+      }
+      else -> {
 
-    fingerprintIcon.drawSensorRectAt(sensorRectOffset)
-    fingerprintProgressDrawable.drawProgressAt(sensorRectOffset)
+        sensorRectOffset.offset(-parentLeft, -parentTop)
+      }
+    }
+
+    // Translate the sensor position into UdfpsEnrollView's view space.
+    val overlayParams =
+      UdfpsOverlayParams(
+        sensorRectOffset,
+        fingerprintProgressDrawable.bounds,
+        displayInfo.naturalWidth,
+        displayInfo.naturalHeight,
+        scaleFactor,
+        rotation,
+        sensorType.toInt(),
+      )
+
+    fingerprintIcon.drawSensorRectAt(overlayParams)
+    fingerprintProgressDrawable.drawProgressAt(overlayParams)
 
     touchExplorationAnnouncer = TouchExplorationAnnouncer(context, this, overlayParams, udfpsUtils)
   }
@@ -126,11 +132,8 @@
         onEnrollmentProgress(event.remainingSteps, event.totalStepsRequired)
       is FingerEnrollState.Acquired -> onAcquired(event.acquiredGood)
       is FingerEnrollState.EnrollHelp -> onEnrollmentHelp()
-      is FingerEnrollState.PointerDown -> onPointerDown()
-      is FingerEnrollState.PointerUp -> onPointerUp()
-      is FingerEnrollState.OverlayShown -> overlayShown()
-      is FingerEnrollState.EnrollError ->
-        throw IllegalArgumentException("$TAG should not handle udfps error")
+      // Else ignore
+      else -> {}
     }
   }
 
@@ -145,7 +148,6 @@
     }
   }
 
-  private fun udfpsError(errMsgId: Int, errString: String) {}
   /**
    * Sends a touch exploration event to the [onHoverListener] this should only be used for
    * debugging.
@@ -170,8 +172,15 @@
     onHoverListener = listener
   }
 
-  private fun overlayShown() {
-    Log.e(TAG, "Implement overlayShown")
+  /** Indicates the overlay has been shown */
+  fun overlayShown() {
+    Log.d(TAG, "Showing udfps overlay")
+    findViewById<ImageView?>(R.id.udfps_enroll_animation_fp_progress_view)?.also {
+      it.setImageDrawable(fingerprintProgressDrawable)
+    }
+    findViewById<ImageView>(R.id.udfps_enroll_animation_fp_view)?.also {
+      it.setImageDrawable(fingerprintIcon)
+    }
   }
 
   /** Receive enroll progress event */
@@ -190,16 +199,6 @@
     if (animateIfLastStepGood) fingerprintProgressDrawable.onLastStepAcquired()
   }
 
-  /** Receive onPointerDown event */
-  private fun onPointerDown() {
-    fingerprintIcon.stopDrawing()
-  }
-
-  /** Receive onPointerUp event */
-  private fun onPointerUp() {
-    fingerprintIcon.startDrawing()
-  }
-
   override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
     super.onLayout(changed, left, top, right, bottom)
     // Because the layout has changed, we need to recompute all locations.
@@ -261,6 +260,17 @@
     fingerprintIcon.updateGuidedEnrollment(point, false)
   }
 
+  /** Indicates if the enroll icon should be drawn. */
+  fun shouldDrawIcon(it: Boolean) {
+    post {
+      if (it) {
+        fingerprintIcon.startDrawing()
+      } else {
+        fingerprintIcon.stopDrawing()
+      }
+    }
+  }
+
   companion object {
     private const val TAG = "UdfpsEnrollView"
   }
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt
index 7ddb142..bbe3cfd 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollEnrollingViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel
 
+import android.util.Log
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import com.android.systemui.biometrics.shared.model.FingerprintSensor
@@ -68,7 +69,8 @@
   val enrollFlow =
     enrollFlowShouldBeRunning.transformLatest {
       if (it) {
-        fingerprintEnrollViewModel.enrollFlow.collect { event -> emit(event) }
+        fingerprintEnrollViewModel.enrollFlow.collect { event ->
+          emit(event) }
       }
     }
 
@@ -82,4 +84,8 @@
         as T
     }
   }
+
+  companion object {
+    private val TAG = "FingerprintEnrollEnrollingViewModel"
+  }
 }
diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
index e017143..44915fe 100644
--- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
@@ -66,6 +66,7 @@
 public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment {
     public static final String KEY_DEVICE_ADDRESS = "device_address";
     private static final String TAG = "BTDeviceDetailsFrg";
+    private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
 
     @VisibleForTesting
     static int EDIT_DEVICE_NAME_ITEM_ID = Menu.FIRST;
@@ -95,11 +96,14 @@
     LocalBluetoothManager mManager;
     @VisibleForTesting
     CachedBluetoothDevice mCachedDevice;
+    BluetoothAdapter mBluetoothAdapter;
 
     @Nullable
     InputDevice mInputDevice;
 
     private UserManager mUserManager;
+    int mExtraControlViewWidth = 0;
+    boolean mExtraControlUriLoaded = false;
 
     private final BluetoothCallback mBluetoothCallback =
             new BluetoothCallback() {
@@ -115,6 +119,16 @@
                 }
             };
 
+    private final BluetoothAdapter.OnMetadataChangedListener mExtraControlMetadataListener =
+            (device, key, value) -> {
+                if (key == METADATA_FAST_PAIR_CUSTOMIZED_FIELDS
+                        && mExtraControlViewWidth > 0
+                        && !mExtraControlUriLoaded) {
+                    Log.i(TAG, "Update extra control UI because of metadata change.");
+                    updateExtraControlUri(mExtraControlViewWidth);
+                }
+            };
+
     public BluetoothDeviceDetailsFragment() {
         super(DISALLOW_CONFIG_BLUETOOTH);
     }
@@ -173,6 +187,7 @@
     public void onAttach(Context context) {
         mDeviceAddress = getArguments().getString(KEY_DEVICE_ADDRESS);
         mManager = getLocalBluetoothManager(context);
+        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
         mCachedDevice = getCachedDevice(mDeviceAddress);
         mUserManager = getUserManager();
 
@@ -202,12 +217,18 @@
                 : null);
 
         mManager.getEventManager().registerCallback(mBluetoothCallback);
+        mBluetoothAdapter.addOnMetadataChangedListener(
+                mCachedDevice.getDevice(),
+                context.getMainExecutor(),
+                mExtraControlMetadataListener);
     }
 
     @Override
     public void onDetach() {
         super.onDetach();
         mManager.getEventManager().unregisterCallback(mBluetoothCallback);
+        mBluetoothAdapter.removeOnMetadataChangedListener(
+                mCachedDevice.getDevice(), mExtraControlMetadataListener);
     }
 
     private void updateExtraControlUri(int viewWidth) {
@@ -222,9 +243,9 @@
                 controlUri = Uri.parse(uri + viewWidth);
             } catch (NullPointerException exception) {
                 Log.d(TAG, "unable to parse uri");
-                controlUri = null;
             }
         }
+        mExtraControlUriLoaded |= controlUri != null;
         final SlicePreferenceController slicePreferenceController = use(
                 SlicePreferenceController.class);
         slicePreferenceController.setSliceUri(sliceEnabled ? controlUri : null);
@@ -253,7 +274,8 @@
                     if (view.getWidth() <= 0) {
                         return;
                     }
-                    updateExtraControlUri(view.getWidth() - getPaddingSize());
+                    mExtraControlViewWidth = view.getWidth() - getPaddingSize();
+                    updateExtraControlUri(mExtraControlViewWidth);
                     view.getViewTreeObserver().removeOnGlobalLayoutListener(
                             mOnGlobalLayoutListener);
                 }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioDialogFragment.java
similarity index 93%
rename from src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java
rename to src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioDialogFragment.java
index df94694..db82619 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioDialogFragment.java
@@ -32,7 +32,7 @@
 import java.util.List;
 
 /** Provides a dialog to choose the active device for calls and alarms. */
-public class CallsAndAlarmsDialogFragment extends InstrumentedDialogFragment {
+public class AudioSharingCallAudioDialogFragment extends InstrumentedDialogFragment {
     private static final String TAG = "CallsAndAlarmsDialog";
     private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items";
 
@@ -55,7 +55,7 @@
     }
 
     /**
-     * Display the {@link CallsAndAlarmsDialogFragment} dialog.
+     * Display the {@link AudioSharingCallAudioDialogFragment} dialog.
      *
      * @param host The Fragment this dialog will be hosted.
      * @param deviceItems The connected device items in audio sharing session.
@@ -71,7 +71,8 @@
         if (manager.findFragmentByTag(TAG) == null) {
             final Bundle bundle = new Bundle();
             bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
-            final CallsAndAlarmsDialogFragment dialog = new CallsAndAlarmsDialogFragment();
+            final AudioSharingCallAudioDialogFragment dialog =
+                    new AudioSharingCallAudioDialogFragment();
             dialog.setArguments(bundle);
             dialog.show(manager, TAG);
         }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioPreferenceController.java
similarity index 96%
rename from src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceController.java
rename to src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioPreferenceController.java
index 8aaebc6..e848f88 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceController.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioPreferenceController.java
@@ -51,6 +51,8 @@
 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 import com.android.settingslib.utils.ThreadUtils;
 
+import com.google.common.collect.ImmutableList;
+
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -60,7 +62,7 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /** PreferenceController to control the dialog to choose the active device for calls and alarms */
-public class CallsAndAlarmsPreferenceController extends AudioSharingBasePreferenceController
+public class AudioSharingCallAudioPreferenceController extends AudioSharingBasePreferenceController
         implements BluetoothCallback {
     private static final String TAG = "CallsAndAlarmsPreferenceController";
     private static final String PREF_KEY = "calls_and_alarms";
@@ -131,7 +133,7 @@
                 }
             };
 
-    public CallsAndAlarmsPreferenceController(Context context) {
+    public AudioSharingCallAudioPreferenceController(Context context) {
         super(context, PREF_KEY);
         mBtManager = Utils.getLocalBtManager(mContext);
         mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
@@ -176,16 +178,13 @@
                         }
                         updateDeviceItemsInSharingSession();
                         if (mDeviceItemsInSharingSession.size() >= 1) {
-                            CallsAndAlarmsDialogFragment.show(
+                            AudioSharingCallAudioDialogFragment.show(
                                     mFragment,
                                     mDeviceItemsInSharingSession,
                                     (AudioSharingDeviceItem item) -> {
-                                        if (!mGroupedConnectedDevices.containsKey(
-                                                item.getGroupId())) {
-                                            return;
-                                        }
                                         List<CachedBluetoothDevice> devices =
-                                                mGroupedConnectedDevices.get(item.getGroupId());
+                                                mGroupedConnectedDevices.getOrDefault(
+                                                        item.getGroupId(), ImmutableList.of());
                                         @Nullable
                                         CachedBluetoothDevice lead =
                                                 AudioSharingUtils.getLeadDevice(devices);
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
index 275d197..c3248c7 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java
@@ -33,7 +33,7 @@
     SettingsMainSwitchBar mMainSwitchBar;
     private AudioSharingSwitchBarController mSwitchBarController;
     private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController;
-    private CallsAndAlarmsPreferenceController mCallsAndAlarmsPreferenceController;
+    private AudioSharingCallAudioPreferenceController mAudioSharingCallAudioPreferenceController;
     private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController;
     private AudioStreamsCategoryController mAudioStreamsCategoryController;
 
@@ -67,8 +67,9 @@
         mAudioSharingDeviceVolumeGroupController =
                 use(AudioSharingDeviceVolumeGroupController.class);
         mAudioSharingDeviceVolumeGroupController.init(this);
-        mCallsAndAlarmsPreferenceController = use(CallsAndAlarmsPreferenceController.class);
-        mCallsAndAlarmsPreferenceController.init(this);
+        mAudioSharingCallAudioPreferenceController =
+                use(AudioSharingCallAudioPreferenceController.class);
+        mAudioSharingCallAudioPreferenceController.init(this);
         mAudioSharingPlaySoundPreferenceController =
                 use(AudioSharingPlaySoundPreferenceController.class);
         mAudioStreamsCategoryController = use(AudioStreamsCategoryController.class);
@@ -100,7 +101,7 @@
 
     private void updateVisibilityForAttachedPreferences() {
         mAudioSharingDeviceVolumeGroupController.updateVisibility();
-        mCallsAndAlarmsPreferenceController.updateVisibility();
+        mAudioSharingCallAudioPreferenceController.updateVisibility();
         mAudioSharingPlaySoundPreferenceController.updateVisibility();
         mAudioStreamsCategoryController.updateVisibility();
     }
diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java
index 2e4930c..ea5eede 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamMediaService.java
@@ -42,6 +42,7 @@
 import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 import com.android.settingslib.bluetooth.BluetoothCallback;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.bluetooth.VolumeControlProfile;
@@ -60,6 +61,7 @@
     private static final String LEAVE_BROADCAST_ACTION = "leave_broadcast_action";
     private static final String LEAVE_BROADCAST_TEXT = "Leave Broadcast";
     private static final String CHANNEL_ID = "bluetooth_notification_channel";
+    private static final String DEFAULT_DEVICE_NAME = "";
     private static final int STATIC_PLAYBACK_DURATION = 100;
     private static final int STATIC_PLAYBACK_POSITION = 30;
     private static final int ZERO_PLAYBACK_SPEED = 0;
@@ -355,16 +357,34 @@
         return mIsMuted ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build();
     }
 
+    private String getDeviceName() {
+        if (mDevices == null || mDevices.isEmpty() || mLocalBtManager == null) {
+            return DEFAULT_DEVICE_NAME;
+        }
+
+        CachedBluetoothDeviceManager manager = mLocalBtManager.getCachedDeviceManager();
+        if (manager == null) {
+            return DEFAULT_DEVICE_NAME;
+        }
+
+        CachedBluetoothDevice device = manager.findDevice(mDevices.get(0));
+        return device != null ? device.getName() : DEFAULT_DEVICE_NAME;
+    }
+
     private Notification buildNotification() {
+        String deviceName = getDeviceName();
+        Notification.MediaStyle mediaStyle =
+                new Notification.MediaStyle()
+                        .setMediaSession(
+                                mLocalSession != null ? mLocalSession.getSessionToken() : null);
+        if (deviceName != null && !deviceName.isEmpty()) {
+            mediaStyle.setRemotePlaybackInfo(
+                    deviceName, com.android.internal.R.drawable.ic_bt_headset_hfp, null);
+        }
         Notification.Builder notificationBuilder =
                 new Notification.Builder(this, CHANNEL_ID)
                         .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
-                        .setStyle(
-                                new Notification.MediaStyle()
-                                        .setMediaSession(
-                                                mLocalSession != null
-                                                        ? mLocalSession.getSessionToken()
-                                                        : null))
+                        .setStyle(mediaStyle)
                         .setContentText(this.getString(BROADCAST_CONTENT_TEXT))
                         .setSilent(true);
         return notificationBuilder.build();
diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java
index 7c601c0..1c14712 100644
--- a/src/com/android/settings/core/gateway/SettingsGateway.java
+++ b/src/com/android/settings/core/gateway/SettingsGateway.java
@@ -86,6 +86,7 @@
 import com.android.settings.bugreporthandler.BugReportHandlerPicker;
 import com.android.settings.communal.CommunalDashboardFragment;
 import com.android.settings.connecteddevice.AdvancedConnectedDeviceDashboardFragment;
+import com.android.settings.connecteddevice.BluetoothDashboardFragment;
 import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment;
 import com.android.settings.connecteddevice.NfcAndPaymentFragment;
 import com.android.settings.connecteddevice.PreviouslyConnectedDeviceDashboardFragment;
@@ -213,6 +214,7 @@
             AdvancedConnectedDeviceDashboardFragment.class.getName(),
             CreateShortcut.class.getName(),
             BluetoothPairingDetail.class.getName(),
+            BluetoothDashboardFragment.class.getName(),
             WifiNetworkDetailsFragment.class.getName(),
             ConfigureWifiSettings.class.getName(),
             SavedAccessPointsWifiSettings2.class.getName(),
diff --git a/src/com/android/settings/datausage/DataUsageSummaryPreference.java b/src/com/android/settings/datausage/DataUsageSummaryPreference.java
index 93d930c..6500501 100644
--- a/src/com/android/settings/datausage/DataUsageSummaryPreference.java
+++ b/src/com/android/settings/datausage/DataUsageSummaryPreference.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.graphics.Typeface;
 import android.icu.text.MessageFormat;
+import android.telephony.SubscriptionPlan;
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.TextUtils;
@@ -66,7 +67,7 @@
     @Nullable
     private Long mCycleEndTimeMs;
     /** The time of the last update in standard milliseconds since the epoch */
-    private long mSnapshotTimeMs;
+    private long mSnapshotTimeMs = SubscriptionPlan.TIME_UNKNOWN;
     /** Name of carrier, or null if not available */
     private CharSequence mCarrierName;
     private CharSequence mLimitInfoText;
diff --git a/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.kt b/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.kt
index 8b31f67..6f62c30 100644
--- a/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.kt
+++ b/src/com/android/settings/datausage/DataUsageSummaryPreferenceController.kt
@@ -21,20 +21,18 @@
 import android.net.NetworkTemplate
 import android.text.TextUtils
 import android.util.Log
-import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
 import androidx.preference.PreferenceScreen
 import com.android.settings.R
 import com.android.settings.datausage.lib.DataUsageLib.getMobileTemplate
 import com.android.settings.datausage.lib.INetworkCycleDataRepository
 import com.android.settings.datausage.lib.NetworkCycleDataRepository
 import com.android.settings.network.ProxySubscriptionManager
+import com.android.settings.network.policy.NetworkPolicyRepository
 import com.android.settings.network.telephony.TelephonyBasePreferenceController
+import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
 import kotlin.math.max
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 /**
@@ -47,6 +45,7 @@
     subId: Int,
     private val proxySubscriptionManager: ProxySubscriptionManager =
         ProxySubscriptionManager.getInstance(context),
+    private val networkPolicyRepository: NetworkPolicyRepository = NetworkPolicyRepository(context),
     private val networkCycleDataRepositoryFactory: (
         template: NetworkTemplate,
     ) -> INetworkCycleDataRepository = { NetworkCycleDataRepository(context, it) },
@@ -64,37 +63,37 @@
             proxySubscriptionManager.getAccessibleSubscriptionInfo(mSubId)
         } else null
     }
+
+    private val networkTemplate by lazy { getMobileTemplate(mContext, mSubId) }
+
     private val networkCycleDataRepository by lazy {
-        networkCycleDataRepositoryFactory(getMobileTemplate(mContext, mSubId))
+        networkCycleDataRepositoryFactory(networkTemplate)
     }
-    private val policy by lazy { networkCycleDataRepository.getPolicy() }
+
     private lateinit var preference: DataUsageSummaryPreference
 
     override fun getAvailabilityStatus(subId: Int) =
-        if (subInfo != null && policy != null) AVAILABLE else CONDITIONALLY_UNAVAILABLE
+        if (subInfo != null) AVAILABLE else CONDITIONALLY_UNAVAILABLE
 
     override fun displayPreference(screen: PreferenceScreen) {
         super.displayPreference(screen)
         preference = screen.findPreference(preferenceKey)!!
-        policy?.let {
-            preference.setLimitInfo(it.getLimitInfo())
-            val dataBarSize = max(it.limitBytes, it.warningBytes)
-            if (dataBarSize > NetworkPolicy.WARNING_DISABLED) {
-                setDataBarSize(dataBarSize)
-            }
-        }
     }
 
     override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
-        viewLifecycleOwner.lifecycleScope.launch {
-            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
-                update()
+        networkPolicyRepository.networkPolicyFlow(networkTemplate)
+            .collectLatestWithLifecycle(viewLifecycleOwner) { policy ->
+                preference.isVisible = subInfo != null && policy != null
+                if (policy != null) update(policy)
             }
-        }
     }
 
-    private suspend fun update() {
-        val policy = policy ?: return
+    private suspend fun update(policy: NetworkPolicy) {
+        preference.setLimitInfo(policy.getLimitInfo())
+        val dataBarSize = max(policy.limitBytes, policy.warningBytes)
+        if (dataBarSize > NetworkPolicy.WARNING_DISABLED) {
+            setDataBarSize(dataBarSize)
+        }
         val dataPlanInfo = withContext(Dispatchers.Default) {
             dataPlanRepositoryFactory(networkCycleDataRepository).getDataPlanInfo(
                 policy = policy,
diff --git a/src/com/android/settings/fuelgauge/BatterySettingsStorage.java b/src/com/android/settings/fuelgauge/BatterySettingsStorage.java
index 99edbec..4c8b9b8 100644
--- a/src/com/android/settings/fuelgauge/BatterySettingsStorage.java
+++ b/src/com/android/settings/fuelgauge/BatterySettingsStorage.java
@@ -57,7 +57,8 @@
 
 /** An implementation to backup and restore battery configurations. */
 public final class BatterySettingsStorage extends ObservableBackupRestoreStorage {
-    public static final String TAG = "BatteryBackupHelper";
+    private static final String NAME = "BatteryBackupHelper";
+    private static final String TAG = "BatterySettingsStorage";
 
     // Definition for the device build information.
     public static final String KEY_BUILD_BRAND = "device_build_brand";
@@ -89,7 +90,7 @@
      */
     public static @NonNull BatterySettingsStorage get(@NonNull Context context) {
         return (BatterySettingsStorage)
-                BackupRestoreStorageManager.getInstance(context).getOrThrow(TAG);
+                BackupRestoreStorageManager.getInstance(context).getOrThrow(NAME);
     }
 
     public BatterySettingsStorage(@NonNull Context context) {
@@ -99,7 +100,7 @@
     @NonNull
     @Override
     public String getName() {
-        return TAG;
+        return NAME;
     }
 
     @Override
diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java
index b53bf47..9e08664 100644
--- a/src/com/android/settings/fuelgauge/BatteryUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryUtils.java
@@ -393,7 +393,7 @@
                     packageName, uid, ActivityManager.RESTRICTION_LEVEL_BACKGROUND_RESTRICTED,
                     mode == AppOpsManager.MODE_IGNORED,
                     ActivityManager.RESTRICTION_REASON_USER,
-                    "settings", 0);
+                    "settings", ActivityManager.RESTRICTION_SOURCE_USER, 0L);
         }
         // Control whether app could run jobs in the background
         mAppOpsManager.setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName, mode);
diff --git a/src/com/android/settings/network/MobileDataEnabledFlow.kt b/src/com/android/settings/network/MobileDataEnabledFlow.kt
index 2342377..1f995a9 100644
--- a/src/com/android/settings/network/MobileDataEnabledFlow.kt
+++ b/src/com/android/settings/network/MobileDataEnabledFlow.kt
@@ -28,8 +28,8 @@
  *
  * Note: This flow can only notify enabled status changes, cannot provide the latest status.
  */
-fun Context.mobileDataEnabledFlow(subId: Int): Flow<Unit> {
-    val flow = settingsGlobalChangeFlow(Settings.Global.MOBILE_DATA)
+fun Context.mobileDataEnabledFlow(subId: Int, sendInitialValue: Boolean = true): Flow<Unit> {
+    val flow = settingsGlobalChangeFlow(Settings.Global.MOBILE_DATA, sendInitialValue)
     return when (subId) {
         SubscriptionManager.INVALID_SUBSCRIPTION_ID -> flow
         else -> {
diff --git a/src/com/android/settings/network/ProxySubscriptionManager.java b/src/com/android/settings/network/ProxySubscriptionManager.java
index 7e276e8..614491a 100644
--- a/src/com/android/settings/network/ProxySubscriptionManager.java
+++ b/src/com/android/settings/network/ProxySubscriptionManager.java
@@ -28,7 +28,6 @@
 import android.util.Log;
 
 import androidx.annotation.Keep;
-import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleObserver;
@@ -243,15 +242,6 @@
     }
 
     /**
-     * Gets a list of active, visible subscription Id(s) of the currently active SIM(s).
-     *
-     * @return the list of subId's that are active and visible; the length may be 0.
-     */
-    public @NonNull int[] getActiveSubscriptionIdList() {
-        return mSubscriptionMonitor.getActiveSubscriptionIdList();
-    }
-
-    /**
      * Clear data cached within proxy
      */
     public void clearCache() {
diff --git a/src/com/android/settings/network/UiccSlotUtil.java b/src/com/android/settings/network/UiccSlotUtil.java
index df23f12..f15808f 100644
--- a/src/com/android/settings/network/UiccSlotUtil.java
+++ b/src/com/android/settings/network/UiccSlotUtil.java
@@ -329,38 +329,6 @@
         return INVALID_PHYSICAL_SLOT_ID;
     }
 
-    // Device |                                        |Slot   |
-    // Working|                                        |Mapping|
-    // State  |Type                                    |Mode   |Friendly name
-    //--------------------------------------------------------------------------
-    // Single |SIM pSIM [RIL 0]                        |1      |pSIM active
-    // Single |SIM MEP Port #0 [RIL0]                  |2      |eSIM Port0 active
-    // Single |SIM MEP Port #1 [RIL0]                  |2.1    |eSIM Port1 active
-    // DSDS   |pSIM [RIL 0] + MEP Port #0 [RIL 1]      |3      |pSIM+Port0
-    // DSDS   |pSIM [RIL 0] + MEP Port #1 [RIL 1]      |3.1    |pSIM+Port1
-    // DSDS   |MEP Port #0 [RIL 0] + MEP Port #1 [RIL1]|3.2    |Dual-Ports-A
-    // DSDS   |MEP Port #1 [RIL 0] + MEP Port #0 [RIL1]|4      |Dual-Ports-B
-    //
-    // The rules are:
-    // 1. pSIM's logical slots always is [RIL 0].
-    // 2. assign the new active port to the same stack that will be de-activated
-    //    For example: mode#3->mode#4
-    // 3. Add an eSIM carrier or enable eSIM carrier. The cases are at the below.
-    //    1) 1   => 2 / 2.1 / 3 / 3.1
-    //    2) 2   => 1 / 3 / 3.2
-    //    3) 2.1 => 3.1 / 4
-    //    4) 3   => 4
-    //    5) 3.1 => 3.2
-    //    Note:
-    //        1) 2 <=> 2.1  blocked by LPA (reason: existing active port in SS so just re-use)
-    //        2) 3 <=> 3.1 blocked by LPA (reason: if pSIM+an active port, re-use the active port)
-    // 4. pSIM insertion or enabling
-    //     1) 2   => 1 / 3
-    //     2) 2.1 => 1 / 3.1
-    //     3) 3.2 => 3 / 3.1
-    //     4) 4   => 3 / 3.1
-
-
     @VisibleForTesting
     static Collection<UiccSlotMapping> prepareUiccSlotMappings(
             Collection<UiccSlotMapping> uiccSlotMappings, boolean isPsim, int physicalSlotId,
diff --git a/src/com/android/settings/network/apn/ApnStatus.kt b/src/com/android/settings/network/apn/ApnStatus.kt
index aa816fc..2f41f23 100644
--- a/src/com/android/settings/network/apn/ApnStatus.kt
+++ b/src/com/android/settings/network/apn/ApnStatus.kt
@@ -174,20 +174,13 @@
  * @return An error message if the apn data is invalid, otherwise return null.
  */
 fun validateApnData(apnData: ApnData, context: Context): String? {
-    var errorMsg: String?
-    val name = apnData.name
-    val apn = apnData.apn
-    errorMsg = if (name == "") {
-        context.resources.getString(R.string.error_name_empty)
-    } else if (apn == "") {
-        context.resources.getString(R.string.error_apn_empty)
-    } else {
-        validateMMSC(true, apnData.mmsc, context)
+    val errorMsg: String? = when {
+        apnData.name.isEmpty() -> context.resources.getString(R.string.error_name_empty)
+        apnData.apn.isEmpty() -> context.resources.getString(R.string.error_apn_empty)
+        apnData.apnType.isEmpty() -> context.resources.getString(R.string.error_apn_type_empty)
+        else -> validateMMSC(true, apnData.mmsc, context) ?: isItemExist(apnData, context)
     }
-    if (errorMsg == null) {
-        errorMsg = isItemExist(apnData, context)
-    }
-    return errorMsg?.apply { Log.d(TAG, "APN data not valid, reason: $this") }
+    return errorMsg?.also { Log.d(TAG, "APN data not valid, reason: $it") }
 }
 
 /**
diff --git a/src/com/android/settings/network/apn/ApnTypeCheckBox.kt b/src/com/android/settings/network/apn/ApnTypeCheckBox.kt
index aa757cc..f4e73fb 100644
--- a/src/com/android/settings/network/apn/ApnTypeCheckBox.kt
+++ b/src/com/android/settings/network/apn/ApnTypeCheckBox.kt
@@ -24,6 +24,7 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import com.android.settings.R
+import com.android.settings.network.apn.ApnTypes.isValid
 import com.android.settings.network.apn.ApnTypes.toApnType
 import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckBox
 
@@ -47,6 +48,8 @@
         label = stringResource(R.string.apn_type),
         options = apnTypeOptions,
         enabled = apnData.isFieldEnabled(Telephony.Carriers.TYPE),
+        errorMessage = stringResource(R.string.error_apn_type_empty)
+            .takeUnless { apnTypeOptions.isValid() },
     ) {
         onTypeChanged(apnTypeOptions.toApnType())
         updateMmsSelected()
diff --git a/src/com/android/settings/network/apn/ApnTypes.kt b/src/com/android/settings/network/apn/ApnTypes.kt
index d384625..b9bc480 100644
--- a/src/com/android/settings/network/apn/ApnTypes.kt
+++ b/src/com/android/settings/network/apn/ApnTypes.kt
@@ -101,6 +101,8 @@
         }
     }
 
+    fun List<SettingsDropdownCheckOption>.isValid(): Boolean = any { it.selected.value }
+
     fun List<SettingsDropdownCheckOption>.toApnType(): String {
         val (selectAllOptions, regularOptions) = partition { it.isSelectAll }
         for (selectAllOption in selectAllOptions) {
diff --git a/src/com/android/settings/network/policy/NetworkPolicyRepository.kt b/src/com/android/settings/network/policy/NetworkPolicyRepository.kt
new file mode 100644
index 0000000..a0cab14
--- /dev/null
+++ b/src/com/android/settings/network/policy/NetworkPolicyRepository.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.network.policy
+
+import android.content.Context
+import android.net.NetworkPolicy
+import android.net.NetworkPolicyManager
+import android.net.NetworkTemplate
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+
+class NetworkPolicyRepository(context: Context) {
+    private val networkPolicyManager = context.getSystemService(NetworkPolicyManager::class.java)!!
+
+    fun getNetworkPolicy(networkTemplate: NetworkTemplate): NetworkPolicy? =
+        networkPolicyManager.networkPolicies.find { policy -> policy.template == networkTemplate }
+
+    fun networkPolicyFlow(networkTemplate: NetworkTemplate): Flow<NetworkPolicy?> = flow {
+        emit(getNetworkPolicy(networkTemplate))
+    }.flowOn(Dispatchers.Default)
+}
diff --git a/src/com/android/settings/network/telephony/AutoDataSwitchPreferenceController.java b/src/com/android/settings/network/telephony/AutoDataSwitchPreferenceController.java
index ef74a2e..fcbfdef 100644
--- a/src/com/android/settings/network/telephony/AutoDataSwitchPreferenceController.java
+++ b/src/com/android/settings/network/telephony/AutoDataSwitchPreferenceController.java
@@ -19,20 +19,14 @@
 import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
 import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
 
-import android.app.settings.SettingsEnums;
 import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.PersistableBundle;
-import android.telephony.CarrierConfigManager;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
-import android.telephony.ims.ImsException;
-import android.telephony.ims.ImsManager;
-import android.telephony.ims.ImsMmTelManager;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.lifecycle.LifecycleObserver;
 import androidx.lifecycle.OnLifecycleEvent;
 import androidx.preference.Preference;
@@ -40,15 +34,11 @@
 import androidx.preference.TwoStatePreference;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.settings.R;
 import com.android.settings.datausage.DataUsageUtils;
 import com.android.settings.flags.Flags;
 import com.android.settings.network.MobileDataContentObserver;
-import com.android.settings.network.ProxySubscriptionManager;
 import com.android.settings.network.SubscriptionsChangeListener;
-import com.android.settings.network.ims.WifiCallingQueryImsState;
-import com.android.settings.overlay.FeatureFactory;
-import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
+import com.android.settings.network.telephony.wificalling.CrossSimCallingViewModel;
 
 /**
  * Controls whether switch mobile data to the non-default SIM if the non-default SIM has better
@@ -63,25 +53,29 @@
 public class AutoDataSwitchPreferenceController extends TelephonyTogglePreferenceController
         implements LifecycleObserver,
         SubscriptionsChangeListener.SubscriptionsChangeListenerClient {
-    private static final String LOG_TAG = "AutoDataSwitchPrefCtrl";
 
+    @Nullable
     private TwoStatePreference mPreference;
+    @Nullable
     private SubscriptionsChangeListener mChangeListener;
+    @Nullable
     private TelephonyManager mManager;
+    @Nullable
     private MobileDataContentObserver mMobileDataContentObserver;
+    @Nullable
+    private CrossSimCallingViewModel mCrossSimCallingViewModel;
+    @Nullable
     private PreferenceScreen mScreen;
 
-    private final MetricsFeatureProvider mMetricsFeatureProvider;
-
-    public AutoDataSwitchPreferenceController(Context context,
-            String preferenceKey) {
+    public AutoDataSwitchPreferenceController(
+            @NonNull Context context, @NonNull String preferenceKey) {
         super(context, preferenceKey);
-        mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
     }
 
-    void init(int subId) {
+    void init(int subId, @Nullable CrossSimCallingViewModel crossSimCallingViewModel) {
         this.mSubId = subId;
         mManager = mContext.getSystemService(TelephonyManager.class).createForSubscriptionId(subId);
+        mCrossSimCallingViewModel = crossSimCallingViewModel;
     }
 
     @OnLifecycleEvent(ON_RESUME)
@@ -121,35 +115,15 @@
                 TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH);
     }
 
-    private int getOtherSubId(@NonNull int[] subIds) {
-        if (subIds.length > 1) {
-            for (int subId : subIds) {
-                if (subId != mSubId) {
-                    return subId;
-                }
-            }
-        }
-        return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
-    }
-
-    private boolean isEnabled(int subId) {
-        if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-            return false;
-        }
-        TelephonyManager telephonyManager = mContext.getSystemService(
-                TelephonyManager.class).createForSubscriptionId(subId);
-        return telephonyManager != null && telephonyManager.isMobileDataPolicyEnabled(
-                        TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH);
-    }
-
     @Override
     public boolean setChecked(boolean isChecked) {
-        mManager.setMobileDataPolicyEnabled(
-                TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH,
-                isChecked);
-        if (mContext.getResources().getBoolean(
-                R.bool.config_auto_data_switch_enables_cross_sim_calling)) {
-            trySetCrossSimCalling(mContext, getActiveSubscriptionIdList(), isChecked /* enabled */);
+        if (mManager != null) {
+            mManager.setMobileDataPolicyEnabled(
+                    TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH,
+                    isChecked);
+        }
+        if (mCrossSimCallingViewModel != null) {
+            mCrossSimCallingViewModel.updateCrossSimCalling();
         }
         return true;
     }
@@ -159,40 +133,6 @@
         return DataUsageUtils.hasMobileData(mContext);
     }
 
-    private boolean isCrossSimCallingAllowedByPlatform(Context context, int subId) {
-        if ((new WifiCallingQueryImsState(context, subId)).isWifiCallingSupported()) {
-            PersistableBundle bundle = getCarrierConfigForSubId(subId);
-            return (bundle != null) && bundle.getBoolean(
-                    CarrierConfigManager.KEY_CARRIER_CROSS_SIM_IMS_AVAILABLE_BOOL,
-                    false /*default*/);
-        }
-        return false;
-    }
-
-    protected ImsMmTelManager getImsMmTelManager(Context context, int subId) {
-        ImsManager imsMgr = context.getSystemService(ImsManager.class);
-        return (imsMgr == null) ? null : imsMgr.getImsMmTelManager(subId);
-    }
-
-    private void trySetCrossSimCallingPerSub(Context context, int subId, boolean enabled) {
-        try {
-            getImsMmTelManager(context, subId).setCrossSimCallingEnabled(enabled);
-        } catch (ImsException | IllegalArgumentException | NullPointerException exception) {
-            Log.w(LOG_TAG, "failed to change cross SIM calling configuration to " + enabled
-                    + " for subID " + subId + "with exception: ", exception);
-        }
-    }
-
-    private void trySetCrossSimCalling(Context context, int[] subIds, boolean enabled) {
-        mMetricsFeatureProvider.action(mContext,
-                SettingsEnums.ACTION_UPDATE_CROSS_SIM_CALLING_ON_AUTO_DATA_SWITCH_EVENT, enabled);
-        for (int subId : subIds) {
-            if (isCrossSimCallingAllowedByPlatform(context, subId)) {
-                trySetCrossSimCallingPerSub(context, subId, enabled);
-            }
-        }
-    }
-
     @Override
     public int getAvailabilityStatus(int subId) {
         if (Flags.isDualSimOnboardingEnabled()
@@ -221,20 +161,11 @@
         updateState(mPreference);
     }
 
-    private int[] getActiveSubscriptionIdList() {
-        return ProxySubscriptionManager.getInstance(mContext).getActiveSubscriptionIdList();
-    }
-
     /**
      * Trigger displaying preference when Mobile data content changed.
      */
     @VisibleForTesting
     public void refreshPreference() {
-        if (mContext.getResources().getBoolean(
-                R.bool.config_auto_data_switch_enables_cross_sim_calling)) {
-            int[] subIds = getActiveSubscriptionIdList();
-            trySetCrossSimCalling(mContext, subIds, isEnabled(getOtherSubId(subIds)));
-        }
         if (mScreen != null) {
             super.displayPreference(mScreen);
         }
diff --git a/src/com/android/settings/network/telephony/MobileNetworkImeiPreferenceController.kt b/src/com/android/settings/network/telephony/MobileNetworkImeiPreferenceController.kt
index e134681..a6fb7ba 100644
--- a/src/com/android/settings/network/telephony/MobileNetworkImeiPreferenceController.kt
+++ b/src/com/android/settings/network/telephony/MobileNetworkImeiPreferenceController.kt
@@ -17,7 +17,6 @@
 package com.android.settings.network.telephony
 
 import android.content.Context
-import android.os.UserManager
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
 import android.telephony.TelephonyManager
@@ -41,6 +40,7 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
+
 /**
  * Preference controller for "IMEI"
  */
@@ -123,17 +123,21 @@
         ImeiInfoDialogFragment.show(fragment, simSlot, preference.title.toString())
         return true
     }
+
     private fun getImei(): String {
         val phoneType = getPhoneType()
         return if (phoneType == TelephonyManager.PHONE_TYPE_CDMA) mTelephonyManager.meid?: String()
                 else mTelephonyManager.imei?: String()
     }
+
     private fun getTitleForGsmPhone(): String {
-        return mContext.getString(R.string.status_imei)
+        return mContext.getString(
+            if (isPrimaryImei()) R.string.imei_primary else R.string.status_imei)
     }
 
     private fun getTitleForCdmaPhone(): String {
-        return mContext.getString(R.string.status_meid_number)
+        return mContext.getString(
+            if (isPrimaryImei()) R.string.meid_primary else R.string.status_meid_number)
     }
 
     private fun getTitle(): String {
@@ -142,6 +146,28 @@
                 else getTitleForGsmPhone()
     }
 
+    /**
+     * As per GSMA specification TS37, below Primary IMEI requirements are mandatory to support
+     *
+     * TS37_2.2_REQ_5
+     * TS37_2.2_REQ_8 (Attached the document has description about this test cases)
+     */
+    protected fun isPrimaryImei(): Boolean {
+        val imei = getImei()
+        var primaryImei = String()
+
+        try {
+            primaryImei = mTelephonyManager.primaryImei
+        } catch (exception: Exception) {
+            Log.e(TAG, "PrimaryImei not available. $exception")
+        }
+        return primaryImei == imei && isMultiSim()
+    }
+
+    private fun isMultiSim(): Boolean {
+        return mTelephonyManager.activeModemCount > 1
+    }
+
     fun getPhoneType(): Int {
         return mTelephonyManager.currentPhoneType
     }
diff --git a/src/com/android/settings/network/telephony/MobileNetworkSettings.java b/src/com/android/settings/network/telephony/MobileNetworkSettings.java
index d57de1a..c9a2a0b 100644
--- a/src/com/android/settings/network/telephony/MobileNetworkSettings.java
+++ b/src/com/android/settings/network/telephony/MobileNetworkSettings.java
@@ -38,6 +38,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.ViewModelProvider;
 import androidx.preference.Preference;
 
 import com.android.settings.R;
@@ -53,6 +54,7 @@
 import com.android.settings.network.telephony.cdma.CdmaSystemSelectPreferenceController;
 import com.android.settings.network.telephony.gsm.AutoSelectPreferenceController;
 import com.android.settings.network.telephony.gsm.OpenNetworkSelectPagePreferenceController;
+import com.android.settings.network.telephony.wificalling.CrossSimCallingViewModel;
 import com.android.settings.search.BaseSearchIndexProvider;
 import com.android.settings.wifi.WifiPickerTrackerHelper;
 import com.android.settingslib.core.AbstractPreferenceController;
@@ -240,7 +242,9 @@
         use(CarrierSettingsVersionPreferenceController.class).init(mSubId);
         use(BillingCyclePreferenceController.class).init(mSubId);
         use(MmsMessagePreferenceController.class).init(mSubId);
-        use(AutoDataSwitchPreferenceController.class).init(mSubId);
+        final var crossSimCallingViewModel =
+                new ViewModelProvider(this).get(CrossSimCallingViewModel.class);
+        use(AutoDataSwitchPreferenceController.class).init(mSubId, crossSimCallingViewModel);
         use(DisabledSubscriptionController.class).init(mSubId);
         use(DeleteSimProfilePreferenceController.class).init(mSubId);
         use(DisableSimFooterPreferenceController.class).init(mSubId);
diff --git a/src/com/android/settings/network/telephony/SubscriptionRepository.kt b/src/com/android/settings/network/telephony/SubscriptionRepository.kt
index 4fd28a2..3ee8548 100644
--- a/src/com/android/settings/network/telephony/SubscriptionRepository.kt
+++ b/src/com/android/settings/network/telephony/SubscriptionRepository.kt
@@ -124,6 +124,14 @@
             }
         }
         // Matching the sorting order in SubscriptionManagerService.getAvailableSubscriptionInfoList
-        .sortedWith(compareBy({ it.simSlotIndex }, { it.subscriptionId }))
+        .sortedWith(compareBy({ it.sortableSimSlotIndex }, { it.subscriptionId }))
         .also { Log.d(TAG, "getSelectableSubscriptionInfoList: $it") }
 }
+
+/** Subscription with invalid sim slot index has lowest sort order. */
+private val SubscriptionInfo.sortableSimSlotIndex: Int
+    get() = if (simSlotIndex != SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
+        simSlotIndex
+    } else {
+        Int.MAX_VALUE
+    }
diff --git a/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt b/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt
index dd1cbd5..9bc10e5 100644
--- a/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt
+++ b/src/com/android/settings/network/telephony/ims/ImsMmTelRepository.kt
@@ -51,6 +51,8 @@
         @MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int,
         @AccessNetworkConstants.TransportType transportType: Int,
     ): Boolean
+
+    suspend fun setCrossSimCallingEnabled(enabled: Boolean)
 }
 
 class ImsMmTelRepositoryImpl(
@@ -130,6 +132,7 @@
         @MmTelFeature.MmTelCapabilities.MmTelCapability capability: Int,
         @AccessNetworkConstants.TransportType transportType: Int,
     ): Boolean = withContext(Dispatchers.Default) {
+        val logName = "isSupported(capability=$capability,transportType=$transportType)"
         suspendCancellableCoroutine { continuation ->
             try {
                 imsMmTelManager.isSupported(
@@ -140,9 +143,18 @@
                 )
             } catch (e: Exception) {
                 continuation.resume(false)
-                Log.w(TAG, "[$subId] isSupported failed", e)
+                Log.w(TAG, "[$subId] $logName failed", e)
             }
-        }.also { Log.d(TAG, "[$subId] isSupported = $it") }
+        }.also { Log.d(TAG, "[$subId] $logName = $it") }
+    }
+
+    override suspend fun setCrossSimCallingEnabled(enabled: Boolean) {
+        try {
+            imsMmTelManager.setCrossSimCallingEnabled(enabled)
+            Log.d(TAG, "[$subId] setCrossSimCallingEnabled: $enabled")
+        } catch (e: Exception) {
+            Log.e(TAG, "[$subId] failed setCrossSimCallingEnabled to $enabled", e)
+        }
     }
 
     private companion object {
diff --git a/src/com/android/settings/network/telephony/wificalling/CrossSimCallingViewModel.kt b/src/com/android/settings/network/telephony/wificalling/CrossSimCallingViewModel.kt
new file mode 100644
index 0000000..5dcac1e
--- /dev/null
+++ b/src/com/android/settings/network/telephony/wificalling/CrossSimCallingViewModel.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.network.telephony.wificalling
+
+import android.app.Application
+import android.app.settings.SettingsEnums
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionManager
+import android.telephony.TelephonyManager
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import com.android.settings.R
+import com.android.settings.network.mobileDataEnabledFlow
+import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl
+import com.android.settings.network.telephony.requireSubscriptionManager
+import com.android.settings.network.telephony.safeGetConfig
+import com.android.settings.network.telephony.subscriptionsChangedFlow
+import com.android.settings.network.telephony.telephonyManager
+import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.plus
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class CrossSimCallingViewModel(
+    private val application: Application,
+) : AndroidViewModel(application) {
+
+    private val subscriptionManager = application.requireSubscriptionManager()
+    private val carrierConfigManager =
+        application.getSystemService(CarrierConfigManager::class.java)!!
+    private val scope = viewModelScope + Dispatchers.Default
+    private val metricsFeatureProvider = featureFactory.metricsFeatureProvider
+    private val updateChannel = Channel<Unit>()
+
+    init {
+        val resources = application.resources
+        if (resources.getBoolean(R.bool.config_auto_data_switch_enables_cross_sim_calling)) {
+            application.subscriptionsChangedFlow()
+                .flatMapLatest {
+                    val activeSubIds = subscriptionManager.activeSubscriptionIdList.toList()
+                    merge(
+                        activeSubIds.anyMobileDataEnableChangedFlow(),
+                        updateChannel.receiveAsFlow(),
+                    ).map {
+                        activeSubIds to crossSimCallNewEnabled(activeSubIds)
+                    }
+                }
+                .distinctUntilChanged()
+                .onEach { (activeSubIds, newEnabled) ->
+                    updateCrossSimCalling(activeSubIds, newEnabled)
+                }
+                .launchIn(scope)
+        }
+    }
+
+    fun updateCrossSimCalling() {
+        updateChannel.trySend(Unit)
+    }
+
+    private fun List<Int>.anyMobileDataEnableChangedFlow() = map { subId ->
+        application.mobileDataEnabledFlow(subId = subId, sendInitialValue = false)
+    }.merge()
+
+    private suspend fun updateCrossSimCalling(activeSubIds: List<Int>, newEnabled: Boolean) {
+        metricsFeatureProvider.action(
+            application,
+            SettingsEnums.ACTION_UPDATE_CROSS_SIM_CALLING_ON_AUTO_DATA_SWITCH_EVENT,
+            newEnabled,
+        )
+        activeSubIds.filter { crossSimAvailable(it) }.forEach { subId ->
+            ImsMmTelRepositoryImpl(application, subId)
+                .setCrossSimCallingEnabled(newEnabled)
+        }
+    }
+
+    private suspend fun crossSimAvailable(subId: Int): Boolean =
+        WifiCallingRepository(application, subId).isWifiCallingSupported() &&
+            crossSimImsAvailable(subId)
+
+    private fun crossSimImsAvailable(subId: Int): Boolean =
+        carrierConfigManager.safeGetConfig(
+            keys = listOf(CarrierConfigManager.KEY_CARRIER_CROSS_SIM_IMS_AVAILABLE_BOOL),
+            subId = subId,
+        ).getBoolean(CarrierConfigManager.KEY_CARRIER_CROSS_SIM_IMS_AVAILABLE_BOOL, false)
+
+    private fun crossSimCallNewEnabled(activeSubscriptionIdList: List<Int>): Boolean {
+        val defaultDataSubId = SubscriptionManager.getDefaultDataSubscriptionId()
+        return SubscriptionManager.isValidSubscriptionId(defaultDataSubId) &&
+            activeSubscriptionIdList.any { subId ->
+                subId != defaultDataSubId &&
+                    application.telephonyManager(subId).isMobileDataPolicyEnabled(
+                        TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH
+                    )
+            }
+    }
+}
diff --git a/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt b/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt
index ac95404..a5d4ba8 100644
--- a/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt
+++ b/src/com/android/settings/network/telephony/wificalling/WifiCallingRepository.kt
@@ -29,17 +29,19 @@
 import com.android.settings.network.telephony.ims.ImsMmTelRepositoryImpl
 import com.android.settings.network.telephony.ims.imsFeatureProvisionedFlow
 import com.android.settings.network.telephony.subscriptionsChangedFlow
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
 
 class WifiCallingRepository(
     private val context: Context,
     private val subId: Int,
-    private val imsMmTelRepository : ImsMmTelRepository = ImsMmTelRepositoryImpl(context, subId)
+    private val imsMmTelRepository: ImsMmTelRepository = ImsMmTelRepositoryImpl(context, subId)
 ) {
     private val telephonyManager = context.getSystemService(TelephonyManager::class.java)!!
         .createForSubscriptionId(subId)
@@ -76,10 +78,14 @@
 
     private fun isWifiCallingSupportedFlow(): Flow<Boolean> {
         return imsMmTelRepository.imsReadyFlow().map { imsReady ->
-            imsReady && imsMmTelRepository.isSupported(
-                capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
-                transportType = AccessNetworkConstants.TRANSPORT_TYPE_WLAN,
-            )
+            imsReady && isWifiCallingSupported()
         }
     }
+
+    suspend fun isWifiCallingSupported(): Boolean = withContext(Dispatchers.Default) {
+        imsMmTelRepository.isSupported(
+            capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
+            transportType = AccessNetworkConstants.TRANSPORT_TYPE_WLAN,
+        )
+    }
 }
diff --git a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
index b5e5dc7..587f640 100644
--- a/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
+++ b/src/com/android/settings/notification/modes/AbstractZenModePreferenceController.java
@@ -16,9 +16,10 @@
 
 package com.android.settings.notification.modes;
 
-import android.app.AutomaticZenRule;
 import android.app.Flags;
 import android.content.Context;
+import android.service.notification.ZenPolicy;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -26,11 +27,17 @@
 
 import com.android.settingslib.core.AbstractPreferenceController;
 
+import com.google.common.base.Preconditions;
+
+import java.util.function.Function;
+
 /**
  * Base class for any preference controllers pertaining to any single Zen mode.
  */
 abstract class AbstractZenModePreferenceController extends AbstractPreferenceController {
 
+    private static final String TAG = "AbstractZenModePreferenceController";
+
     @Nullable
     protected ZenModesBackend mBackend;
 
@@ -38,7 +45,7 @@
     private ZenMode mZenMode;
 
     @NonNull
-    final String mKey;
+    private final String mKey;
 
     // ZenModesBackend should only be passed in if the preference controller may set the user's
     // policy for this zen mode. Otherwise, if the preference controller is essentially read-only
@@ -67,20 +74,56 @@
         updateState(preference);
     }
 
-    @Nullable
-    public ZenMode getMode() {
-        return mZenMode;
+    @Override
+    public final void updateState(Preference preference) {
+        super.updateState(preference);
+        if (mZenMode != null) {
+            updateState(preference, mZenMode);
+        }
     }
 
-    @Nullable
-    public AutomaticZenRule getAZR() {
-        if (mZenMode == null || mZenMode.getRule() == null) {
+    abstract void updateState(Preference preference, @NonNull ZenMode zenMode);
+
+    @Override
+    public final CharSequence getSummary() {
+        if (mZenMode != null) {
+            return getSummary(mZenMode);
+        } else {
             return null;
         }
-        return mZenMode.getRule();
     }
 
-    /** Implementations of this class should override
-     *  {@link AbstractPreferenceController#updateState(Preference)} to specify what should
-     *  happen when the preference is updated */
+    @Nullable
+    protected CharSequence getSummary(@NonNull ZenMode zenMode) {
+        return null;
+    }
+
+    /**
+     * Subclasses should call this method (or a more specific one, like {@link #savePolicy} from
+     * their {@code onPreferenceChange()} or similar, in order to apply changes to the mode being
+     * edited (e.g. {@code saveMode(mode -> { mode.setX(value); return mode; } }.
+     *
+     * @param updater Function to update the {@link ZenMode}. Modifying and returning the same
+     *                instance is ok.
+     */
+    protected final boolean saveMode(Function<ZenMode, ZenMode> updater) {
+        Preconditions.checkState(mBackend != null);
+        ZenMode mode = mZenMode;
+        if (mode == null) {
+            Log.wtf(TAG, "Cannot save mode, it hasn't been loaded (" + getClass() + ")");
+            return false;
+        }
+        mode = updater.apply(mode);
+        mBackend.updateMode(mode);
+        return true;
+    }
+
+    protected final boolean savePolicy(Function<ZenPolicy.Builder, ZenPolicy.Builder> updater) {
+        return saveMode(mode -> {
+            ZenPolicy.Builder policyBuilder = new ZenPolicy.Builder(mode.getPolicy());
+            policyBuilder = updater.apply(policyBuilder);
+            mode.setPolicy(policyBuilder.build());
+            return mode;
+        });
+    }
 }
diff --git a/src/com/android/settings/notification/modes/ZenMode.java b/src/com/android/settings/notification/modes/ZenMode.java
index ca9bec5..058799b 100644
--- a/src/com/android/settings/notification/modes/ZenMode.java
+++ b/src/com/android/settings/notification/modes/ZenMode.java
@@ -26,6 +26,7 @@
 import android.app.NotificationManager;
 import android.content.Context;
 import android.graphics.drawable.Drawable;
+import android.service.notification.ZenDeviceEffects;
 import android.service.notification.ZenPolicy;
 import android.util.Log;
 
@@ -87,7 +88,7 @@
                     .build();
 
     private final String mId;
-    private final AutomaticZenRule mRule;
+    private AutomaticZenRule mRule;
     private final boolean mIsActive;
     private final boolean mIsManualDnd;
 
@@ -196,6 +197,13 @@
         mRule.setZenPolicy(policy);
     }
 
+    @NonNull
+    public ZenDeviceEffects getDeviceEffects() {
+        return mRule.getDeviceEffects() != null
+                ? mRule.getDeviceEffects()
+                : new ZenDeviceEffects.Builder().build();
+    }
+
     public boolean canBeDeleted() {
         return !mIsManualDnd;
     }
diff --git a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java
index 1d1d750..746af44 100644
--- a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java
@@ -20,12 +20,15 @@
 
 import android.content.Context;
 import android.os.Bundle;
+
+import androidx.annotation.NonNull;
 import androidx.preference.Preference;
+
 import com.android.settings.core.SubSettingLauncher;
 
-public class ZenModeCallsLinkPreferenceController extends AbstractZenModePreferenceController  {
+class ZenModeCallsLinkPreferenceController extends AbstractZenModePreferenceController  {
 
-    private ZenModeSummaryHelper mSummaryHelper;
+    private final ZenModeSummaryHelper mSummaryHelper;
 
     public ZenModeCallsLinkPreferenceController(Context context, String key,
             ZenModesBackend backend) {
@@ -34,16 +37,15 @@
     }
 
     @Override
-    public void updateState(Preference preference) {
-        super.updateState(preference);
+    public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, getMode().getId());
+        bundle.putString(MODE_ID, zenMode.getId());
         // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeCallsFragment.class.getName())
                 .setSourceMetricsCategory(0)
                 .setArguments(bundle)
                 .toIntent());
-        preference.setSummary(mSummaryHelper.getCallsSettingSummary(getMode()));
+        preference.setSummary(mSummaryHelper.getCallsSettingSummary(zenMode));
     }
 }
\ No newline at end of file
diff --git a/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceController.java
new file mode 100644
index 0000000..bca7b55
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceController.java
@@ -0,0 +1,82 @@
+/*
+ * 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.content.Context;
+import android.service.notification.ZenDeviceEffects;
+
+import androidx.annotation.NonNull;
+import androidx.preference.Preference;
+import androidx.preference.TwoStatePreference;
+
+public class ZenModeDisplayEffectPreferenceController extends AbstractZenModePreferenceController
+        implements Preference.OnPreferenceChangeListener {
+
+    public ZenModeDisplayEffectPreferenceController(Context context, String key,
+            ZenModesBackend backend) {
+        super(context, key, backend);
+    }
+
+    @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;
+            }
+        }
+    }
+
+    @Override
+    public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
+        final boolean allow = (Boolean) newValue;
+        return saveMode(zenMode -> {
+            ZenDeviceEffects.Builder updatedEffects = new ZenDeviceEffects.Builder(
+                    zenMode.getDeviceEffects());
+            switch (getPreferenceKey()) {
+                case "effect_greyscale":
+                    updatedEffects.setShouldDisplayGrayscale(allow);
+                    break;
+                case "effect_aod":
+                    updatedEffects.setShouldSuppressAmbientDisplay(allow);
+                    break;
+                case "effect_wallpaper":
+                    updatedEffects.setShouldDimWallpaper(allow);
+                    break;
+                case "effect_dark_theme":
+                    updatedEffects.setShouldUseNightMode(allow);
+                    break;
+            }
+            zenMode.getRule().setDeviceEffects(updatedEffects.build());
+            return zenMode;
+        });
+    }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java b/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java
new file mode 100644
index 0000000..0972049
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeDisplayFragment.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.notification.modes;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import com.android.settings.R;
+import com.android.settingslib.core.AbstractPreferenceController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Settings page that shows what device effects/notification visuals will change when this mode
+ * is on.
+ */
+public class ZenModeDisplayFragment extends ZenModeFragmentBase {
+
+    @Override
+    protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
+        List<AbstractPreferenceController> prefControllers = new ArrayList<>();
+        prefControllers.add(new ZenModeNotifVisLinkPreferenceController(
+                context, "notification_visibility", mBackend));
+        prefControllers.add(new ZenModeDisplayEffectPreferenceController(
+                context, "effect_greyscale", mBackend));
+        prefControllers.add(new ZenModeDisplayEffectPreferenceController(
+                context, "effect_aod", mBackend));
+        prefControllers.add(new ZenModeDisplayEffectPreferenceController(
+                context, "effect_wallpaper", mBackend));
+        prefControllers.add(new ZenModeDisplayEffectPreferenceController(
+                context, "effect_dark_theme", mBackend));
+        return prefControllers;
+    }
+
+    @Override
+    protected int getPreferenceScreenResId() {
+        return R.xml.modes_display_settings;
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        // TODO: b/332937635 - make this the correct metrics category
+        return SettingsEnums.DND_PEOPLE;
+    }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java
new file mode 100644
index 0000000..8720a4b
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceController.java
@@ -0,0 +1,55 @@
+/*
+ * 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.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.preference.Preference;
+
+import com.android.settings.core.SubSettingLauncher;
+
+class ZenModeDisplayLinkPreferenceController extends AbstractZenModePreferenceController  {
+
+    private final ZenModeSummaryHelper mSummaryHelper;
+
+    public ZenModeDisplayLinkPreferenceController(Context context, String key,
+            ZenModesBackend backend) {
+        super(context, key, backend);
+        mSummaryHelper = new ZenModeSummaryHelper(context, backend);
+    }
+
+    @Override
+    void updateState(Preference preference, @NonNull ZenMode zenMode) {
+        Bundle bundle = new Bundle();
+        bundle.putString(MODE_ID, zenMode.getId());
+        // TODO(b/332937635): Update metrics category
+        preference.setIntent(new SubSettingLauncher(mContext)
+                .setDestination(ZenModeDisplayFragment.class.getName())
+                .setSourceMetricsCategory(0)
+                .setArguments(bundle)
+                .toIntent());
+    }
+
+    @Override
+    public CharSequence getSummary(@NonNull ZenMode zenMode) {
+        return mSummaryHelper.getDisplayEffectsSummary(zenMode);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java
index 51772f0..7f805ee 100644
--- a/src/com/android/settings/notification/modes/ZenModeFragment.java
+++ b/src/com/android/settings/notification/modes/ZenModeFragment.java
@@ -35,14 +35,14 @@
 
     @Override
     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
-        // TODO: fill in with all the elements of this page. Each should be an instance of
-        //       {@link AbstractZenModePreferenceController}.
         List<AbstractPreferenceController> prefControllers = new ArrayList<>();
         prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend));
         prefControllers.add(new ZenModePeopleLinkPreferenceController(
                 context, "zen_mode_people", mBackend));
         prefControllers.add(new ZenModeOtherLinkPreferenceController(
                 context, "zen_other_settings", mBackend));
+        prefControllers.add(new ZenModeDisplayLinkPreferenceController(
+                context, "mode_display_settings", mBackend));
         return prefControllers;
     }
 
diff --git a/src/com/android/settings/notification/modes/ZenModeHeaderController.java b/src/com/android/settings/notification/modes/ZenModeHeaderController.java
index f55c02d..f791519 100644
--- a/src/com/android/settings/notification/modes/ZenModeHeaderController.java
+++ b/src/com/android/settings/notification/modes/ZenModeHeaderController.java
@@ -47,9 +47,8 @@
     }
 
     @Override
-    public void updateState(Preference preference) {
-        ZenMode mode = getMode();
-        if (mode == null || mFragment == null) {
+    public void updateState(Preference preference, @NonNull ZenMode zenMode) {
+        if (mFragment == null) {
             return;
         }
 
@@ -62,9 +61,9 @@
         }
 
         FutureUtil.whenDone(
-                mode.getIcon(IconLoader.getInstance(mContext)),
+                zenMode.getIcon(IconLoader.getInstance(mContext)),
                 icon -> mHeaderController.setIcon(icon)
-                        .setLabel(mode.getRule().getName())
+                        .setLabel(zenMode.getRule().getName())
                         .done(false /* rebindActions */),
                 mContext.getMainExecutor());
     }
diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java
index 8261008..300ebbc 100644
--- a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java
@@ -20,10 +20,13 @@
 
 import android.content.Context;
 import android.os.Bundle;
+
+import androidx.annotation.NonNull;
 import androidx.preference.Preference;
+
 import com.android.settings.core.SubSettingLauncher;
 
-public class ZenModeMessagesLinkPreferenceController extends AbstractZenModePreferenceController {
+class ZenModeMessagesLinkPreferenceController extends AbstractZenModePreferenceController {
     private final ZenModeSummaryHelper mSummaryHelper;
 
     public ZenModeMessagesLinkPreferenceController(Context context, String key,
@@ -33,11 +36,9 @@
     }
 
     @Override
-    public void updateState(Preference preference) {
-        super.updateState(preference);
-
+    public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, getMode().getId());
+        bundle.putString(MODE_ID, zenMode.getId());
         // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeMessagesFragment.class.getName())
@@ -46,6 +47,6 @@
                 .toIntent());
 
         preference.setEnabled(true);
-        preference.setSummary(mSummaryHelper.getMessagesSettingSummary(getMode().getPolicy()));
+        preference.setSummary(mSummaryHelper.getMessagesSettingSummary(zenMode.getPolicy()));
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModeNotifVisFragment.java b/src/com/android/settings/notification/modes/ZenModeNotifVisFragment.java
new file mode 100644
index 0000000..3fdfec6
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeNotifVisFragment.java
@@ -0,0 +1,63 @@
+/*
+ * 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.settings.SettingsEnums;
+import android.content.Context;
+import android.service.notification.ZenPolicy;
+import com.android.settings.R;
+import com.android.settingslib.core.AbstractPreferenceController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Settings page that shows what notification visuals will change when this mode is on.
+ */
+public class ZenModeNotifVisFragment extends ZenModeFragmentBase {
+    @Override
+    protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
+        List<AbstractPreferenceController> prefControllers = new ArrayList<>();
+        prefControllers.add(new ZenModeNotifVisPreferenceController(context,
+                "zen_effect_intent", ZenPolicy.VISUAL_EFFECT_FULL_SCREEN_INTENT, null, mBackend));
+        prefControllers.add(new ZenModeNotifVisPreferenceController(context,
+                "zen_effect_light", ZenPolicy.VISUAL_EFFECT_LIGHTS, null, mBackend));
+        prefControllers.add(new ZenModeNotifVisPreferenceController(context,
+                "zen_effect_peek", ZenPolicy.VISUAL_EFFECT_PEEK, null, mBackend));
+        prefControllers.add(new ZenModeNotifVisPreferenceController(context,
+                "zen_effect_status", ZenPolicy.VISUAL_EFFECT_STATUS_BAR,
+                new int[] {ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST}, mBackend));
+        prefControllers.add(new ZenModeNotifVisPreferenceController(context,
+                "zen_effect_badge", ZenPolicy.VISUAL_EFFECT_BADGE, null, mBackend));
+        prefControllers.add(new ZenModeNotifVisPreferenceController(context,
+                "zen_effect_ambient", ZenPolicy.VISUAL_EFFECT_AMBIENT, null, mBackend));
+        prefControllers.add(new ZenModeNotifVisPreferenceController(context,
+                "zen_effect_list", ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST, null, mBackend));
+        return prefControllers;
+    }
+
+    @Override
+    protected int getPreferenceScreenResId() {
+        return R.xml.modes_notif_vis_settings;
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        // TODO: b/332937635 - make this the correct metrics category
+        return SettingsEnums.DND_PEOPLE;
+    }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java
new file mode 100644
index 0000000..da3b3be
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceController.java
@@ -0,0 +1,55 @@
+/*
+ * 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.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.preference.Preference;
+
+import com.android.settings.core.SubSettingLauncher;
+
+class ZenModeNotifVisLinkPreferenceController extends AbstractZenModePreferenceController  {
+
+    private final ZenModeSummaryHelper mSummaryBuilder;
+
+    public ZenModeNotifVisLinkPreferenceController(Context context, String key,
+            ZenModesBackend backend) {
+        super(context, key, backend);
+        mSummaryBuilder = new ZenModeSummaryHelper(context, backend);
+    }
+
+    @Override
+    public void updateState(Preference preference, @NonNull ZenMode zenMode) {
+        Bundle bundle = new Bundle();
+        bundle.putString(MODE_ID, zenMode.getId());
+        // TODO(b/332937635): Update metrics category
+        preference.setIntent(new SubSettingLauncher(mContext)
+                .setDestination(ZenModeNotifVisFragment.class.getName())
+                .setSourceMetricsCategory(0)
+                .setArguments(bundle)
+                .toIntent());
+    }
+
+    @Override
+    public CharSequence getSummary(@NonNull ZenMode zenMode) {
+        return mSummaryBuilder.getBlockedEffectsSummary(zenMode);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceController.java
new file mode 100644
index 0000000..39f0d3c
--- /dev/null
+++ b/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceController.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.notification.modes;
+
+import android.content.Context;
+import android.service.notification.ZenPolicy;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.CheckBoxPreference;
+import androidx.preference.Preference;
+
+import com.android.settings.widget.DisabledCheckBoxPreference;
+
+public class ZenModeNotifVisPreferenceController extends AbstractZenModePreferenceController
+        implements Preference.OnPreferenceChangeListener {
+
+    @VisibleForTesting protected @ZenPolicy.VisualEffect int mEffect;
+
+    // if any of these effects are suppressed, this effect must be too
+    @VisibleForTesting protected @ZenPolicy.VisualEffect int[] mParentSuppressedEffects;
+
+    public ZenModeNotifVisPreferenceController(Context context, String key,
+            @ZenPolicy.VisualEffect int visualEffect,
+            @ZenPolicy.VisualEffect int[] parentSuppressedEffects, ZenModesBackend backend) {
+        super(context, key, backend);
+        mEffect = visualEffect;
+        mParentSuppressedEffects = parentSuppressedEffects;
+    }
+
+    @Override
+    public boolean isAvailable() {
+        if (!super.isAvailable()) {
+            return false;
+        }
+
+        if (mEffect == ZenPolicy.VISUAL_EFFECT_LIGHTS) {
+            return mContext.getResources()
+                    .getBoolean(com.android.internal.R.bool.config_intrusiveNotificationLed);
+        }
+        return true;
+    }
+
+    @Override
+    public void updateState(Preference preference, @NonNull ZenMode zenMode) {
+
+        boolean suppressed = !zenMode.getPolicy().isVisualEffectAllowed(mEffect, false);
+        boolean parentSuppressed = false;
+        if (mParentSuppressedEffects != null) {
+            for (@ZenPolicy.VisualEffect int parentEffect : mParentSuppressedEffects) {
+                if (!zenMode.getPolicy().isVisualEffectAllowed(parentEffect, true)) {
+                    parentSuppressed = true;
+                }
+            }
+        }
+        if (parentSuppressed) {
+            ((CheckBoxPreference) preference).setChecked(true);
+            onPreferenceChange(preference, true);
+            ((DisabledCheckBoxPreference) preference).enableCheckbox(false);
+        } else {
+            ((DisabledCheckBoxPreference) preference).enableCheckbox(true);
+            ((CheckBoxPreference) preference).setChecked(suppressed);
+        }
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        final boolean allowEffect = !((Boolean) newValue);
+        return savePolicy(policy -> policy.showVisualEffect(mEffect, allowEffect));
+    }
+}
diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
index a43f8b0..1a00207 100644
--- a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java
@@ -16,20 +16,22 @@
 
 package com.android.settings.notification.modes;
 
-
 import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
 
 import android.content.Context;
 import android.os.Bundle;
+
+import androidx.annotation.NonNull;
 import androidx.preference.Preference;
+
 import com.android.settings.core.SubSettingLauncher;
 
 /**
  * Preference with a link and summary about what other sounds can break through the mode
  */
-public class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceController {
+class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceController {
 
-    ZenModeSummaryHelper mSummaryHelper;
+    private final ZenModeSummaryHelper mSummaryHelper;
 
     public ZenModeOtherLinkPreferenceController(Context context, String key,
             ZenModesBackend backend) {
@@ -38,15 +40,14 @@
     }
 
     @Override
-    public void updateState(Preference preference) {
-        super.updateState(preference);
+    public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, getMode().getId());
+        bundle.putString(MODE_ID, zenMode.getId());
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModeOtherFragment.class.getName())
                 .setSourceMetricsCategory(0)
                 .setArguments(bundle)
                 .toIntent());
-        preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(getMode()));
+        preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode));
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java
index e31fa0f..a770164e 100644
--- a/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeOtherPreferenceController.java
@@ -23,11 +23,12 @@
 import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM;
 
 import android.content.Context;
-import android.service.notification.ZenPolicy;
+
+import androidx.annotation.NonNull;
 import androidx.preference.Preference;
 import androidx.preference.TwoStatePreference;
 
-public class ZenModeOtherPreferenceController extends AbstractZenModePreferenceController
+class ZenModeOtherPreferenceController extends AbstractZenModePreferenceController
         implements Preference.OnPreferenceChangeListener {
 
     public ZenModeOtherPreferenceController(Context context, String key,
@@ -36,24 +37,15 @@
     }
 
     @Override
-    public void updateState(Preference preference) {
-        super.updateState(preference);
-
+    public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         TwoStatePreference pref = (TwoStatePreference) preference;
-        pref.setChecked(getMode().getPolicy().isCategoryAllowed(getCategory(), true));
+        pref.setChecked(zenMode.getPolicy().isCategoryAllowed(getCategory(), true));
     }
 
     @Override
     public boolean onPreferenceChange(Preference preference, Object newValue) {
         final boolean allow = (Boolean) newValue;
-
-        ZenPolicy diffPolicy = new ZenPolicy.Builder()
-                .allowCategory(getCategory(), allow)
-                .build();
-        getMode().setPolicy(diffPolicy);
-        mBackend.updateMode(getMode());
-
-        return true;
+        return savePolicy(policy -> policy.allowCategory(getCategory(), allow));
     }
 
     private int getCategory() {
diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
index f122006..55a83d6 100644
--- a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java
@@ -16,21 +16,22 @@
 
 package com.android.settings.notification.modes;
 
-
 import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID;
 
 import android.content.Context;
 import android.os.Bundle;
+
+import androidx.annotation.NonNull;
 import androidx.preference.Preference;
-import com.android.settings.R;
+
 import com.android.settings.core.SubSettingLauncher;
 
 /**
  * Preference with a link and summary about what calls and messages can break through the mode
  */
-public class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceController {
+class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceController {
 
-    ZenModeSummaryHelper mSummaryHelper;
+    private final ZenModeSummaryHelper mSummaryHelper;
 
     public ZenModePeopleLinkPreferenceController(Context context, String key,
             ZenModesBackend backend) {
@@ -39,16 +40,15 @@
     }
 
     @Override
-    public void updateState(Preference preference) {
-        super.updateState(preference);
+    public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         Bundle bundle = new Bundle();
-        bundle.putString(MODE_ID, getMode().getId());
+        bundle.putString(MODE_ID, zenMode.getId());
         // TODO(b/332937635): Update metrics category
         preference.setIntent(new SubSettingLauncher(mContext)
                 .setDestination(ZenModePeopleFragment.class.getName())
                 .setSourceMetricsCategory(0)
                 .setArguments(bundle)
                 .toIntent());
-        preference.setSummary(mSummaryHelper.getPeopleSummary(getMode()));
+        preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode));
     }
 }
diff --git a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java
index a71bbe8..16e8858 100644
--- a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java
@@ -36,10 +36,13 @@
 import android.service.notification.ConversationChannelWrapper;
 import android.service.notification.ZenPolicy;
 import android.view.View;
+
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceCategory;
 import androidx.preference.PreferenceScreen;
+
 import com.android.settings.R;
 import com.android.settings.core.SubSettingLauncher;
 import com.android.settings.notification.app.ConversationListSettings;
@@ -59,7 +62,7 @@
  * bypass DND for calls or messages, which may be one of the following values: starred contacts, all
  * contacts, priority conversations (for messages only), anyone, or no one.
  */
-public class ZenModePrioritySendersPreferenceController
+class ZenModePrioritySendersPreferenceController
         extends AbstractZenModePreferenceController {
     private final boolean mIsMessages; // if this is false, then this preference is for calls
 
@@ -124,12 +127,12 @@
     }
 
     @Override
-    public void updateState(Preference preference) {
+    public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         if (mIsMessages) {
             updateChannelCounts();
         }
-        final int currContactsSetting = getPrioritySenders();
-        final int currConversationsSetting = getPriorityConversationSenders();
+        final int currContactsSetting = getPrioritySenders(zenMode.getPolicy());
+        final int currConversationsSetting = getPriorityConversationSenders(zenMode.getPolicy());
         for (SelectorWithWidgetPreference pref : mSelectorPreferences) {
             // for each preference, check whether the current state matches what this state
             // would look like if the button were checked.
@@ -173,17 +176,17 @@
         mNumImportantConversations = numImportantConversations;
     }
 
-    private int getPrioritySenders() {
+    private int getPrioritySenders(ZenPolicy policy) {
         if (mIsMessages) {
-            return getMode().getPolicy().getPriorityMessageSenders();
+            return policy.getPriorityMessageSenders();
         } else {
-            return getMode().getPolicy().getPriorityCallSenders();
+            return policy.getPriorityCallSenders();
         }
     }
 
-    private int getPriorityConversationSenders() {
+    private int getPriorityConversationSenders(ZenPolicy policy) {
         if (mIsMessages) {
-            return getMode().getPolicy().getPriorityConversationSenders();
+            return policy.getPriorityConversationSenders();
         }
         return CONVERSATION_SENDERS_UNSET;
     }
@@ -419,29 +422,31 @@
     @VisibleForTesting
     SelectorWithWidgetPreference.OnClickListener mSelectorClickListener =
             new SelectorWithWidgetPreference.OnClickListener() {
-        @Override
-        public void onRadioButtonClicked(SelectorWithWidgetPreference preference) {
-            // The settingsToSaveOnClick function takes whether the preference is a
-            // checkbox into account to determine whether this selection is checked or unchecked.
-            final int[] settingsToSave = settingsToSaveOnClick(preference,
-                    getPrioritySenders(), getPriorityConversationSenders());
-            final int prioritySendersSetting = settingsToSave[0];
-            final int priorityConvosSetting = settingsToSave[1];
+                @Override
+                public void onRadioButtonClicked(SelectorWithWidgetPreference preference) {
+                    savePolicy(policy -> {
+                        ZenPolicy previousPolicy = policy.build();
+                        // The settingsToSaveOnClick function takes whether the preference is a
+                        // checkbox into account to determine whether this selection is checked or
+                        // unchecked.
+                        final int[] settingsToSave = settingsToSaveOnClick(preference,
+                                getPrioritySenders(previousPolicy),
+                                getPriorityConversationSenders(previousPolicy));
+                        final int prioritySendersSetting = settingsToSave[0];
+                        final int priorityConvosSetting = settingsToSave[1];
 
-            ZenPolicy.Builder diffPolicy = new ZenPolicy.Builder();
-            if (prioritySendersSetting != PEOPLE_TYPE_UNSET) {
-                if (mIsMessages) {
-                    diffPolicy.allowMessages(prioritySendersSetting);
-
-                } else {
-                    diffPolicy.allowCalls(prioritySendersSetting);
+                        if (prioritySendersSetting != PEOPLE_TYPE_UNSET) {
+                            if (mIsMessages) {
+                                policy.allowMessages(prioritySendersSetting);
+                            } else {
+                                policy.allowCalls(prioritySendersSetting);
+                            }
+                        }
+                        if (mIsMessages && priorityConvosSetting != CONVERSATION_SENDERS_UNSET) {
+                            policy.allowConversations(priorityConvosSetting);
+                        }
+                        return policy;
+                    });
                 }
-            }
-            if (mIsMessages && priorityConvosSetting != CONVERSATION_SENDERS_UNSET) {
-                diffPolicy.allowConversations(priorityConvosSetting);
-            }
-            getMode().setPolicy(diffPolicy.build());
-            mBackend.updateMode(getMode());
-        }
-    };
+            };
 }
diff --git a/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java
index d6de9c2..7569051 100644
--- a/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java
@@ -17,20 +17,17 @@
 package com.android.settings.notification.modes;
 
 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
-import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE;
 import static android.service.notification.ZenPolicy.STATE_ALLOW;
 
-import android.app.settings.SettingsEnums;
 import android.content.Context;
-import android.provider.Settings;
-import android.service.notification.ZenPolicy;
-import android.util.Log;
+
+import androidx.annotation.NonNull;
 import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
 import androidx.preference.TwoStatePreference;
+
 import com.android.settings.R;
 
-public class ZenModeRepeatCallersPreferenceController extends AbstractZenModePreferenceController
+class ZenModeRepeatCallersPreferenceController extends AbstractZenModePreferenceController
         implements Preference.OnPreferenceChangeListener {
 
     private final int mRepeatCallersThreshold;
@@ -43,14 +40,12 @@
     }
 
     @Override
-    public void updateState(Preference preference) {
-        super.updateState(preference);
-
+    public void updateState(Preference preference, @NonNull ZenMode zenMode) {
         TwoStatePreference pref = (TwoStatePreference) preference;
 
         boolean anyCallersCanBypassDnd =
-                getMode().getPolicy().getPriorityCategoryCalls() == STATE_ALLOW
-                && getMode().getPolicy().getPriorityCallSenders() == PEOPLE_TYPE_ANYONE;
+                zenMode.getPolicy().getPriorityCategoryCalls() == STATE_ALLOW
+                && zenMode.getPolicy().getPriorityCallSenders() == PEOPLE_TYPE_ANYONE;
         // if any caller can bypass dnd then repeat callers preference is disabled
         if (anyCallersCanBypassDnd) {
             pref.setEnabled(false);
@@ -58,21 +53,16 @@
         } else {
             pref.setEnabled(true);
             pref.setChecked(
-                    getMode().getPolicy().getPriorityCategoryRepeatCallers() == STATE_ALLOW);
+                    zenMode.getPolicy().getPriorityCategoryRepeatCallers() == STATE_ALLOW);
         }
 
         setRepeatCallerSummary(preference);
     }
 
     @Override
-    public boolean onPreferenceChange(Preference preference, Object newValue) {
+    public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
         final boolean allowRepeatCallers = (Boolean) newValue;
-        ZenPolicy diffPolicy = new ZenPolicy.Builder()
-                .allowRepeatCallers(allowRepeatCallers)
-                .build();
-        getMode().setPolicy(diffPolicy);
-        mBackend.updateMode(getMode());
-        return true;
+        return savePolicy(policy -> policy.allowRepeatCallers(allowRepeatCallers));
     }
 
     private void setRepeatCallerSummary(Preference preference) {
diff --git a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java
index cf0c3db..41a3d20 100644
--- a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java
+++ b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java
@@ -30,9 +30,17 @@
 import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS;
 import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REPEAT_CALLERS;
 import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_AMBIENT;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_BADGE;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_FULL_SCREEN_INTENT;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_LIGHTS;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_PEEK;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_STATUS_BAR;
 
 import android.content.Context;
 import android.icu.text.MessageFormat;
+import android.service.notification.ZenDeviceEffects;
 import android.service.notification.ZenPolicy;
 
 import com.android.settings.R;
@@ -44,10 +52,10 @@
 import java.util.Map;
 import java.util.function.Predicate;
 
-public class ZenModeSummaryHelper {
+class ZenModeSummaryHelper {
 
-    private Context mContext;
-    private ZenModesBackend mBackend;
+    private final Context mContext;
+    private final ZenModesBackend mBackend;
 
     public ZenModeSummaryHelper(Context context, ZenModesBackend backend) {
         mContext = context;
@@ -129,10 +137,22 @@
     }
 
     String getBlockedEffectsSummary(ZenMode zenMode) {
-        if (zenMode.getPolicy().shouldShowAllVisualEffects()) {
+        List<Integer> relevantVisualEffects = new ArrayList<>();
+        relevantVisualEffects.add(VISUAL_EFFECT_FULL_SCREEN_INTENT);
+        relevantVisualEffects.add(VISUAL_EFFECT_PEEK);
+        relevantVisualEffects.add(VISUAL_EFFECT_STATUS_BAR);
+        relevantVisualEffects.add(VISUAL_EFFECT_BADGE);
+        relevantVisualEffects.add(VISUAL_EFFECT_AMBIENT);
+        relevantVisualEffects.add(VISUAL_EFFECT_NOTIFICATION_LIST);
+        if (mContext.getResources()
+                .getBoolean(com.android.internal.R.bool.config_intrusiveNotificationLed)) {
+            relevantVisualEffects.add(VISUAL_EFFECT_LIGHTS);
+        }
+
+        if (shouldShowAllVisualEffects(zenMode.getPolicy(), relevantVisualEffects)) {
             return mContext.getResources().getString(
                     R.string.zen_mode_restrict_notifications_summary_muted);
-        } else if (zenMode.getPolicy().shouldHideAllVisualEffects()) {
+        } else if (shouldHideAllVisualEffects(zenMode.getPolicy(), relevantVisualEffects)) {
             return mContext.getResources().getString(
                     R.string.zen_mode_restrict_notifications_summary_hidden);
         } else {
@@ -141,6 +161,89 @@
         }
     }
 
+    private boolean shouldShowAllVisualEffects(ZenPolicy policy, List<Integer> relevantEffects) {
+        for (int i = 0; i < relevantEffects.size(); i++) {
+            if (!policy.isVisualEffectAllowed(relevantEffects.get(i), false)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean shouldHideAllVisualEffects(ZenPolicy policy, List<Integer> relevantEffects) {
+        for (int i = 0; i < relevantEffects.size(); i++) {
+            if (policy.isVisualEffectAllowed(relevantEffects.get(i), false)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    String getDisplayEffectsSummary(ZenMode zenMode) {
+        boolean isFirst = true;
+        List<String> enabledEffects = new ArrayList<>();
+        if (!zenMode.getPolicy().shouldShowAllVisualEffects()) {
+            enabledEffects.add(getBlockedEffectsSummary(zenMode));
+            isFirst = false;
+        }
+        ZenDeviceEffects currEffects =  zenMode.getRule().getDeviceEffects();
+        if (currEffects != null) {
+            if (currEffects.shouldDisplayGrayscale()) {
+                if (isFirst) {
+                    enabledEffects.add(mContext.getString(R.string.mode_grayscale_title));
+                } else {
+                    enabledEffects.add(mContext.getString(
+                            R.string.mode_grayscale_title_secondary_list));
+                }
+                isFirst = false;
+            }
+            if (currEffects.shouldSuppressAmbientDisplay()) {
+                if (isFirst) {
+                    enabledEffects.add(mContext.getString(R.string.mode_aod_title));
+                } else {
+                    enabledEffects.add(mContext.getString(
+                            R.string.mode_aod_title_secondary_list));
+                }
+                isFirst = false;
+            }
+            if (currEffects.shouldDimWallpaper()) {
+                if (isFirst) {
+                    enabledEffects.add(mContext.getString(R.string.mode_wallpaper_title));
+                } else {
+                    enabledEffects.add(mContext.getString(
+                            R.string.mode_wallpaper_title_secondary_list));
+                }
+                isFirst = false;
+            }
+            if (currEffects.shouldUseNightMode()) {
+                if (isFirst) {
+                    enabledEffects.add(mContext.getString(R.string.mode_dark_theme_title));
+                } else {
+                    enabledEffects.add(mContext.getString(
+                            R.string.mode_dark_theme_title_secondary_list));
+                }
+                isFirst = false;
+            }
+        }
+
+        int numCategories = enabledEffects.size();
+        MessageFormat msgFormat = new MessageFormat(
+                mContext.getString(R.string.mode_display_settings_summary),
+                Locale.getDefault());
+        Map<String, Object> args = new HashMap<>();
+        args.put("count", numCategories);
+        if (numCategories >= 1) {
+            args.put("effect_1", enabledEffects.get(0));
+            if (numCategories >= 2) {
+                args.put("effect_2", enabledEffects.get(1));
+                if (numCategories == 3) {
+                    args.put("effect_3", enabledEffects.get(2));
+                }
+            }
+        }
+        return msgFormat.format(args);
+    }
+
     private List<String> getEnabledCategories(ZenPolicy policy,
             Predicate<Integer> filteredCategories, boolean capitalizeFirstInList) {
         List<String> enabledCategories = new ArrayList<>();
diff --git a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
index ff5c111..ca8fe05 100644
--- a/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
+++ b/src/com/android/settings/notification/modes/ZenModesListPreferenceController.java
@@ -39,7 +39,7 @@
  * containing links to each individual mode. This is a central controller that populates and updates
  * all the preferences that then lead to a mode configuration page.
  */
-public class ZenModesListPreferenceController extends BasePreferenceController {
+class ZenModesListPreferenceController extends BasePreferenceController {
     protected static final String KEY = "zen_modes_list";
 
     @Nullable
diff --git a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
index d5ce3af..8dbcb14 100644
--- a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
+++ b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt
@@ -160,7 +160,8 @@
                 context.activityManager.noteAppRestrictionEnabled(
                     packageName, uid,
                     ActivityManager.RESTRICTION_LEVEL_FORCE_STOPPED, true,
-                    ActivityManager.RESTRICTION_REASON_USER, "settings", 0)
+                    ActivityManager.RESTRICTION_REASON_USER, "settings",
+                    ActivityManager.RESTRICTION_SOURCE_USER, 0)
             }
             context.activityManager.forceStopPackageAsUser(packageName, userId)
         }
diff --git a/src/com/android/settings/spa/network/AutomaticDataSwitchingPreference.kt b/src/com/android/settings/spa/network/AutomaticDataSwitchingPreference.kt
index 824a935..e79be4a 100644
--- a/src/com/android/settings/spa/network/AutomaticDataSwitchingPreference.kt
+++ b/src/com/android/settings/spa/network/AutomaticDataSwitchingPreference.kt
@@ -20,8 +20,10 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.viewmodel.compose.viewModel
 import com.android.settings.R
 import com.android.settings.network.telephony.TelephonyRepository
+import com.android.settings.network.telephony.wificalling.CrossSimCallingViewModel
 import com.android.settingslib.spa.widget.preference.SwitchPreference
 import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
 import kotlinx.coroutines.Dispatchers
@@ -34,6 +36,7 @@
 ) {
     val autoDataSummary = stringResource(id = R.string.primary_sim_automatic_data_msg)
     val coroutineScope = rememberCoroutineScope()
+    val crossSimCallingViewModel = viewModel<CrossSimCallingViewModel>() // handles backup calling
     SwitchPreference(
         object : SwitchPreferenceModel {
             override val title = stringResource(id = R.string.primary_sim_automatic_data_title)
@@ -42,6 +45,7 @@
             override val onCheckedChange: (Boolean) -> Unit = { newEnabled ->
                 coroutineScope.launch(Dispatchers.Default) {
                     setAutoDataEnabled(newEnabled)
+                    crossSimCallingViewModel.updateCrossSimCalling()
                 }
             }
         }
@@ -54,5 +58,4 @@
         policy = TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH,
         enabled = newEnabled,
     )
-    //TODO: setup backup calling
 }
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioDialogFragmentTest.java
similarity index 95%
rename from tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragmentTest.java
rename to tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioDialogFragmentTest.java
index 53e0d71..4477fa3 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioDialogFragmentTest.java
@@ -51,7 +51,7 @@
             ShadowAlertDialogCompat.class,
             ShadowBluetoothAdapter.class,
         })
-public class CallsAndAlarmsDialogFragmentTest {
+public class AudioSharingCallAudioDialogFragmentTest {
     @Rule public final MockitoRule mocks = MockitoJUnit.rule();
     @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
@@ -64,7 +64,7 @@
             new AudioSharingDeviceItem(TEST_DEVICE_NAME2, /* groupId= */ 1, /* isActive= */ true);
 
     private Fragment mParent;
-    private CallsAndAlarmsDialogFragment mFragment;
+    private AudioSharingCallAudioDialogFragment mFragment;
     private ShadowBluetoothAdapter mShadowBluetoothAdapter;
 
     @Before
@@ -76,7 +76,7 @@
                 BluetoothStatusCodes.FEATURE_SUPPORTED);
         mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
                 BluetoothStatusCodes.FEATURE_SUPPORTED);
-        mFragment = new CallsAndAlarmsDialogFragment();
+        mFragment = new AudioSharingCallAudioDialogFragment();
         mParent = new Fragment();
         FragmentController.setupFragment(
                 mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null);
diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioPreferenceControllerTest.java
similarity index 98%
rename from tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceControllerTest.java
rename to tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioPreferenceControllerTest.java
index 614cb5b..bdfc71f 100644
--- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingCallAudioPreferenceControllerTest.java
@@ -88,7 +88,7 @@
             ShadowBluetoothUtils.class,
             ShadowThreadUtils.class,
         })
-public class CallsAndAlarmsPreferenceControllerTest {
+public class AudioSharingCallAudioPreferenceControllerTest {
     private static final String PREF_KEY = "calls_and_alarms";
     private static final String TEST_DEVICE_NAME1 = "test1";
     private static final String TEST_DEVICE_NAME2 = "test2";
@@ -118,7 +118,7 @@
     @Mock private CachedBluetoothDevice mCachedDevice3;
     @Mock private BluetoothLeBroadcastReceiveState mState;
     @Mock private ContentResolver mContentResolver;
-    private CallsAndAlarmsPreferenceController mController;
+    private AudioSharingCallAudioPreferenceController mController;
     @Spy private ContentObserver mContentObserver;
     private ShadowBluetoothAdapter mShadowBluetoothAdapter;
     private LocalBluetoothManager mBtManager;
@@ -151,7 +151,7 @@
         bisSyncState.add(1L);
         when(mState.getBisSyncState()).thenReturn(bisSyncState);
         when(mContext.getContentResolver()).thenReturn(mContentResolver);
-        mController = new CallsAndAlarmsPreferenceController(mContext);
+        mController = new AudioSharingCallAudioPreferenceController(mContext);
         mController.init(null);
         mContentObserver = mController.getSettingsObserver();
         mPreference = new Preference(mContext);
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java
index 545f773..704637f 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BootBroadcastReceiverTest.java
@@ -43,6 +43,7 @@
 
 import java.time.Clock;
 import java.util.List;
+import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
 
 /** Tests of {@link BootBroadcastReceiver}. */
@@ -56,6 +57,7 @@
 
     @Before
     public void setUp() {
+        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
         mContext = ApplicationProvider.getApplicationContext();
         mPeriodicJobManager = PeriodicJobManager.getInstance(mContext);
         mShadowAlarmManager = shadowOf(mContext.getSystemService(AlarmManager.class));
@@ -136,7 +138,7 @@
 
         mReceiver.onReceive(mContext, new Intent(Intent.ACTION_TIME_CHANGED));
 
-        TimeUnit.MILLISECONDS.sleep(100);
+        TimeUnit.MILLISECONDS.sleep(1000);
         assertThat(mDao.getAllAfter(0)).isEmpty();
         assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull();
     }
@@ -150,7 +152,7 @@
 
         mReceiver.onReceive(mContext, new Intent(Intent.ACTION_TIME_CHANGED));
 
-        TimeUnit.MILLISECONDS.sleep(100);
+        TimeUnit.MILLISECONDS.sleep(1000);
         assertThat(mDao.getAllAfter(0).size()).isEqualTo(1);
         assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull();
     }
@@ -168,7 +170,7 @@
                                 Intent.EXTRA_TIME_PREF_24_HOUR_FORMAT,
                                 Intent.EXTRA_TIME_PREF_VALUE_USE_12_HOUR));
 
-        TimeUnit.MILLISECONDS.sleep(100);
+        TimeUnit.MILLISECONDS.sleep(1000);
         assertThat(mDao.getAllAfter(0).size()).isEqualTo(1);
         assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNull();
     }
@@ -182,7 +184,7 @@
 
         mReceiver.onReceive(mContext, new Intent(Intent.ACTION_TIMEZONE_CHANGED));
 
-        TimeUnit.MILLISECONDS.sleep(100);
+        TimeUnit.MILLISECONDS.sleep(1000);
         assertThat(mDao.getAllAfter(0)).isEmpty();
         assertThat(mShadowAlarmManager.peekNextScheduledAlarm()).isNotNull();
     }
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventEntityTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventEntityTest.java
index eb72b96..dc9b51d 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventEntityTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/db/AppUsageEventEntityTest.java
@@ -27,7 +27,7 @@
 public final class AppUsageEventEntityTest {
     @Test
     public void testBuilder_returnsExpectedResult() {
-        final long uid = 101L;
+        final int uid = 101;
         final long userId = 1001L;
         final long timestamp = 10001L;
         final int appUsageEventType = 1;
diff --git a/tests/robotests/src/com/android/settings/network/telephony/AutoDataSwitchPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/telephony/AutoDataSwitchPreferenceControllerTest.java
index 29592cf..8db4681 100644
--- a/tests/robotests/src/com/android/settings/network/telephony/AutoDataSwitchPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/network/telephony/AutoDataSwitchPreferenceControllerTest.java
@@ -85,7 +85,7 @@
                 return true;
             }
         };
-        mController.init(SUB_ID_1);
+        mController.init(SUB_ID_1, null);
     }
 
     @Test
diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceControllerTest.java
new file mode 100644
index 0000000..1a62b75
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayEffectPreferenceControllerTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.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 org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+@EnableFlags(Flags.FLAG_MODES_UI)
+public final class ZenModeDisplayEffectPreferenceControllerTest {
+
+    private Context mContext;
+    @Mock
+    private ZenModesBackend mBackend;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+    }
+
+    @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);
+
+        ZenModeDisplayEffectPreferenceController controller =
+                new ZenModeDisplayEffectPreferenceController(
+                        mContext, "effect_greyscale", mBackend);
+
+        controller.updateZenMode(preference, zenMode);
+
+        verify(preference).setChecked(true);
+    }
+
+    @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);
+
+        ZenModeDisplayEffectPreferenceController controller =
+                new ZenModeDisplayEffectPreferenceController(mContext, "effect_greyscale", mBackend);
+
+        controller.updateZenMode(preference, zenMode);
+
+        controller.onPreferenceChange(preference, false);
+
+        ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
+        verify(mBackend).updateMode(captor.capture());
+        assertThat(captor.getValue().getRule().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);
+
+        ZenModeDisplayEffectPreferenceController controller =
+                new ZenModeDisplayEffectPreferenceController(mContext, "effect_aod", mBackend);
+
+        controller.updateZenMode(preference, zenMode);
+
+        verify(preference).setChecked(true);
+    }
+
+    @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);
+
+        ZenModeDisplayEffectPreferenceController controller =
+                new ZenModeDisplayEffectPreferenceController(mContext, "effect_aod", mBackend);
+
+        controller.updateZenMode(preference, zenMode);
+
+        controller.onPreferenceChange(preference, false);
+
+        ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
+        verify(mBackend).updateMode(captor.capture());
+        assertThat(captor.getValue().getRule().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);
+
+        ZenModeDisplayEffectPreferenceController controller =
+                new ZenModeDisplayEffectPreferenceController(
+                        mContext, "effect_wallpaper", mBackend);
+
+        controller.updateZenMode(preference, zenMode);
+
+        verify(preference).setChecked(true);
+    }
+
+    @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);
+
+        ZenModeDisplayEffectPreferenceController controller =
+                new ZenModeDisplayEffectPreferenceController(
+                        mContext, "effect_wallpaper", mBackend);
+
+        controller.updateZenMode(preference, zenMode);
+
+        controller.onPreferenceChange(preference, false);
+
+        ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
+        verify(mBackend).updateMode(captor.capture());
+        assertThat(captor.getValue().getRule().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);
+
+        ZenModeDisplayEffectPreferenceController controller =
+                new ZenModeDisplayEffectPreferenceController(mContext, "effect_dark_theme",
+                        mBackend);
+
+        controller.updateZenMode(preference, zenMode);
+
+        verify(preference).setChecked(true);
+    }
+
+    @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);
+
+        ZenModeDisplayEffectPreferenceController controller =
+                new ZenModeDisplayEffectPreferenceController(mContext, "effect_dark_theme",
+                        mBackend);
+
+        controller.updateZenMode(preference, zenMode);
+
+        controller.onPreferenceChange(preference, false);
+
+        ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
+        verify(mBackend).updateMode(captor.capture());
+        assertThat(captor.getValue().getRule().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
new file mode 100644
index 0000000..9d33b0b
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeDisplayLinkPreferenceControllerTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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 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 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;
+
+@RunWith(RobolectricTestRunner.class)
+public final class ZenModeDisplayLinkPreferenceControllerTest {
+
+    private ZenModeDisplayLinkPreferenceController mController;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private Context mContext;
+    @Mock
+    private ZenModesBackend mBackend;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = RuntimeEnvironment.application;
+
+        mController = new ZenModeDisplayLinkPreferenceController(
+                mContext, "something", mBackend);
+    }
+
+    @Test
+    @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);
+        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
new file mode 100644
index 0000000..646c7aa
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisLinkPreferenceControllerTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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 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 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;
+
+@RunWith(RobolectricTestRunner.class)
+public final class ZenModeNotifVisLinkPreferenceControllerTest {
+
+    private ZenModeNotifVisLinkPreferenceController mController;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private Context mContext;
+    @Mock
+    private ZenModesBackend mBackend;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = RuntimeEnvironment.application;
+
+        mController = new ZenModeNotifVisLinkPreferenceController(
+                mContext, "something", mBackend);
+    }
+
+    @Test
+    @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);
+        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
new file mode 100644
index 0000000..05b4848
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeNotifVisPreferenceControllerTest.java
@@ -0,0 +1,237 @@
+/*
+ * 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.service.notification.ZenPolicy.STATE_ALLOW;
+import static android.service.notification.ZenPolicy.STATE_DISALLOW;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_LIGHTS;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_PEEK;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_STATUS_BAR;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.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 com.android.settings.widget.DisabledCheckBoxPreference;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+@EnableFlags(Flags.FLAG_MODES_UI)
+public final class ZenModeNotifVisPreferenceControllerTest {
+
+    private Context mContext;
+    @Mock
+    private ZenModesBackend mBackend;
+
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private ZenModeNotifVisPreferenceController mController;
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = RuntimeEnvironment.application;
+        mController = new ZenModeNotifVisPreferenceController(mContext,
+                "zen_effect_peek", VISUAL_EFFECT_PEEK, null, mBackend);
+    }
+    @Test
+    public void isAvailable() {
+        // SUPPRESSED_EFFECT_PEEK is always available:
+        assertThat(mController.isAvailable()).isTrue();
+
+        // SUPPRESSED_EFFECT_LIGHTS is only available if the device has an LED:
+        Context mockContext = mock(Context.class);
+        mController = new ZenModeNotifVisPreferenceController(mockContext,
+                "zen_effect_light", VISUAL_EFFECT_LIGHTS, null, mBackend);
+        Resources mockResources = mock(Resources.class);
+        when(mockContext.getResources()).thenReturn(mockResources);
+
+        when(mockResources.getBoolean(com.android.internal.R.bool.config_intrusiveNotificationLed))
+                .thenReturn(false); // no light
+        assertThat(mController.isAvailable()).isFalse();
+
+        when(mockResources.getBoolean(com.android.internal.R.bool.config_intrusiveNotificationLed))
+                .thenReturn(true); // has light
+        assertThat(mController.isAvailable()).isTrue();
+    }
+
+    @Test
+    public void updateState_notChecked() {
+        DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.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);
+
+        mController.updateZenMode(preference, zenMode);
+
+        verify(preference).setChecked(false);
+        verify(preference).enableCheckbox(true);
+    }
+
+    @Test
+    public void updateState_checked() {
+        DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.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);
+
+        mController.updateZenMode(preference, zenMode);
+
+        verify(preference).setChecked(true);
+        verify(preference).enableCheckbox(true);
+    }
+
+    @Test
+    public void updateState_checkedFalse_parentChecked() {
+        DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.class);
+        mController = new ZenModeNotifVisPreferenceController(mContext,
+                "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);
+
+        mController.updateZenMode(preference, zenMode);
+
+        verify(preference).setChecked(true);
+        verify(preference).enableCheckbox(false);
+        ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
+        verify(mBackend).updateMode(captor.capture());
+        assertThat(captor.getValue().getPolicy().getVisualEffectStatusBar())
+                .isEqualTo(STATE_DISALLOW);
+        assertThat(captor.getValue().getPolicy().getVisualEffectNotificationList())
+                .isEqualTo(STATE_DISALLOW); // Untouched
+    }
+
+    @Test
+    public void updateState_checkedFalse_parentNotChecked() {
+        DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.class);
+        mController = new ZenModeNotifVisPreferenceController(mContext,
+                "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);
+
+        mController.updateZenMode(preference, zenMode);
+
+        verify(preference).setChecked(false);
+        verify(preference).enableCheckbox(true);
+        verify(mBackend, never()).updateMode(any());
+    }
+
+    @Test
+    public void onPreferenceChanged_checkedFalse() {
+        DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.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);
+
+        mController.updateZenMode(preference, zenMode);
+
+        mController.onPreferenceChange(preference, false);
+
+        ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
+        verify(mBackend).updateMode(captor.capture());
+        assertThat(captor.getValue().getPolicy().getVisualEffectPeek())
+                .isEqualTo(STATE_ALLOW);
+        assertThat(captor.getValue().getPolicy().getVisualEffectNotificationList())
+                .isEqualTo(STATE_DISALLOW); // Untouched
+    }
+
+    @Test
+    public void onPreferenceChanged_checkedTrue() {
+        DisabledCheckBoxPreference preference = mock(DisabledCheckBoxPreference.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);
+
+        mController.updateZenMode(preference, zenMode);
+
+        mController.onPreferenceChange(preference, true);
+
+        ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
+        verify(mBackend).updateMode(captor.capture());
+        assertThat(captor.getValue().getPolicy().getVisualEffectPeek())
+                .isEqualTo(STATE_DISALLOW);
+        assertThat(captor.getValue().getPolicy().getVisualEffectNotificationList())
+                .isEqualTo(STATE_ALLOW); // Untouched
+    }
+}
\ No newline at end of file
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 67be82f..3e41778 100644
--- a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java
+++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java
@@ -20,11 +20,14 @@
 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;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_AMBIENT;
+import static android.service.notification.ZenPolicy.VISUAL_EFFECT_LIGHTS;
 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 org.junit.Before;
 import org.junit.Test;
@@ -165,4 +168,161 @@
         assertThat(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)).isEqualTo(
                 "Alarms, media, and 3 more can interrupt");
     }
+
+    @Test
+    public void getBlockedEffectsSummary_none() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
+                .setType(AutomaticZenRule.TYPE_BEDTIME)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .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)
+                .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)
+                .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)
+                .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");
+    }
+
+    @Test
+    public void getDisplayEffectsSummary_single_notifVis_unusedEffect() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
+                .setType(AutomaticZenRule.TYPE_BEDTIME)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .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");
+    }
+
+    @Test
+    public void getDisplayEffectsSummary_single_displayEffect() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
+                .setType(AutomaticZenRule.TYPE_BEDTIME)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .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");
+    }
+
+    @Test
+    public void getDisplayEffectsSummary_duo() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
+                .setType(AutomaticZenRule.TYPE_BEDTIME)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .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");
+    }
+
+    @Test
+    public void getDisplayEffectsSummary_trio() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
+                .setType(AutomaticZenRule.TYPE_BEDTIME)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .hideAllVisualEffects()
+                        .allowAlarms(true)
+                        .allowMedia(true)
+                        .allowSystem(true)
+                        .build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(true)
+                        .setShouldDimWallpaper(true)
+                        .build())
+                .build();
+        ZenMode zenMode = new ZenMode("id", rule, true);
+
+        assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo(
+                "Notifications hidden, grayscale, and dim the wallpaper");
+    }
+
+    @Test
+    public void getDisplayEffectsSummary_quad() {
+        AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
+                .setType(AutomaticZenRule.TYPE_BEDTIME)
+                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+                .setZenPolicy(new ZenPolicy.Builder()
+                        .showAllVisualEffects()
+                        .showVisualEffect(VISUAL_EFFECT_AMBIENT, false)
+                        .allowAlarms(true)
+                        .allowMedia(true)
+                        .allowSystem(true)
+                        .build())
+                .setDeviceEffects(new ZenDeviceEffects.Builder()
+                        .setShouldDimWallpaper(true)
+                        .setShouldDisplayGrayscale(true)
+                        .setShouldUseNightMode(true)
+                        .build())
+                .build();
+        ZenMode zenMode = new ZenMode("id", rule, true);
+
+        assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo(
+                "Notifications partially hidden, grayscale, and 2 more");
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java b/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java
index 915231f..10b17a7 100644
--- a/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java
+++ b/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java
@@ -169,7 +169,7 @@
             Context context, long userId, long timestamp, String packageName, boolean multiple) {
         final AppUsageEventEntity entity =
                 new AppUsageEventEntity(
-                        /* uid= */ 101L,
+                        /* uid= */ 101,
                         userId,
                         timestamp,
                         /* appUsageEventType= */ 2,
diff --git a/tests/robotests/testutils/com/android/settings/testutils/shadow/ShadowUtils.java b/tests/robotests/testutils/com/android/settings/testutils/shadow/ShadowUtils.java
index 5f8c434..ed03bcc 100644
--- a/tests/robotests/testutils/com/android/settings/testutils/shadow/ShadowUtils.java
+++ b/tests/robotests/testutils/com/android/settings/testutils/shadow/ShadowUtils.java
@@ -50,6 +50,7 @@
     private static ArraySet<String> sResultLinks = new ArraySet<>();
     private static boolean sIsBatteryPresent;
     private static boolean sIsMultipleBiometricsSupported;
+    private static boolean sIsPrivateProfile;
 
     @Implementation
     protected static int enforceSameOwner(Context context, int userId) {
@@ -82,6 +83,7 @@
         sResultLinks = new ArraySet<>();
         sIsBatteryPresent = true;
         sIsMultipleBiometricsSupported = false;
+        sIsPrivateProfile = false;
     }
 
     public static void setIsDemoUser(boolean isDemoUser) {
@@ -188,4 +190,13 @@
     public static void setIsMultipleBiometricsSupported(boolean isMultipleBiometricsSupported) {
         sIsMultipleBiometricsSupported = isMultipleBiometricsSupported;
     }
+
+    @Implementation
+    protected static boolean isPrivateProfile(int userId, Context context) {
+        return sIsPrivateProfile;
+    }
+
+    public static void setIsPrivateProfile(boolean isPrivateProfile) {
+        sIsPrivateProfile = isPrivateProfile;
+    }
 }
diff --git a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt
index ebdc504..652afa0 100644
--- a/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt
+++ b/tests/screenshot/src/com/android/settings/tests/screenshot/biometrics/fingerprint/Injector.kt
@@ -118,7 +118,12 @@
 
   var rfpsIconTouchViewModel = RFPSIconTouchViewModel()
   var rfpsViewModel =
-    RFPSViewModel(fingerprintEnrollEnrollingViewModel, navigationViewModel, orientationInteractor)
+    RFPSViewModel(
+      fingerprintEnrollEnrollingViewModel,
+      navigationViewModel,
+      orientationInteractor,
+      interactor,
+    )
 
   val fingerprintEnrollConfirmationViewModel =
     FingerprintEnrollConfirmationViewModel(navigationViewModel, interactor)
@@ -151,7 +156,8 @@
           BackgroundViewModel::class.java -> backgroundViewModel
           RFPSIconTouchViewModel::class.java -> rfpsIconTouchViewModel
           FingerprintEnrollEnrollingViewModel::class.java -> fingerprintEnrollEnrollingViewModel
-          FingerprintEnrollConfirmationViewModel::class.java -> fingerprintEnrollConfirmationViewModel
+          FingerprintEnrollConfirmationViewModel::class.java ->
+            fingerprintEnrollConfirmationViewModel
           else -> null
         }
           as T
diff --git a/tests/spa_unit/src/com/android/settings/datausage/DataUsageSummaryPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/DataUsageSummaryPreferenceControllerTest.kt
index 8ebb9c5..35a1a23 100644
--- a/tests/spa_unit/src/com/android/settings/datausage/DataUsageSummaryPreferenceControllerTest.kt
+++ b/tests/spa_unit/src/com/android/settings/datausage/DataUsageSummaryPreferenceControllerTest.kt
@@ -32,8 +32,10 @@
 import com.android.settings.datausage.lib.INetworkCycleDataRepository
 import com.android.settings.datausage.lib.NetworkUsageData
 import com.android.settings.network.ProxySubscriptionManager
+import com.android.settings.network.policy.NetworkPolicyRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.runBlocking
 import org.junit.Before
 import org.junit.Test
@@ -41,6 +43,7 @@
 import org.mockito.kotlin.any
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.doAnswer
 import org.mockito.kotlin.doReturn
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
@@ -69,6 +72,10 @@
         on { get() } doReturn mockSubscriptionManager
     }
 
+    private val mockNetworkPolicyRepository = mock<NetworkPolicyRepository> {
+        on { networkPolicyFlow(any()) } doAnswer { flowOf(policy) }
+    }
+
     private val fakeNetworkCycleDataRepository = object : INetworkCycleDataRepository {
         override fun getCycles(): List<Range<Long>> = emptyList()
         override fun getPolicy() = policy
@@ -86,6 +93,7 @@
         context = context,
         subId = SUB_ID,
         proxySubscriptionManager = mockProxySubscriptionManager,
+        networkPolicyRepository = mockNetworkPolicyRepository,
         networkCycleDataRepositoryFactory = { fakeNetworkCycleDataRepository },
         dataPlanRepositoryFactory = { fakeDataPlanRepository },
     )
@@ -112,7 +120,7 @@
     }
 
     @Test
-    fun getAvailabilityStatus_hasSubInfoAndPolicy_available() {
+    fun getAvailabilityStatus_hasSubInfo_available() {
         mockProxySubscriptionManager.stub {
             on { getAccessibleSubscriptionInfo(SUB_ID) } doReturn SubscriptionInfo.Builder().build()
         }
@@ -134,36 +142,43 @@
     }
 
     @Test
-    fun getAvailabilityStatus_noPolicy_conditionallyUnavailable() {
+    fun onViewCreated_noPolicy_setInvisible() = runBlocking {
         policy = null
+        controller.displayPreference(preferenceScreen)
+        clearInvocations(preference)
 
-        val availabilityStatus = controller.getAvailabilityStatus(SUB_ID)
+        controller.onViewCreated(TestLifecycleOwner())
+        delay(100)
 
-        assertThat(availabilityStatus).isEqualTo(CONDITIONALLY_UNAVAILABLE)
+        verify(preference).isVisible = false
     }
 
     @Test
-    fun displayPreference_policyHasNoLimitInfo() {
+    fun onViewCreated_policyHasNoLimitInfo() = runBlocking {
         policy = mock<NetworkPolicy>().apply {
             warningBytes = NetworkPolicy.WARNING_DISABLED
             limitBytes = NetworkPolicy.LIMIT_DISABLED
         }
-
         controller.displayPreference(preferenceScreen)
 
+        controller.onViewCreated(TestLifecycleOwner())
+        delay(100)
+
         verify(preference).setLimitInfo(null)
         verify(preference, never()).setLabels(any(), any())
     }
 
     @Test
-    fun displayPreference_policyWarningOnly() {
+    fun onViewCreated_policyWarningOnly() = runBlocking {
         policy = mock<NetworkPolicy>().apply {
             warningBytes = 1L
             limitBytes = NetworkPolicy.LIMIT_DISABLED
         }
-
         controller.displayPreference(preferenceScreen)
 
+        controller.onViewCreated(TestLifecycleOwner())
+        delay(100)
+
         val limitInfo = argumentCaptor {
             verify(preference).setLimitInfo(capture())
         }.firstValue.toString()
@@ -172,14 +187,16 @@
     }
 
     @Test
-    fun displayPreference_policyLimitOnly() {
+    fun onViewCreated_policyLimitOnly() = runBlocking {
         policy = mock<NetworkPolicy>().apply {
             warningBytes = NetworkPolicy.WARNING_DISABLED
             limitBytes = 1L
         }
-
         controller.displayPreference(preferenceScreen)
 
+        controller.onViewCreated(TestLifecycleOwner())
+        delay(100)
+
         val limitInfo = argumentCaptor {
             verify(preference).setLimitInfo(capture())
         }.firstValue.toString()
@@ -188,14 +205,16 @@
     }
 
     @Test
-    fun displayPreference_policyHasWarningAndLimit() {
+    fun onViewCreated_policyHasWarningAndLimit() = runBlocking {
         policy = mock<NetworkPolicy>().apply {
             warningBytes = BillingCycleSettings.GIB_IN_BYTES / 2
             limitBytes = BillingCycleSettings.GIB_IN_BYTES
         }
-
         controller.displayPreference(preferenceScreen)
 
+        controller.onViewCreated(TestLifecycleOwner())
+        delay(100)
+
         val limitInfo = argumentCaptor {
             verify(preference).setLimitInfo(capture())
         }.firstValue.toString()
@@ -207,7 +226,6 @@
     fun onViewCreated_emptyDataPlanInfo() = runBlocking {
         dataPlanInfo = EMPTY_DATA_PLAN_INFO
         controller.displayPreference(preferenceScreen)
-        clearInvocations(preference)
 
         controller.onViewCreated(TestLifecycleOwner())
         delay(100)
@@ -229,7 +247,6 @@
     fun onViewCreated_positiveDataPlanInfo() = runBlocking {
         dataPlanInfo = POSITIVE_DATA_PLAN_INFO
         controller.displayPreference(preferenceScreen)
-        clearInvocations(preference)
 
         controller.onViewCreated(TestLifecycleOwner())
         delay(100)
diff --git a/tests/spa_unit/src/com/android/settings/network/apn/ApnTypesTest.kt b/tests/spa_unit/src/com/android/settings/network/apn/ApnTypesTest.kt
index 95471b0..ce0d0f5 100644
--- a/tests/spa_unit/src/com/android/settings/network/apn/ApnTypesTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/apn/ApnTypesTest.kt
@@ -17,7 +17,11 @@
 package com.android.settings.network.apn
 
 import android.telephony.data.ApnSetting
+import androidx.compose.runtime.mutableStateOf
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.network.apn.ApnTypes.isValid
+import com.android.settings.network.apn.ApnTypes.toApnType
+import com.android.settingslib.spa.widget.editor.SettingsDropdownCheckOption
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -26,6 +30,50 @@
 class ApnTypesTest {
 
     @Test
+    fun isValid_hasSelected() {
+        val options = listOf(
+            SettingsDropdownCheckOption(text = APN_TYPE, selected = mutableStateOf(true)),
+        )
+
+        val isValid = options.isValid()
+
+        assertThat(isValid).isTrue()
+    }
+
+    @Test
+    fun isValid_hasNotSelected() {
+        val options = listOf(
+            SettingsDropdownCheckOption(text = APN_TYPE, selected = mutableStateOf(false)),
+        )
+
+        val isValid = options.isValid()
+
+        assertThat(isValid).isFalse()
+    }
+
+    @Test
+    fun toApnType_hasSelected() {
+        val options = listOf(
+            SettingsDropdownCheckOption(text = APN_TYPE, selected = mutableStateOf(true)),
+        )
+
+        val apnType = options.toApnType()
+
+        assertThat(apnType).isEqualTo(APN_TYPE)
+    }
+
+    @Test
+    fun toApnType_hasNotSelected() {
+        val options = listOf(
+            SettingsDropdownCheckOption(text = APN_TYPE, selected = mutableStateOf(false)),
+        )
+
+        val apnType = options.toApnType()
+
+        assertThat(apnType).isEmpty()
+    }
+
+    @Test
     fun getPreSelectedApnType_regular() {
         val apnType = ApnTypes.getPreSelectedApnType(CustomizedConfig())
 
@@ -42,4 +90,8 @@
 
         assertThat(apnType).isEqualTo("default,mms,supl,hipri,fota,cbs,xcap")
     }
+
+    private companion object {
+        const val APN_TYPE = "type"
+    }
 }
diff --git a/tests/spa_unit/src/com/android/settings/network/policy/NetworkPolicyRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/policy/NetworkPolicyRepositoryTest.kt
new file mode 100644
index 0000000..4e782cc
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/network/policy/NetworkPolicyRepositoryTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.network.policy
+
+import android.content.Context
+import android.net.NetworkPolicy
+import android.net.NetworkPolicyManager
+import android.net.NetworkTemplate
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+
+@RunWith(AndroidJUnit4::class)
+class NetworkPolicyRepositoryTest {
+
+    private val mockNetworkPolicyManager = mock<NetworkPolicyManager> {
+        on { networkPolicies } doReturn arrayOf(Policy1, Policy2)
+    }
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+        on { getSystemService(NetworkPolicyManager::class.java) } doReturn mockNetworkPolicyManager
+    }
+
+    private val repository = NetworkPolicyRepository(context)
+
+    @Test
+    fun getNetworkPolicy() {
+        val networkPolicy = repository.getNetworkPolicy(Template1)
+
+        assertThat(networkPolicy).isSameInstanceAs(Policy1)
+    }
+
+    @Test
+    fun networkPolicyFlow() = runBlocking {
+        val networkPolicy = repository.networkPolicyFlow(Template2).firstWithTimeoutOrNull()
+
+        assertThat(networkPolicy).isSameInstanceAs(Policy2)
+    }
+
+    private companion object {
+        val Template1: NetworkTemplate =
+            NetworkTemplate.Builder(NetworkTemplate.MATCH_MOBILE).build()
+        val Template2: NetworkTemplate = NetworkTemplate.Builder(NetworkTemplate.MATCH_WIFI).build()
+        val Policy1 = mock<NetworkPolicy>().apply {
+            template = Template1
+        }
+        val Policy2 = mock<NetworkPolicy>().apply {
+            template = Template2
+        }
+    }
+}
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkImeiPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkImeiPreferenceControllerTest.kt
index 2f67846..1da8fd9 100644
--- a/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkImeiPreferenceControllerTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/MobileNetworkImeiPreferenceControllerTest.kt
@@ -19,18 +19,16 @@
 import android.content.Context
 import android.telephony.SubscriptionInfo
 import android.telephony.TelephonyManager
-import android.telephony.euicc.EuiccManager
 import androidx.fragment.app.Fragment
 import androidx.preference.Preference
 import androidx.preference.PreferenceManager
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.dx.mockito.inline.extended.ExtendedMockito
-import com.android.internal.telephony.PhoneConstants
+import com.android.settings.R
 import com.android.settings.core.BasePreferenceController
 import com.android.settings.network.SubscriptionInfoListViewModel
 import com.android.settings.network.SubscriptionUtil
-import com.android.settingslib.CustomDialogPreferenceCompat
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
 import org.junit.After
@@ -62,6 +60,8 @@
         on { currentPhoneType } doReturn TelephonyManager.PHONE_TYPE_GSM
         on { imei } doReturn mockImei
         on { meid } doReturn mockImei
+        on { primaryImei } doReturn mockImei
+        on { activeModemCount } doReturn 2
     }
 
     private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
@@ -90,7 +90,7 @@
     }
 
     @Test
-    fun refreshData_getPhoneNumber_preferenceSummaryIsExpected() = runBlocking {
+    fun refreshData_getImei_preferenceSummaryIsExpected() = runBlocking {
         whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true)
         whenever(SubscriptionUtil.getActiveSubscriptions(any())).thenReturn(
             listOf(
@@ -111,6 +111,50 @@
     }
 
     @Test
+    fun refreshData_getImeiTitle_showImei() = runBlocking {
+        whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true)
+        whenever(SubscriptionUtil.getActiveSubscriptions(any())).thenReturn(
+            listOf(
+                SUB_INFO_1,
+                SUB_INFO_2
+            )
+        )
+        var mockSubId = 2
+        controller.init(mockFragment, mockSubId)
+        mockImei = "test imei"
+        mockTelephonyManager.stub {
+            on { imei } doReturn mockImei
+            on { primaryImei } doReturn ""
+        }
+
+        controller.refreshData(SUB_INFO_2)
+
+        assertThat(preference.title).isEqualTo(context.getString(R.string.status_imei))
+    }
+
+    @Test
+    fun refreshData_getPrimaryImeiTitle_showPrimaryImei() = runBlocking {
+        whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(true)
+        whenever(SubscriptionUtil.getActiveSubscriptions(any())).thenReturn(
+            listOf(
+                SUB_INFO_1,
+                SUB_INFO_2
+            )
+        )
+        var mockSubId = 2
+        controller.init(mockFragment, mockSubId)
+        mockImei = "test imei"
+        mockTelephonyManager.stub {
+            on { imei } doReturn mockImei
+            on { primaryImei } doReturn mockImei
+        }
+
+        controller.refreshData(SUB_INFO_2)
+
+        assertThat(preference.title).isEqualTo(context.getString(R.string.imei_primary))
+    }
+
+    @Test
     fun getAvailabilityStatus_notSimHardwareVisible() {
         whenever(SubscriptionUtil.isSimHardwareVisible(context)).thenReturn(false)
 
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt
index b2ee018..e233fa4 100644
--- a/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/SubscriptionRepositoryTest.kt
@@ -116,6 +116,27 @@
     }
 
     @Test
+    fun getSelectableSubscriptionInfoList_oneNotInSlot_inSlotSortedFirst() {
+        mockSubscriptionManager.stub {
+            on { getAvailableSubscriptionInfoList() } doReturn listOf(
+                SubscriptionInfo.Builder().apply {
+                    setSimSlotIndex(SubscriptionManager.INVALID_SIM_SLOT_INDEX)
+                    setId(SUB_ID_3_NOT_IN_SLOT)
+                }.build(),
+                SubscriptionInfo.Builder().apply {
+                    setSimSlotIndex(SIM_SLOT_INDEX_1)
+                    setId(SUB_ID_IN_SLOT_1)
+                }.build(),
+            )
+        }
+
+        val subInfos = context.getSelectableSubscriptionInfoList()
+
+        assertThat(subInfos.map { it.simSlotIndex })
+            .containsExactly(SIM_SLOT_INDEX_1, SubscriptionManager.INVALID_SIM_SLOT_INDEX).inOrder()
+    }
+
+    @Test
     fun getSelectableSubscriptionInfoList_sameGroupAndOneHasSlot_returnTheOneWithSimSlotIndex() {
         mockSubscriptionManager.stub {
             on { getAvailableSubscriptionInfoList() } doReturn listOf(
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt
index f198660..04cba73 100644
--- a/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/ims/ImsMmTelRepositoryTest.kt
@@ -43,6 +43,7 @@
 import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
 
 @RunWith(AndroidJUnit4::class)
 class ImsMmTelRepositoryTest {
@@ -155,6 +156,13 @@
         assertThat(isSupported).isTrue()
     }
 
+    @Test
+    fun setCrossSimCallingEnabled() = runBlocking {
+        repository.setCrossSimCallingEnabled(true)
+
+        verify(mockImsMmTelManager).setCrossSimCallingEnabled(true)
+    }
+
     private companion object {
         const val SUB_ID = 1
         const val CAPABILITY = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE
diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/wificalling/WifiCallingRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/wificalling/WifiCallingRepositoryTest.kt
index 1f3acc2..0144f66 100644
--- a/tests/spa_unit/src/com/android/settings/network/telephony/wificalling/WifiCallingRepositoryTest.kt
+++ b/tests/spa_unit/src/com/android/settings/network/telephony/wificalling/WifiCallingRepositoryTest.kt
@@ -17,15 +17,18 @@
 package com.android.settings.network.telephony.wificalling
 
 import android.content.Context
+import android.telephony.AccessNetworkConstants
 import android.telephony.CarrierConfigManager
 import android.telephony.CarrierConfigManager.KEY_USE_WFC_HOME_NETWORK_MODE_IN_ROAMING_NETWORK_BOOL
 import android.telephony.TelephonyManager
 import android.telephony.ims.ImsMmTelManager
+import android.telephony.ims.feature.MmTelFeature
 import androidx.core.os.persistableBundleOf
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.settings.network.telephony.ims.ImsMmTelRepository
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.any
@@ -98,6 +101,22 @@
         assertThat(wiFiCallingMode).isEqualTo(ImsMmTelManager.WIFI_MODE_WIFI_PREFERRED)
     }
 
+    @Test
+    fun isWifiCallingSupported() = runBlocking {
+        mockImsMmTelRepository.stub {
+            onBlocking {
+                isSupported(
+                    capability = MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE,
+                    transportType = AccessNetworkConstants.TRANSPORT_TYPE_WLAN,
+                )
+            } doReturn true
+        }
+
+        val isSupported = repository.isWifiCallingSupported()
+
+        assertThat(isSupported).isTrue()
+    }
+
     private fun mockUseWfcHomeModeForRoaming(config: Boolean) {
         mockCarrierConfigManager.stub {
             on {