Merge "Renaming new start user tests"
diff --git a/ApiDocs.bp b/ApiDocs.bp
index a46ecce..90b6603 100644
--- a/ApiDocs.bp
+++ b/ApiDocs.bp
@@ -422,26 +422,27 @@
         "$(location merge_zips) $(out) $(location :ds-docs-java{.docs.zip}) $(genDir)/ds-docs-kt-moved.zip",
 }
 
-java_genrule {
-    name: "ds-docs-switched",
-    tools: [
-        "switcher4",
-        "soong_zip",
-    ],
-    srcs: [
-        ":ds-docs-java{.docs.zip}",
-        ":ds-docs-kt{.docs.zip}",
-    ],
-    out: ["ds-docs-switched.zip"],
-    dist: {
-        targets: ["docs"],
-    },
-    cmd: "unzip -q $(location :ds-docs-java{.docs.zip}) -d $(genDir) && " +
-        "unzip -q $(location :ds-docs-kt{.docs.zip}) -d $(genDir)/en/reference/kotlin && " +
-        "SWITCHER=$$(cd $$(dirname $(location switcher4)) && pwd)/$$(basename $(location switcher4)) && " +
-        "(cd $(genDir)/en/reference && $$SWITCHER --work platform) > /dev/null && " +
-        "$(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)",
-}
+// Disable doc generation until Doclava is migrated to JDK 17 (b/240421555).
+// java_genrule {
+//     name: "ds-docs-switched",
+//     tools: [
+//         "switcher4",
+//         "soong_zip",
+//     ],
+//     srcs: [
+//         ":ds-docs-java{.docs.zip}",
+//         ":ds-docs-kt{.docs.zip}",
+//     ],
+//     out: ["ds-docs-switched.zip"],
+//     dist: {
+//         targets: ["docs"],
+//     },
+//     cmd: "unzip -q $(location :ds-docs-java{.docs.zip}) -d $(genDir) && " +
+//         "unzip -q $(location :ds-docs-kt{.docs.zip}) -d $(genDir)/en/reference/kotlin && " +
+//         "SWITCHER=$$(cd $$(dirname $(location switcher4)) && pwd)/$$(basename $(location switcher4)) && " +
+//         "(cd $(genDir)/en/reference && $$SWITCHER --work platform) > /dev/null && " +
+//         "$(location soong_zip) -o $(out) -C $(genDir) -D $(genDir)",
+// }
 
 droiddoc {
     name: "ds-static-docs",
diff --git a/core/api/current.txt b/core/api/current.txt
index f2ca78f..d3de158 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -51318,7 +51318,7 @@
     method public void onTransactionCommitted();
   }
 
-  public static class SurfaceControl.TrustedPresentationThresholds {
+  public static final class SurfaceControl.TrustedPresentationThresholds {
     ctor public SurfaceControl.TrustedPresentationThresholds(@FloatRange(from=0.0f, fromInclusive=false, to=1.0f) float, @FloatRange(from=0.0f, fromInclusive=false, to=1.0f) float, @IntRange(from=1) int);
   }
 
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 0476d79..cf92861 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2121,7 +2121,7 @@
     method @Nullable public void query(@NonNull android.app.search.Query, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.app.search.SearchTarget>>);
     method public void registerEmptyQueryResultUpdateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.app.search.SearchSession.Callback);
     method public void requestEmptyQueryResultUpdate();
-    method public void unregisterEmptyQueryResultUpdateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.app.search.SearchSession.Callback);
+    method public void unregisterEmptyQueryResultUpdateCallback(@NonNull android.app.search.SearchSession.Callback);
   }
 
   public static interface SearchSession.Callback {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 34afd8a..360113b 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3270,6 +3270,7 @@
     field public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_ENABLED = "autofill_credential_manager_enabled";
     field public static final String DEVICE_CONFIG_AUTOFILL_CREDENTIAL_MANAGER_IGNORE_VIEWS = "autofill_credential_manager_ignore_views";
     field public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED = "autofill_dialog_enabled";
+    field public static final String DEVICE_CONFIG_AUTOFILL_PCC_CLASSIFICATION_ENABLED = "pcc_classification_enabled";
     field public static final String DEVICE_CONFIG_AUTOFILL_SMART_SUGGESTION_SUPPORTED_MODES = "smart_suggestion_supported_modes";
     field public static final String DEVICE_CONFIG_NON_AUTOFILLABLE_IME_ACTION_IDS = "non_autofillable_ime_action_ids";
     field public static final String DEVICE_CONFIG_PACKAGE_DENYLIST_FOR_UNIMPORTANT_VIEW = "package_deny_list_for_unimportant_view";
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
index ae57959..65b0cfb 100644
--- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -48,6 +48,7 @@
 import android.util.SparseArray;
 import android.util.TypedValue;
 import android.util.Xml;
+import android.view.InputDevice;
 import android.view.View;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -612,9 +613,29 @@
     private boolean mIsAccessibilityTool = false;
 
     /**
+     * {@link InputDevice} sources which may send {@link android.view.MotionEvent}s.
+     * @see #setMotionEventSources(int)
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "SOURCE_" }, value = {
+            InputDevice.SOURCE_MOUSE,
+            InputDevice.SOURCE_STYLUS,
+            InputDevice.SOURCE_BLUETOOTH_STYLUS,
+            InputDevice.SOURCE_TRACKBALL,
+            InputDevice.SOURCE_MOUSE_RELATIVE,
+            InputDevice.SOURCE_TOUCHPAD,
+            InputDevice.SOURCE_TOUCH_NAVIGATION,
+            InputDevice.SOURCE_ROTARY_ENCODER,
+            InputDevice.SOURCE_JOYSTICK,
+            InputDevice.SOURCE_SENSOR
+    })
+    public @interface MotionEventSources {}
+
+    /**
      * The bit mask of {@link android.view.InputDevice} sources that the accessibility
      * service wants to listen to for generic {@link android.view.MotionEvent}s.
      */
+    @MotionEventSources
     private int mMotionEventSources = 0;
 
     /**
@@ -966,6 +987,7 @@
      * Returns the bit mask of {@link android.view.InputDevice} sources that the accessibility
      * service wants to listen to for generic {@link android.view.MotionEvent}s.
      */
+    @MotionEventSources
     public int getMotionEventSources() {
         return mMotionEventSources;
     }
@@ -975,28 +997,29 @@
      * service wants to listen to for generic {@link android.view.MotionEvent}s.
      *
      * <p>
-     * Note: including an {@link android.view.InputDevice} source that does not send
+     * Including an {@link android.view.InputDevice} source that does not send
      * {@link android.view.MotionEvent}s is effectively a no-op for that source, since you will
      * not receive any events from that source.
      * </p>
+     *
      * <p>
-     * Allowed sources include:
-     * <li>{@link android.view.InputDevice#SOURCE_MOUSE}</li>
-     * <li>{@link android.view.InputDevice#SOURCE_STYLUS}</li>
-     * <li>{@link android.view.InputDevice#SOURCE_BLUETOOTH_STYLUS}</li>
-     * <li>{@link android.view.InputDevice#SOURCE_TRACKBALL}</li>
-     * <li>{@link android.view.InputDevice#SOURCE_MOUSE_RELATIVE}</li>
-     * <li>{@link android.view.InputDevice#SOURCE_TOUCHPAD}</li>
-     * <li>{@link android.view.InputDevice#SOURCE_TOUCH_NAVIGATION}</li>
-     * <li>{@link android.view.InputDevice#SOURCE_ROTARY_ENCODER}</li>
-     * <li>{@link android.view.InputDevice#SOURCE_JOYSTICK}</li>
-     * <li>{@link android.view.InputDevice#SOURCE_SENSOR}</li>
+     * See {@link android.view.InputDevice} for complete source definitions.
+     * Many input devices send {@link android.view.InputEvent}s from more than one type of source so
+     * you may need to include multiple {@link android.view.MotionEvent} sources here, in addition
+     * to using {@link AccessibilityService#onKeyEvent} to listen to {@link android.view.KeyEvent}s.
+     * </p>
+     *
+     * <p>
+     * <strong>Note:</strong> {@link android.view.InputDevice} sources contain source class bits
+     * that complicate bitwise flag removal operations. To remove a specific source you should
+     * rebuild the entire value using bitwise OR operations on the individual source constants.
      * </p>
      *
      * @param motionEventSources A bit mask of {@link android.view.InputDevice} sources.
      * @see AccessibilityService#onMotionEvent
+     * @see MotionEventSources
      */
-    public void setMotionEventSources(int motionEventSources) {
+    public void setMotionEventSources(@MotionEventSources int motionEventSources) {
         mMotionEventSources = motionEventSources;
     }
 
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index f6992c9..d3b03c0 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -774,7 +774,15 @@
         return this;
     }
 
-    /** @hide */
+    /**
+     * Returns if this broadcast should not run until the process is in an active process state.
+     *
+     * @return {@code true} if this broadcast should not run until the process is in an active
+     *                      process state. Otherwise, {@code false}.
+     * @see #setDeferUntilActive(boolean)
+     *
+     * @hide
+     */
     @SystemApi
     public boolean isDeferUntilActive() {
         return mIsDeferUntilActive;
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 70d8a5e..bb91ecd 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -2324,10 +2324,13 @@
                 mUiAutomation.connect(flags);
                 return mUiAutomation;
             }
+            final long startUptime = SystemClock.uptimeMillis();
             try {
                 mUiAutomation.connectWithTimeout(flags, CONNECT_TIMEOUT_MILLIS);
                 return mUiAutomation;
             } catch (TimeoutException e) {
+                final long waited = SystemClock.uptimeMillis() - startUptime;
+                Log.e(TAG, "Unable to connect to UiAutomation. Waited for " + waited + " ms", e);
                 mUiAutomation.destroy();
                 mUiAutomation = null;
             }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 1345910..b86b09d 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -703,6 +703,16 @@
      */
     public static final int FLAG_BUBBLE = 0x00001000;
 
+    /**
+     * Bit to be bitswised-ored into the {@link #flags} field that should be
+     * set by the system if this notification is not dismissible.
+     *
+     * This flag is for internal use only; applications cannot set this flag directly.
+     * @hide
+     */
+    public static final int FLAG_NO_DISMISS = 0x00002000;
+
+
     private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList(
             BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class,
             DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class,
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index e323e89..909073e 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -265,15 +265,9 @@
     public void onCreate() {
     }
 
-    /**
-     * Provided as a convenience for agent implementations that need an opportunity
-     * to do one-time initialization before the actual backup or restore operation
-     * is begun with information about the calling user.
-     * <p>
-     *
-     * @hide
-     */
+    /** @hide */
     public void onCreate(UserHandle user) {
+        mUser = user;
         onCreate();
     }
 
@@ -284,7 +278,6 @@
      */
     @Deprecated
     public void onCreate(UserHandle user, @BackupDestination int backupDestination) {
-        mUser = user;
         mBackupDestination = backupDestination;
 
         onCreate(user);
@@ -295,7 +288,6 @@
     */
     public void onCreate(UserHandle user, @BackupDestination int backupDestination,
             @OperationType int operationType) {
-        mUser = user;
         mBackupDestination = backupDestination;
         mLogger = new BackupRestoreEventLogger(operationType);
 
diff --git a/core/java/android/app/search/SearchSession.java b/core/java/android/app/search/SearchSession.java
index eda68dd..9e0a1d0 100644
--- a/core/java/android/app/search/SearchSession.java
+++ b/core/java/android/app/search/SearchSession.java
@@ -204,7 +204,6 @@
      * @param callback The callback to be unregistered.
      */
     public void unregisterEmptyQueryResultUpdateCallback(
-            @NonNull @CallbackExecutor Executor callbackExecutor,
             @NonNull Callback callback) {
         synchronized (mRegisteredCallbacks) {
             if (mIsClosed.get()) {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index a6e074c..6aa7f3f 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -6237,7 +6237,7 @@
      *
      * @param permission The name of the permission being checked.
      * @param pid The process ID being checked against.  Must be > 0.
-     * @param uid The user ID being checked against.  A uid of 0 is the root
+     * @param uid The UID being checked against.  A uid of 0 is the root
      * user, which will pass every permission check.
      *
      * @return {@link PackageManager#PERMISSION_GRANTED} if the given
@@ -6327,7 +6327,7 @@
      *
      * @param permission The name of the permission being checked.
      * @param pid The process ID being checked against.  Must be &gt; 0.
-     * @param uid The user ID being checked against.  A uid of 0 is the root
+     * @param uid The UID being checked against.  A uid of 0 is the root
      * user, which will pass every permission check.
      * @param message A message to include in the exception if it is thrown.
      *
@@ -6471,7 +6471,7 @@
      *
      * @param uri The uri that is being checked.
      * @param pid The process ID being checked against.  Must be &gt; 0.
-     * @param uid The user ID being checked against.  A uid of 0 is the root
+     * @param uid The UID being checked against.  A uid of 0 is the root
      * user, which will pass every permission check.
      * @param modeFlags The access modes to check.
      *
@@ -6499,7 +6499,7 @@
      *
      * @param uris The list of URIs that is being checked.
      * @param pid The process ID being checked against.  Must be &gt; 0.
-     * @param uid The user ID being checked against.  A uid of 0 is the root
+     * @param uid The UID being checked against.  A uid of 0 is the root
      * user, which will pass every permission check.
      * @param modeFlags The access modes to check for the list of uris
      *
@@ -6625,7 +6625,7 @@
      * @param writePermission The permission that provides overall write
      * access, or null to not do this check.
      * @param pid The process ID being checked against.  Must be &gt; 0.
-     * @param uid The user ID being checked against.  A uid of 0 is the root
+     * @param uid The UID being checked against.  A uid of 0 is the root
      * user, which will pass every permission check.
      * @param modeFlags The access modes to check.
      *
@@ -6649,7 +6649,7 @@
      *
      * @param uri The uri that is being checked.
      * @param pid The process ID being checked against.  Must be &gt; 0.
-     * @param uid The user ID being checked against.  A uid of 0 is the root
+     * @param uid The UID being checked against.  A uid of 0 is the root
      * user, which will pass every permission check.
      * @param modeFlags The access modes to enforce.
      * @param message A message to include in the exception if it is thrown.
@@ -6708,7 +6708,7 @@
      * @param writePermission The permission that provides overall write
      * access, or null to not do this check.
      * @param pid The process ID being checked against.  Must be &gt; 0.
-     * @param uid The user ID being checked against.  A uid of 0 is the root
+     * @param uid The UID being checked against.  A uid of 0 is the root
      * user, which will pass every permission check.
      * @param modeFlags The access modes to enforce.
      * @param message A message to include in the exception if it is thrown.
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index fd73719..83f0894 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1103,6 +1103,17 @@
     public static final long ALWAYS_SANDBOX_DISPLAY_APIS = 185004937L; // buganizer id
 
     /**
+     * This change id excludes the packages it is applied to from ignoreOrientationRequest behaviour
+     * that can be enabled by the device manufacturers for the com.android.server.wm.DisplayArea
+     * or for the whole display.
+     * @hide
+     */
+    @ChangeId
+    @Overridable
+    @Disabled
+    public static final long OVERRIDE_RESPECT_REQUESTED_ORIENTATION = 236283604L; // buganizer id
+
+    /**
      * This change id excludes the packages it is applied to from the camera compat force rotation
      * treatment. See com.android.server.wm.DisplayRotationCompatPolicy for context.
      * @hide
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index a980158..7378ac7 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -2510,8 +2510,18 @@
      *
      * <p>This returns a mapping of package names for this user id to whether we dispatch Tag
      * intents to the package. {@link #ACTION_NDEF_DISCOVERED}, {@link #ACTION_TECH_DISCOVERED} or
-    *  {@link #ACTION_TAG_DISCOVERED} will not be dispatched to an Activity if its package is
-    *  disallowed.
+     * {@link #ACTION_TAG_DISCOVERED} will not be dispatched to an Activity if its package is
+     * mapped to {@code false}.
+     * <p>There are three different possible cases:
+     * <p>A package not being in the preference list.
+     * It does not contain any Tag intent filters or the user never triggers a Tag detection that
+     * matches the intent filter of the package.
+     * <p>A package being mapped to {@code true}.
+     * When a package has been launched by a tag detection for the first time, the package name is
+     * put to the map and by default mapped to {@code true}. The package will receive Tag intents as
+     * usual.
+     * <p>A package being mapped to {@code false}.
+     * The user chooses to disable this package and it will not receive any Tag intents anymore.
      *
      * @param userId the user to whom this preference list will belong to
      * @return a map of the UserId which indicates the mapping from package name to
diff --git a/core/java/android/service/voice/OWNERS b/core/java/android/service/voice/OWNERS
index 59a0c2e..ec44100 100644
--- a/core/java/android/service/voice/OWNERS
+++ b/core/java/android/service/voice/OWNERS
@@ -1,3 +1,7 @@
 # Bug component: 533220
 
 include /core/java/android/app/assist/OWNERS
+
+# The owner here should not be assist owner
+liangyuchen@google.com
+tuanng@google.com
diff --git a/core/java/android/util/RotationUtils.java b/core/java/android/util/RotationUtils.java
index 3e7c67e..f20767b 100644
--- a/core/java/android/util/RotationUtils.java
+++ b/core/java/android/util/RotationUtils.java
@@ -27,6 +27,7 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.view.Surface;
 import android.view.Surface.Rotation;
 import android.view.SurfaceControl;
 
@@ -245,4 +246,23 @@
                 throw new IllegalArgumentException("Unknown rotation: " + rotation);
         }
     }
+
+    /**
+     * Reverses the rotation direction around the Z axis. Note that this method assumes all
+     * rotations are relative to {@link Surface.ROTATION_0}.
+     *
+     * @param rotation the original rotation.
+     * @return the new rotation that should be applied.
+     */
+    @Surface.Rotation
+    public static int reverseRotationDirectionAroundZAxis(@Surface.Rotation int rotation) {
+        // Flipping 270 and 90 has the same effect as changing the direction which rotation is
+        // applied.
+        if (rotation == Surface.ROTATION_90) {
+            rotation = Surface.ROTATION_270;
+        } else if (rotation == Surface.ROTATION_270) {
+            rotation = Surface.ROTATION_90;
+        }
+        return rotation;
+    }
 }
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index e26c7be..3a02c48 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -337,6 +337,9 @@
     @Nullable
     public DisplayShape displayShape;
 
+    @Nullable
+    public SurfaceControl.RefreshRateRange layoutLimitedRefreshRate;
+
     public static final @android.annotation.NonNull Creator<DisplayInfo> CREATOR = new Creator<DisplayInfo>() {
         @Override
         public DisplayInfo createFromParcel(Parcel source) {
@@ -411,7 +414,8 @@
                 && brightnessDefault == other.brightnessDefault
                 && Objects.equals(roundedCorners, other.roundedCorners)
                 && installOrientation == other.installOrientation
-                && Objects.equals(displayShape, other.displayShape);
+                && Objects.equals(displayShape, other.displayShape)
+                && Objects.equals(layoutLimitedRefreshRate, other.layoutLimitedRefreshRate);
     }
 
     @Override
@@ -466,6 +470,7 @@
         roundedCorners = other.roundedCorners;
         installOrientation = other.installOrientation;
         displayShape = other.displayShape;
+        layoutLimitedRefreshRate = other.layoutLimitedRefreshRate;
     }
 
     public void readFromParcel(Parcel source) {
@@ -526,6 +531,7 @@
         }
         installOrientation = source.readInt();
         displayShape = source.readTypedObject(DisplayShape.CREATOR);
+        layoutLimitedRefreshRate = source.readTypedObject(SurfaceControl.RefreshRateRange.CREATOR);
     }
 
     @Override
@@ -584,6 +590,7 @@
         }
         dest.writeInt(installOrientation);
         dest.writeTypedObject(displayShape, flags);
+        dest.writeTypedObject(layoutLimitedRefreshRate, flags);
     }
 
     @Override
@@ -758,7 +765,7 @@
         sb.append(name);
         sb.append("\", displayId ");
         sb.append(displayId);
-        sb.append("\", displayGroupId ");
+        sb.append(", displayGroupId ");
         sb.append(displayGroupId);
         sb.append(flagsToString(flags));
         sb.append(", real ");
@@ -843,6 +850,8 @@
         sb.append(brightnessDefault);
         sb.append(", installOrientation ");
         sb.append(Surface.rotationToString(installOrientation));
+        sb.append(", layoutLimitedRefreshRate ");
+        sb.append(layoutLimitedRefreshRate);
         sb.append("}");
         return sb.toString();
     }
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index b003659..18e7e05 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -1757,7 +1757,7 @@
      * Information about the min and max refresh rate DM would like to set the display to.
      * @hide
      */
-    public static final class RefreshRateRange {
+    public static final class RefreshRateRange implements Parcelable {
         public static final String TAG = "RefreshRateRange";
 
         // The tolerance within which we consider something approximately equals.
@@ -1826,6 +1826,35 @@
             this.min = other.min;
             this.max = other.max;
         }
+
+        /**
+         * Writes the RefreshRateRange to parce
+         *
+         * @param dest parcel to write the transaction to
+         */
+        @Override
+        public void writeToParcel(@NonNull Parcel dest, @WriteFlags int flags) {
+            dest.writeFloat(min);
+            dest.writeFloat(max);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        public static final @NonNull Creator<RefreshRateRange> CREATOR =
+                new Creator<RefreshRateRange>() {
+                    @Override
+                    public RefreshRateRange createFromParcel(Parcel in) {
+                        return new RefreshRateRange(in.readFloat(), in.readFloat());
+                    }
+
+                    @Override
+                    public RefreshRateRange[] newArray(int size) {
+                        return new RefreshRateRange[size];
+                    }
+                };
     }
 
     /**
@@ -2476,10 +2505,10 @@
      * {@link Transaction#setTrustedPresentationCallback(SurfaceControl,
      * TrustedPresentationThresholds, Executor, Consumer)}
      */
-    public static class TrustedPresentationThresholds {
-        private float mMinAlpha;
-        private float mMinFractionRendered;
-        private int mStabilityRequirementMs;
+    public static final class TrustedPresentationThresholds {
+        private final float mMinAlpha;
+        private final float mMinFractionRendered;
+        private final int mStabilityRequirementMs;
 
         /**
          * Creates a TrustedPresentationThresholds that's used when calling
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 872b4f9..cea397c 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -8839,6 +8839,8 @@
 
         mInsetsController.dump(prefix, writer);
 
+        mOnBackInvokedDispatcher.dump(prefix, writer);
+
         writer.println(prefix + "View Hierarchy:");
         dumpViewHierarchy(innerPrefix, writer, mView);
     }
diff --git a/core/java/android/view/WindowLayout.java b/core/java/android/view/WindowLayout.java
index 5ec5219..3b8298e 100644
--- a/core/java/android/view/WindowLayout.java
+++ b/core/java/android/view/WindowLayout.java
@@ -17,9 +17,9 @@
 package android.view;
 
 import static android.view.InsetsSource.ID_IME;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.systemBars;
 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
@@ -109,14 +109,6 @@
             // Ensure that windows with a non-ALWAYS display cutout mode are laid out in
             // the cutout safe zone.
             final Rect displayFrame = state.getDisplayFrame();
-            final InsetsSource statusBarSource = state.peekSource(ITYPE_STATUS_BAR);
-            if (statusBarSource != null && displayCutoutSafe.top > displayFrame.top) {
-                // Make sure that the zone we're avoiding for the cutout is at least as tall as the
-                // status bar; otherwise fullscreen apps will end up cutting halfway into the status
-                // bar.
-                displayCutoutSafeExceptMaybeBars.top =
-                        Math.max(statusBarSource.getFrame().bottom, displayCutoutSafe.top);
-            }
             if (cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES) {
                 if (displayFrame.width() < displayFrame.height()) {
                     displayCutoutSafeExceptMaybeBars.top = MIN_Y;
@@ -131,7 +123,7 @@
                     && (cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
                     || cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)) {
                 final Insets systemBarsInsets = state.calculateInsets(
-                        displayFrame, WindowInsets.Type.systemBars(), requestedVisibleTypes);
+                        displayFrame, systemBars(), requestedVisibleTypes);
                 if (systemBarsInsets.left > 0) {
                     displayCutoutSafeExceptMaybeBars.left = MIN_X;
                 }
@@ -145,12 +137,11 @@
                     displayCutoutSafeExceptMaybeBars.bottom = MAX_Y;
                 }
             }
-            if (type == TYPE_INPUT_METHOD) {
-                final InsetsSource navSource = state.peekSource(ITYPE_NAVIGATION_BAR);
-                if (navSource != null && navSource.calculateInsets(displayFrame, true).bottom > 0) {
-                    // The IME can always extend under the bottom cutout if the navbar is there.
-                    displayCutoutSafeExceptMaybeBars.bottom = MAX_Y;
-                }
+            if (type == TYPE_INPUT_METHOD
+                    && displayCutoutSafeExceptMaybeBars.bottom != MAX_Y
+                    && state.calculateInsets(displayFrame, navigationBars(), true).bottom > 0) {
+                // The IME can always extend under the bottom cutout if the navbar is there.
+                displayCutoutSafeExceptMaybeBars.bottom = MAX_Y;
             }
             final boolean attachedInParent = attachedWindowFrame != null && !layoutInScreen;
 
diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java
index cba399f..e7c610b 100644
--- a/core/java/android/view/autofill/AutofillFeatureFlags.java
+++ b/core/java/android/view/autofill/AutofillFeatureFlags.java
@@ -145,19 +145,13 @@
      * <p>For example, a list with only 1 package would be, {@code Package1:;}. A list with one
      * denied activity {@code Activity1} under {@code Package1} and a full denied package
      * {@code Package2} would be {@code Package1:Activity1;Package2:;}
-     *
-     * @hide
      */
-    @TestApi
     public static final String DEVICE_CONFIG_PACKAGE_DENYLIST_FOR_UNIMPORTANT_VIEW =
             "package_deny_list_for_unimportant_view";
 
     /**
      * Whether the heuristics check for view is enabled
-     *
-     * @hide
      */
-    @TestApi
     public static final String DEVICE_CONFIG_TRIGGER_FILL_REQUEST_ON_UNIMPORTANT_VIEW =
             "trigger_fill_request_on_unimportant_view";
 
@@ -169,15 +163,24 @@
      *
      * <p> For example, a imeAction list could be "2,3,4", corresponding to ime_action definition
      * in {@link android.view.inputmethod.EditorInfo.java}</p>
-     *
-     * @hide
      */
-    @TestApi
     @SuppressLint("IntentName")
     public static final String DEVICE_CONFIG_NON_AUTOFILLABLE_IME_ACTION_IDS =
             "non_autofillable_ime_action_ids";
     // END AUTOFILL FOR ALL APPS FLAGS //
 
+
+    // START AUTOFILL PCC CLASSIFICATION FLAGS
+
+    /**
+     * Sets the fill dialog feature enabled or not.
+     */
+    public static final String DEVICE_CONFIG_AUTOFILL_PCC_CLASSIFICATION_ENABLED =
+            "pcc_classification_enabled";
+
+    // END AUTOFILL PCC CLASSIFICATION FLAGS
+
+
     /**
      * Sets a value of delay time to show up the inline tooltip view.
      *
@@ -191,6 +194,7 @@
     private static final boolean DEFAULT_HAS_FILL_DIALOG_UI_FEATURE = false;
     private static final String DEFAULT_FILL_DIALOG_ENABLED_HINTS = "";
 
+
     // CREDENTIAL MANAGER DEFAULTS
     // Credential manager is enabled by default so as to allow testing by app developers
     private static final boolean DEFAULT_CREDENTIAL_MANAGER_ENABLED = true;
@@ -199,6 +203,13 @@
     private static final boolean DEFAULT_CREDENTIAL_MANAGER_SUPPRESS_SAVE_DIALOG = false;
     // END CREDENTIAL MANAGER DEFAULTS
 
+
+    // AUTOFILL PCC CLASSIFICATION FLAGS DEFAULTS
+    // Default for whether the pcc classification is enabled for autofill.
+    private static final boolean DEFAULT_AUTOFILL_PCC_CLASSIFICATION_ENABLED = false;
+    // END AUTOFILL PCC CLASSIFICATION FLAGS DEFAULTS
+
+
     private AutofillFeatureFlags() {};
 
     /**
@@ -302,4 +313,23 @@
             DeviceConfig.NAMESPACE_AUTOFILL,
             DEVICE_CONFIG_PACKAGE_DENYLIST_FOR_UNIMPORTANT_VIEW, "");
     }
+
+
+    // START AUTOFILL PCC CLASSIFICATION FUNCTIONS
+
+    /**
+     * Whether Autofill PCC Detection is enabled.
+     *
+     * @hide
+     */
+    public static boolean isAutofillPccClassificationEnabled() {
+        // TODO(b/266379948): Add condition for checking whether device has PCC first
+
+        return DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_AUTOFILL,
+                DEVICE_CONFIG_AUTOFILL_PCC_CLASSIFICATION_ENABLED,
+                DEFAULT_AUTOFILL_PCC_CLASSIFICATION_ENABLED);
+    }
+
+    // END AUTOFILL PCC CLASSIFICATION FUNCTIONS
 }
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index a0bd7f7..34b75a4 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -211,6 +211,12 @@
         IOnBackInvokedCallback getIOnBackInvokedCallback() {
             return mIOnBackInvokedCallback;
         }
+
+        @Override
+        public String toString() {
+            return "ImeCallback=ImeOnBackInvokedCallback@" + mId
+                    + " Callback=" + mIOnBackInvokedCallback;
+        }
     }
 
     /**
diff --git a/core/java/android/window/ProxyOnBackInvokedDispatcher.java b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
index 09d8b0f..56c05b2 100644
--- a/core/java/android/window/ProxyOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
@@ -180,16 +180,7 @@
                 return;
             }
             clearCallbacksOnDispatcher();
-            if (actualDispatcher instanceof ProxyOnBackInvokedDispatcher) {
-                // We don't want to nest ProxyDispatchers, so if we are given on, we unwrap its
-                // actual dispatcher.
-                // This can happen when an Activity is recreated but the Window is preserved (e.g.
-                // when going from split-screen back to single screen)
-                mActualDispatcher =
-                        ((ProxyOnBackInvokedDispatcher) actualDispatcher).mActualDispatcher;
-            } else {
-                mActualDispatcher = actualDispatcher;
-            }
+            mActualDispatcher = actualDispatcher;
             transferCallbacksToDispatcher();
         }
     }
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 7a5510c..d34ece9 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -29,6 +29,7 @@
 import android.view.IWindow;
 import android.view.IWindowSession;
 
+import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -232,6 +233,26 @@
         return Checker.isOnBackInvokedCallbackEnabled(mChecker.getContext());
     }
 
+    /**
+     * Dump information about this WindowOnBackInvokedDispatcher
+     * @param prefix the prefix that will be prepended to each line of the produced output
+     * @param writer the writer that will receive the resulting text
+     */
+    public void dump(String prefix, PrintWriter writer) {
+        String innerPrefix = prefix + "    ";
+        writer.println(prefix + "WindowOnBackDispatcher:");
+        if (mAllCallbacks.isEmpty()) {
+            writer.println(prefix + "<None>");
+            return;
+        }
+
+        writer.println(innerPrefix + "Top Callback: " + getTopCallback());
+        writer.println(innerPrefix + "Callbacks: ");
+        mAllCallbacks.forEach((callback, priority) -> {
+            writer.println(innerPrefix + "  Callback: " + callback + " Priority=" + priority);
+        });
+    }
+
     static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub {
         private final WeakReference<OnBackInvokedCallback> mCallback;
 
diff --git a/core/java/com/android/internal/accessibility/common/MagnificationConstants.java b/core/java/com/android/internal/accessibility/common/MagnificationConstants.java
new file mode 100644
index 0000000..94c230b
--- /dev/null
+++ b/core/java/com/android/internal/accessibility/common/MagnificationConstants.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.accessibility.common;
+
+/**
+ * Collection of common constants for accessibility shortcut.
+ */
+public final class MagnificationConstants {
+    private MagnificationConstants() {}
+
+    /**
+     * The min value for the magnification persisted scale. We assume if the scale is lower than
+     * the min value, there will be no obvious magnification effect.
+     */
+    public static final float PERSISTED_SCALE_MIN_VALUE = 1.3f;
+}
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index c6aca44..b63041b 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -378,8 +378,12 @@
             // window, as we'll be skipping the addView in handleResumeActivity(), and
             // the token will not be updated as for a new window.
             getAttributes().token = preservedWindow.getAttributes().token;
-            mProxyOnBackInvokedDispatcher.setActualDispatcher(
-                    preservedWindow.getOnBackInvokedDispatcher());
+            final ViewRootImpl viewRoot = mDecor.getViewRootImpl();
+            if (viewRoot != null) {
+                // Clear the old callbacks and attach to the new window.
+                viewRoot.getOnBackInvokedDispatcher().clear();
+                onViewRootImplSet(viewRoot);
+            }
         }
         // Even though the device doesn't support picture-in-picture mode,
         // an user can force using it through developer options.
diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java
index 786941f..74a9d16 100644
--- a/core/java/com/android/internal/security/VerityUtils.java
+++ b/core/java/com/android/internal/security/VerityUtils.java
@@ -81,6 +81,15 @@
         }
     }
 
+    /** Enables fs-verity for an open file without signature. */
+    public static void setUpFsverity(int fd) throws IOException {
+        int errno = enableFsverityForFdNative(fd);
+        if (errno != 0) {
+            throw new IOException("Failed to enable fs-verity on FD(" + fd + "): "
+                    + Os.strerror(errno));
+        }
+    }
+
     /** Returns whether the file has fs-verity enabled. */
     public static boolean hasFsverity(@NonNull String filePath) {
         int retval = statxForFsverityNative(filePath);
@@ -211,6 +220,7 @@
     }
 
     private static native int enableFsverityNative(@NonNull String filePath);
+    private static native int enableFsverityForFdNative(int fd);
     private static native int measureFsverityNative(@NonNull String filePath,
             @NonNull byte[] digest);
     private static native int statxForFsverityNative(@NonNull String filePath);
diff --git a/core/jni/com_android_internal_security_VerityUtils.cpp b/core/jni/com_android_internal_security_VerityUtils.cpp
index 3e5689b..4a9e2d4 100644
--- a/core/jni/com_android_internal_security_VerityUtils.cpp
+++ b/core/jni/com_android_internal_security_VerityUtils.cpp
@@ -38,13 +38,8 @@
 
 namespace {
 
-int enableFsverity(JNIEnv *env, jobject /* clazz */, jstring filePath) {
-    ScopedUtfChars path(env, filePath);
-    if (path.c_str() == nullptr) {
-        return EINVAL;
-    }
-    ::android::base::unique_fd rfd(open(path.c_str(), O_RDONLY | O_CLOEXEC));
-    if (rfd.get() < 0) {
+int enableFsverityForFd(JNIEnv *env, jobject clazz, jint fd) {
+    if (fd < 0) {
         return errno;
     }
 
@@ -55,12 +50,21 @@
     arg.salt_size = 0;
     arg.salt_ptr = reinterpret_cast<uintptr_t>(nullptr);
 
-    if (ioctl(rfd.get(), FS_IOC_ENABLE_VERITY, &arg) < 0) {
+    if (ioctl(fd, FS_IOC_ENABLE_VERITY, &arg) < 0) {
         return errno;
     }
     return 0;
 }
 
+int enableFsverity(JNIEnv *env, jobject clazz, jstring filePath) {
+    ScopedUtfChars path(env, filePath);
+    if (path.c_str() == nullptr) {
+        return EINVAL;
+    }
+    ::android::base::unique_fd rfd(open(path.c_str(), O_RDONLY | O_CLOEXEC));
+    return enableFsverityForFd(env, clazz, rfd.get());
+}
+
 // Returns whether the file has fs-verity enabled.
 // 0 if it is not present, 1 if is present, and -errno if there was an error.
 int statxForFsverity(JNIEnv *env, jobject /* clazz */, jstring filePath) {
@@ -126,6 +130,7 @@
 }
 const JNINativeMethod sMethods[] = {
         {"enableFsverityNative", "(Ljava/lang/String;)I", (void *)enableFsverity},
+        {"enableFsverityForFdNative", "(I)I", (void *)enableFsverityForFd},
         {"statxForFsverityNative", "(Ljava/lang/String;)I", (void *)statxForFsverity},
         {"measureFsverityNative", "(Ljava/lang/String;[B)I", (void *)measureFsverity},
 };
diff --git a/core/proto/android/os/system_properties.proto b/core/proto/android/os/system_properties.proto
index 4f3eeb0..84c82e0 100644
--- a/core/proto/android/os/system_properties.proto
+++ b/core/proto/android/os/system_properties.proto
@@ -514,7 +514,10 @@
 
         optional string gfx_driver_whitelist_0 = 45;
 
-        // Next Tag: 46
+        optional bool  egl_blobcache_multifile = 46;
+        optional int32 egl_blobcache_multifile_limit = 47;
+
+        // Next Tag: 48
     }
     optional Ro ro = 21;
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 6aee3cd..0378539 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -1751,7 +1751,7 @@
     <!-- Allows an application to access wrist temperature data from the watch sensors.
          If you're requesting this permission, you must also request
          {@link #BODY_SENSORS_WRIST_TEMPERATURE}. Requesting this permission by itself doesn't
-         give you heart rate body sensors access.
+         give you wrist temperature body sensors access.
          <p class="note"><strong>Note: </strong> This permission is for Wear OS only.
          <p>Protection level: dangerous
 
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 0c13484..14eaf34 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -983,6 +983,13 @@
     <integer-array name="config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis">
     </integer-array>
 
+    <!-- Boolean indicating whether secondary built-in displays should have their orientation
+         match the active default display. This config assumes that the secondary display only
+         requires swapping ROTATION_90 and ROTATION_270.
+         TODO(b/265991392): This should eventually be configured and parsed in
+          display_settings.xml -->
+    <bool name="config_matchSecondaryInternalDisplaysOrientationToReverseDefaultDisplay">true</bool>
+
     <!-- Indicate available ColorDisplayManager.COLOR_MODE_xxx. -->
     <integer-array name="config_availableColorModes">
         <!-- Example:
@@ -4110,16 +4117,6 @@
     -->
     <string-array translatable="false" name="config_convert_to_emergency_number_map" />
 
-    <!-- An array of packages for which notifications cannot be blocked.
-         Should only be used for core device functionality that must not be
-         rendered inoperative for safety reasons, like the phone dialer and
-         SMS handler. -->
-    <string-array translatable="false" name="config_nonBlockableNotificationPackages">
-        <item>com.android.dialer</item>
-        <item>com.android.messaging</item>
-        <item>com.android.cellbroadcastreceiver.module</item>
-    </string-array>
-
     <!-- An array of packages that can make sound on the ringer stream in priority-only DND
      mode -->
     <string-array translatable="false" name="config_priorityOnlyDndExemptPackages">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 4260576..7777f11 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3404,6 +3404,13 @@
        TODO(b/265312193): Remove this workaround when this bug is fixed.-->
   <java-symbol type="array" name="config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis" />
 
+  <!-- Boolean indicating whether secondary built-in displays should have their orientation
+       match the active default display. This config assumes that the secondary display only
+       requires swapping ROTATION_90 and ROTATION_270.
+       TODO(b/265991392): This should eventually be configured and parsed in
+        display_settings.xml -->
+  <java-symbol type="bool" name="config_matchSecondaryInternalDisplaysOrientationToReverseDefaultDisplay" />
+
   <!-- Default user restrictions for the SYSTEM user -->
   <java-symbol type="array" name="config_defaultFirstUserRestrictions" />
 
@@ -3419,7 +3426,6 @@
 
   <java-symbol type="array" name="config_convert_to_emergency_number_map" />
 
-  <java-symbol type="array" name="config_nonBlockableNotificationPackages" />
   <java-symbol type="array" name="config_priorityOnlyDndExemptPackages" />
 
   <!-- Screen-size-dependent modes for picker dialogs. -->
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java b/core/tests/coretests/src/android/view/WindowLayoutTests.java
similarity index 89%
rename from services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
rename to core/tests/coretests/src/android/view/WindowLayoutTests.java
index 731a235..5cac98d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowLayoutTests.java
+++ b/core/tests/coretests/src/android/view/WindowLayoutTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.server.wm;
+package android.view;
 
 import static android.view.InsetsSource.ID_IME;
-import static android.view.InsetsState.ITYPE_NAVIGATION_BAR;
-import static android.view.InsetsState.ITYPE_STATUS_BAR;
 import static android.view.WindowInsets.Type.displayCutout;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.navigationBars;
@@ -39,13 +37,6 @@
 import android.graphics.Insets;
 import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
-import android.view.DisplayCutout;
-import android.view.Gravity;
-import android.view.InsetsSource;
-import android.view.InsetsState;
-import android.view.WindowInsets;
-import android.view.WindowLayout;
-import android.view.WindowManager;
 import android.window.ClientWindowFrames;
 
 import androidx.test.filters.SmallTest;
@@ -72,6 +63,11 @@
             new Rect(DISPLAY_WIDTH / 4, 0, DISPLAY_WIDTH * 3 / 4, DISPLAY_CUTOUT_HEIGHT);
     private static final Insets WATERFALL_INSETS = Insets.of(6, 0, 12, 0);
 
+    private static final int ID_STATUS_BAR = InsetsSource.createId(
+            null /* owner */, 0 /* index */, statusBars());
+    private static final int ID_NAVIGATION_BAR = InsetsSource.createId(
+            null /* owner */, 0 /* index */, navigationBars());
+
     private final WindowLayout mWindowLayout = new WindowLayout();
     private final ClientWindowFrames mFrames = new ClientWindowFrames();
 
@@ -90,9 +86,9 @@
         mAttrs = new WindowManager.LayoutParams();
         mState = new InsetsState();
         mState.setDisplayFrame(new Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT));
-        mState.getOrCreateSource(ITYPE_STATUS_BAR, statusBars()).setFrame(
+        mState.getOrCreateSource(ID_STATUS_BAR, statusBars()).setFrame(
                 0, 0, DISPLAY_WIDTH, STATUS_BAR_HEIGHT);
-        mState.getOrCreateSource(ITYPE_NAVIGATION_BAR, navigationBars()).setFrame(
+        mState.getOrCreateSource(ID_NAVIGATION_BAR, navigationBars()).setFrame(
                 0, DISPLAY_HEIGHT - NAVIGATION_BAR_HEIGHT, DISPLAY_WIDTH, DISPLAY_HEIGHT);
         mState.getDisplayCutoutSafe(mDisplayCutoutSafe);
         mWindowBounds = new Rect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT);
@@ -268,8 +264,8 @@
 
     @Test
     public void fitInvisibleInsets() {
-        mState.setSourceVisible(ITYPE_STATUS_BAR, false);
-        mState.setSourceVisible(ITYPE_NAVIGATION_BAR, false);
+        mState.setSourceVisible(ID_STATUS_BAR, false);
+        mState.setSourceVisible(ID_NAVIGATION_BAR, false);
         computeFrames();
 
         assertInsetByTopBottom(0, 0, mFrames.displayFrame);
@@ -279,8 +275,8 @@
 
     @Test
     public void fitInvisibleInsetsIgnoringVisibility() {
-        mState.setSourceVisible(ITYPE_STATUS_BAR, false);
-        mState.setSourceVisible(ITYPE_NAVIGATION_BAR, false);
+        mState.setSourceVisible(ID_STATUS_BAR, false);
+        mState.setSourceVisible(ID_NAVIGATION_BAR, false);
         mAttrs.setFitInsetsIgnoringVisibility(true);
         computeFrames();
 
@@ -324,11 +320,11 @@
         mAttrs.setFitInsetsTypes(0);
         computeFrames();
 
-        assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+        assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
                 mFrames.displayFrame);
-        assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+        assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
                 mFrames.parentFrame);
-        assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+        assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
                 mFrames.frame);
     }
 
@@ -365,17 +361,17 @@
     @Test
     public void layoutInDisplayCutoutModeDefaultWithInvisibleSystemBars() {
         addDisplayCutout();
-        mState.setSourceVisible(ITYPE_STATUS_BAR, false);
-        mState.setSourceVisible(ITYPE_NAVIGATION_BAR, false);
+        mState.setSourceVisible(ID_STATUS_BAR, false);
+        mState.setSourceVisible(ID_NAVIGATION_BAR, false);
         mAttrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
         mAttrs.setFitInsetsTypes(0);
         computeFrames();
 
-        assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+        assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
                 mFrames.displayFrame);
-        assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+        assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
                 mFrames.parentFrame);
-        assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+        assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
                 mFrames.frame);
     }
 
@@ -398,11 +394,11 @@
         mAttrs.setFitInsetsTypes(0);
         computeFrames();
 
-        assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+        assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
                 mFrames.displayFrame);
-        assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+        assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
                 mFrames.parentFrame);
-        assertInsetBy(WATERFALL_INSETS.left, STATUS_BAR_HEIGHT, WATERFALL_INSETS.right, 0,
+        assertInsetBy(WATERFALL_INSETS.left, DISPLAY_CUTOUT_HEIGHT, WATERFALL_INSETS.right, 0,
                 mFrames.frame);
     }
 
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index bccf283..f2d6250 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1771,6 +1771,12 @@
       "group": "WM_DEBUG_KEEP_SCREEN_ON",
       "at": "com\/android\/server\/wm\/RootWindowContainer.java"
     },
+    "-464564167": {
+      "message": "Current transition prevents automatic focus change",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_FOCUS",
+      "at": "com\/android\/server\/wm\/DisplayContent.java"
+    },
     "-463348344": {
       "message": "Removing and adding activity %s to root task at top callers=%s",
       "level": "INFO",
@@ -2065,12 +2071,6 @@
       "group": "WM_DEBUG_ANIM",
       "at": "com\/android\/server\/wm\/SurfaceAnimator.java"
     },
-    "-206549078": {
-      "message": "Not moving display (displayId=%d) to top. Top focused displayId=%d. Reason: config_perDisplayFocusEnabled",
-      "level": "INFO",
-      "group": "WM_DEBUG_FOCUS_LIGHT",
-      "at": "com\/android\/server\/wm\/WindowManagerService.java"
-    },
     "-203358733": {
       "message": "commitFinishDrawingLocked: mDrawState=READY_TO_SHOW %s",
       "level": "INFO",
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 67963a3..d94e8e4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -154,7 +154,7 @@
                 activityOptions);
 
         // Set adjacent to each other so that the containers below will be invisible.
-        setAdjacentTaskFragments(wct, launchingFragmentToken, secondaryFragmentToken, rule);
+        setAdjacentTaskFragmentsWithRule(wct, launchingFragmentToken, secondaryFragmentToken, rule);
         setCompanionTaskFragment(wct, launchingFragmentToken, secondaryFragmentToken, rule,
                 false /* isStacked */);
     }
@@ -167,7 +167,7 @@
     void expandTaskFragment(@NonNull WindowContainerTransaction wct,
             @NonNull IBinder fragmentToken) {
         resizeTaskFragment(wct, fragmentToken, new Rect());
-        setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */);
+        clearAdjacentTaskFragments(wct, fragmentToken);
         updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
         updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
     }
@@ -238,26 +238,37 @@
         wct.reparentActivityToTaskFragment(fragmentToken, reparentActivityToken);
     }
 
-    void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
-            @NonNull IBinder primary, @Nullable IBinder secondary, @Nullable SplitRule splitRule) {
-        if (secondary == null) {
-            wct.clearAdjacentTaskFragments(primary);
-            return;
-        }
-
+    /**
+     * Sets the two given TaskFragments as adjacent to each other with respecting the given
+     * {@link SplitRule} for {@link WindowContainerTransaction.TaskFragmentAdjacentParams}.
+     */
+    void setAdjacentTaskFragmentsWithRule(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule) {
         WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = null;
         final boolean finishSecondaryWithPrimary =
-                splitRule != null && SplitContainer.shouldFinishSecondaryWithPrimary(splitRule);
+                SplitContainer.shouldFinishSecondaryWithPrimary(splitRule);
         final boolean finishPrimaryWithSecondary =
-                splitRule != null && SplitContainer.shouldFinishPrimaryWithSecondary(splitRule);
+                SplitContainer.shouldFinishPrimaryWithSecondary(splitRule);
         if (finishSecondaryWithPrimary || finishPrimaryWithSecondary) {
             adjacentParams = new WindowContainerTransaction.TaskFragmentAdjacentParams();
             adjacentParams.setShouldDelayPrimaryLastActivityRemoval(finishSecondaryWithPrimary);
             adjacentParams.setShouldDelaySecondaryLastActivityRemoval(finishPrimaryWithSecondary);
         }
+        setAdjacentTaskFragments(wct, primary, secondary, adjacentParams);
+    }
+
+    void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder primary, @NonNull IBinder secondary,
+            @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams) {
         wct.setAdjacentTaskFragments(primary, secondary, adjacentParams);
     }
 
+    void clearAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken) {
+        // Clear primary will also clear secondary.
+        wct.clearAdjacentTaskFragments(fragmentToken);
+    }
+
     void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct,
             @NonNull IBinder primary, @NonNull IBinder secondary, @NonNull SplitRule splitRule,
             boolean isStacked) {
@@ -268,7 +279,7 @@
         } else {
             finishPrimaryWithSecondary = shouldFinishPrimaryWithSecondary(splitRule);
         }
-        wct.setCompanionTaskFragment(primary, finishPrimaryWithSecondary ? secondary : null);
+        setCompanionTaskFragment(wct, primary, finishPrimaryWithSecondary ? secondary : null);
 
         final boolean finishSecondaryWithPrimary;
         if (isStacked) {
@@ -277,7 +288,12 @@
         } else {
             finishSecondaryWithPrimary = shouldFinishSecondaryWithPrimary(splitRule);
         }
-        wct.setCompanionTaskFragment(secondary, finishSecondaryWithPrimary ? primary : null);
+        setCompanionTaskFragment(wct, secondary, finishSecondaryWithPrimary ? primary : null);
+    }
+
+    void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary,
+            @Nullable IBinder secondary) {
+        wct.setCompanionTaskFragment(primary, secondary);
     }
 
     void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
index 825c670..18497ad 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java
@@ -21,9 +21,11 @@
 import android.os.IBinder;
 import android.util.Pair;
 import android.util.Size;
+import android.window.TaskFragmentParentInfo;
 import android.window.WindowContainerTransaction;
 
 import androidx.annotation.NonNull;
+import androidx.window.extensions.core.util.function.Function;
 
 /**
  * Client-side descriptor of a split that holds two containers.
@@ -35,8 +37,12 @@
     private final TaskFragmentContainer mSecondaryContainer;
     @NonNull
     private final SplitRule mSplitRule;
+    /** @see SplitContainer#getCurrentSplitAttributes() */
     @NonNull
-    private SplitAttributes mSplitAttributes;
+    private SplitAttributes mCurrentSplitAttributes;
+    /** @see SplitContainer#getDefaultSplitAttributes() */
+    @NonNull
+    private SplitAttributes mDefaultSplitAttributes;
     @NonNull
     private final IBinder mToken;
 
@@ -48,7 +54,8 @@
         mPrimaryContainer = primaryContainer;
         mSecondaryContainer = secondaryContainer;
         mSplitRule = splitRule;
-        mSplitAttributes = splitAttributes;
+        mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes();
+        mCurrentSplitAttributes = splitAttributes;
         mToken = new Binder("SplitContainer");
 
         if (shouldFinishPrimaryWithSecondary(splitRule)) {
@@ -82,9 +89,37 @@
         return mSplitRule;
     }
 
+    /**
+     * Returns the current {@link SplitAttributes} this {@code SplitContainer} is showing.
+     * <p>
+     * If the {@code SplitAttributes} calculator function is not set by
+     * {@link SplitController#setSplitAttributesCalculator(Function)}, the current
+     * {@code SplitAttributes} is either to expand the containers if the size constraints of
+     * {@link #getSplitRule()} are not satisfied,
+     * or the {@link #getDefaultSplitAttributes()}, otherwise.
+     * </p><p>
+     * If the {@code SplitAttributes} calculator function is set, the current
+     * {@code SplitAttributes} will be customized by the function, which can be any
+     * {@code SplitAttributes}.
+     * </p>
+     *
+     * @see SplitAttributes.SplitType.ExpandContainersSplitType
+     */
     @NonNull
-    SplitAttributes getSplitAttributes() {
-        return mSplitAttributes;
+    SplitAttributes getCurrentSplitAttributes() {
+        return mCurrentSplitAttributes;
+    }
+
+    /**
+     * Returns the default {@link SplitAttributes} when the parent task container bounds satisfy
+     * {@link #getSplitRule()} constraints.
+     * <p>
+     * The value is usually from {@link SplitRule#getDefaultSplitAttributes} unless it is overridden
+     * by {@link SplitController#updateSplitAttributes(IBinder, SplitAttributes)}.
+     */
+    @NonNull
+    SplitAttributes getDefaultSplitAttributes() {
+        return mDefaultSplitAttributes;
     }
 
     @NonNull
@@ -95,11 +130,19 @@
     /**
      * Updates the {@link SplitAttributes} to this container.
      * It is usually used when there's a folding state change or
-     * {@link SplitController#onTaskFragmentParentInfoChanged(WindowContainerTransaction, int,
-     * Configuration)}.
+     * {@link SplitController#onTaskFragmentParentInfoChanged(WindowContainerTransaction,
+     * int, TaskFragmentParentInfo)}.
      */
-    void setSplitAttributes(@NonNull SplitAttributes splitAttributes) {
-        mSplitAttributes = splitAttributes;
+    void updateCurrentSplitAttributes(@NonNull SplitAttributes splitAttributes) {
+        mCurrentSplitAttributes = splitAttributes;
+    }
+
+    /**
+     * Overrides the default {@link SplitAttributes} to this container, which may be different
+     * from {@link SplitRule#getDefaultSplitAttributes}.
+     */
+    void updateDefaultSplitAttributes(@NonNull SplitAttributes splitAttributes) {
+        mDefaultSplitAttributes = splitAttributes;
     }
 
     @NonNull
@@ -121,7 +164,7 @@
     @NonNull
     SplitInfo toSplitInfo() {
         return new SplitInfo(mPrimaryContainer.toActivityStack(),
-                mSecondaryContainer.toActivityStack(), mSplitAttributes, mToken);
+                mSecondaryContainer.toActivityStack(), mCurrentSplitAttributes, mToken);
     }
 
     static boolean shouldFinishPrimaryWithSecondary(@NonNull SplitRule splitRule) {
@@ -180,9 +223,10 @@
     public String toString() {
         return "SplitContainer{"
                 + " primaryContainer=" + mPrimaryContainer
-                + " secondaryContainer=" + mSecondaryContainer
-                + " splitRule=" + mSplitRule
-                + " splitAttributes" + mSplitAttributes
+                + ", secondaryContainer=" + mSecondaryContainer
+                + ", splitRule=" + mSplitRule
+                + ", currentSplitAttributes" + mCurrentSplitAttributes
+                + ", defaultSplitAttributes" + mDefaultSplitAttributes
                 + "}";
     }
 }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index ff58201..2c1ddf7 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -87,6 +87,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
@@ -279,6 +280,98 @@
         }
     }
 
+    @Override
+    public void finishActivityStacks(@NonNull Set<IBinder> activityStackTokens) {
+        if (activityStackTokens.isEmpty()) {
+            return;
+        }
+        synchronized (mLock) {
+            // Translate ActivityStack to TaskFragmentContainer.
+            final List<TaskFragmentContainer> pendingFinishingContainers =
+                    activityStackTokens.stream()
+                    .map(token -> {
+                        synchronized (mLock) {
+                            return getContainer(token);
+                        }
+                    }).filter(Objects::nonNull)
+                    .toList();
+
+            if (pendingFinishingContainers.isEmpty()) {
+                return;
+            }
+            // Start transaction with close transit type.
+            final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
+            transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
+            final WindowContainerTransaction wct = transactionRecord.getTransaction();
+
+            forAllTaskContainers(taskContainer -> {
+                synchronized (mLock) {
+                    final List<TaskFragmentContainer> containers = taskContainer.mContainers;
+                    // Clean up the TaskFragmentContainers by the z-order from the lowest.
+                    for (int i = 0; i < containers.size() - 1; i++) {
+                        final TaskFragmentContainer container = containers.get(i);
+                        if (pendingFinishingContainers.contains(container)) {
+                            // Don't update records here to prevent double invocation.
+                            container.finish(false /* shouldFinishDependant */, mPresenter,
+                                    wct, this, false /* shouldRemoveRecord */);
+                        }
+                    }
+                    // Remove container records.
+                    removeContainers(taskContainer, pendingFinishingContainers);
+                    // Update the change to the client side.
+                    updateContainersInTaskIfVisible(wct, taskContainer.getTaskId());
+                }
+            });
+
+            // Apply the transaction.
+            transactionRecord.apply(false /* shouldApplyIndependently */);
+        }
+    }
+
+    @Override
+    public void invalidateTopVisibleSplitAttributes() {
+        synchronized (mLock) {
+            WindowContainerTransaction wct = mTransactionManager.startNewTransaction()
+                    .getTransaction();
+            forAllTaskContainers(taskContainer -> {
+                synchronized (mLock) {
+                    updateContainersInTaskIfVisible(wct, taskContainer.getTaskId());
+                }
+            });
+            mTransactionManager.getCurrentTransactionRecord()
+                    .apply(false /* shouldApplyIndependently */);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void forAllTaskContainers(@NonNull Consumer<TaskContainer> callback) {
+        for (int i = mTaskContainers.size() - 1; i >= 0; --i) {
+            callback.accept(mTaskContainers.valueAt(i));
+        }
+    }
+
+    @Override
+    public void updateSplitAttributes(@NonNull IBinder splitInfoToken,
+            @NonNull SplitAttributes splitAttributes) {
+        synchronized (mLock) {
+            final SplitContainer splitContainer = getSplitContainer(splitInfoToken);
+            if (splitContainer == null) {
+                Log.w(TAG, "Cannot find SplitContainer for token:" + splitInfoToken);
+                return;
+            }
+            WindowContainerTransaction wct = mTransactionManager.startNewTransaction()
+                    .getTransaction();
+            if (updateSplitContainerIfNeeded(splitContainer, wct, splitAttributes)) {
+                splitContainer.updateDefaultSplitAttributes(splitAttributes);
+                mTransactionManager.getCurrentTransactionRecord()
+                        .apply(false /* shouldApplyIndependently */);
+            } else {
+                // Abort if the SplitContainer wasn't updated.
+                mTransactionManager.getCurrentTransactionRecord().abort();
+            }
+        }
+    }
+
     /**
      * Called when the transaction is ready so that the organizer can update the TaskFragments based
      * on the changes in transaction.
@@ -432,6 +525,9 @@
             // All overrides will be cleanup.
             container.setLastRequestedBounds(null /* bounds */);
             container.setLastRequestedWindowingMode(WINDOWING_MODE_UNDEFINED);
+            container.clearLastAdjacentTaskFragment();
+            container.setLastCompanionTaskFragment(null /* fragmentToken */);
+            container.setLastRequestAnimationParams(TaskFragmentAnimationParams.DEFAULT);
             cleanupForEnterPip(wct, container);
         } else if (wasInPip) {
             // Exit PIP.
@@ -648,35 +744,6 @@
         }
     }
 
-    /** Returns whether the given {@link TaskContainer} may show in split. */
-    // Suppress GuardedBy warning because lint asks to mark this method as
-    // @GuardedBy(mPresenter.mController.mLock), which is mLock itself
-    @SuppressWarnings("GuardedBy")
-    @GuardedBy("mLock")
-    private boolean mayShowSplit(@NonNull TaskContainer taskContainer) {
-        // No split inside PIP.
-        if (taskContainer.isInPictureInPicture()) {
-            return false;
-        }
-        // Always assume the TaskContainer if SplitAttributesCalculator is set
-        if (mSplitAttributesCalculator != null) {
-            return true;
-        }
-        // Check if the parent container bounds can support any split rule.
-        for (EmbeddingRule rule : mSplitRules) {
-            if (!(rule instanceof SplitRule)) {
-                continue;
-            }
-            final SplitRule splitRule = (SplitRule) rule;
-            final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(
-                    taskContainer.getTaskProperties(), splitRule, null /* minDimensionsPair */);
-            if (shouldShowSplit(splitAttributes)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     @VisibleForTesting
     @GuardedBy("mLock")
     void onActivityCreated(@NonNull WindowContainerTransaction wct,
@@ -1360,20 +1427,33 @@
      * Removes the container from bookkeeping records.
      */
     void removeContainer(@NonNull TaskFragmentContainer container) {
+        removeContainers(container.getTaskContainer(), Collections.singletonList(container));
+    }
+
+    /**
+     * Removes containers from bookkeeping records.
+     */
+    void removeContainers(@NonNull TaskContainer taskContainer,
+            @NonNull List<TaskFragmentContainer> containers) {
         // Remove all split containers that included this one
-        final TaskContainer taskContainer = container.getTaskContainer();
-        taskContainer.mContainers.remove(container);
+        taskContainer.mContainers.removeAll(containers);
         // Marked as a pending removal which will be removed after it is actually removed on the
         // server side (#onTaskFragmentVanished).
         // In this way, we can keep track of the Task bounds until we no longer have any
         // TaskFragment there.
-        taskContainer.mFinishedContainer.add(container.getTaskFragmentToken());
+        taskContainer.mFinishedContainer.addAll(containers.stream().map(
+                TaskFragmentContainer::getTaskFragmentToken).toList());
 
         // Cleanup any split references.
         final List<SplitContainer> containersToRemove = new ArrayList<>();
         for (SplitContainer splitContainer : taskContainer.mSplitContainers) {
-            if (container.equals(splitContainer.getSecondaryContainer())
-                    || container.equals(splitContainer.getPrimaryContainer())) {
+            if (containersToRemove.contains(splitContainer)) {
+                // Don't need to check because it has been in the remove list.
+                continue;
+            }
+            if (containers.stream().anyMatch(container ->
+                    splitContainer.getPrimaryContainer().equals(container)
+                            || splitContainer.getSecondaryContainer().equals(container))) {
                 containersToRemove.add(splitContainer);
             }
         }
@@ -1381,7 +1461,7 @@
 
         // Cleanup any dependent references.
         for (TaskFragmentContainer containerToUpdate : taskContainer.mContainers) {
-            containerToUpdate.removeContainerToFinishOnExit(container);
+            containerToUpdate.removeContainersToFinishOnExit(containers);
         }
     }
 
@@ -1461,26 +1541,53 @@
         if (splitContainer == null) {
             return;
         }
+
+        updateSplitContainerIfNeeded(splitContainer, wct, null /* splitAttributes */);
+    }
+
+    /**
+     * Updates {@link SplitContainer} with the given {@link SplitAttributes} if the
+     * {@link SplitContainer} is the top most and not finished. If passed {@link SplitAttributes}
+     * are {@code null}, the {@link SplitAttributes} will be calculated with
+     * {@link SplitPresenter#computeSplitAttributes(TaskContainer.TaskProperties, SplitRule, Pair)}.
+     *
+     * @param splitContainer The {@link SplitContainer} to update
+     * @param splitAttributes Update with this {@code splitAttributes} if it is not {@code null}.
+     *                        Otherwise, use the value calculated by
+     *                        {@link SplitPresenter#computeSplitAttributes(
+     *                        TaskContainer.TaskProperties, SplitRule, Pair)}
+     *
+     * @return {@code true} if the update succeed. Otherwise, returns {@code false}.
+     */
+    @GuardedBy("mLock")
+    private boolean updateSplitContainerIfNeeded(@NonNull SplitContainer splitContainer,
+            @NonNull WindowContainerTransaction wct, @Nullable SplitAttributes splitAttributes) {
         if (!isTopMostSplit(splitContainer)) {
             // Skip position update - it isn't the topmost split.
-            return;
+            return false;
         }
         if (splitContainer.getPrimaryContainer().isFinished()
                 || splitContainer.getSecondaryContainer().isFinished()) {
             // Skip position update - one or both containers are finished.
-            return;
+            return false;
         }
-        final TaskContainer taskContainer = splitContainer.getTaskContainer();
-        final SplitRule splitRule = splitContainer.getSplitRule();
-        final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair();
-        final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(
-                taskContainer.getTaskProperties(), splitRule, minDimensionsPair);
-        splitContainer.setSplitAttributes(splitAttributes);
+        if (splitAttributes == null) {
+            final TaskContainer.TaskProperties taskProperties = splitContainer.getTaskContainer()
+                    .getTaskProperties();
+            final SplitRule splitRule = splitContainer.getSplitRule();
+            final SplitAttributes defaultSplitAttributes = splitContainer
+                    .getDefaultSplitAttributes();
+            final Pair<Size, Size> minDimensionsPair = splitContainer.getMinDimensionsPair();
+            splitAttributes = mPresenter.computeSplitAttributes(taskProperties, splitRule,
+                    defaultSplitAttributes, minDimensionsPair);
+        }
+        splitContainer.updateCurrentSplitAttributes(splitAttributes);
         if (dismissPlaceholderIfNecessary(wct, splitContainer)) {
             // Placeholder was finished, the positions will be updated when its container is emptied
-            return;
+            return true;
         }
-        mPresenter.updateSplitContainer(splitContainer, container, wct);
+        mPresenter.updateSplitContainer(splitContainer, wct);
+        return true;
     }
 
     /** Whether the given split is the topmost split in the Task. */
@@ -1576,7 +1683,7 @@
         final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity,
                 placeholderRule.getPlaceholderIntent());
         final SplitAttributes splitAttributes = mPresenter.computeSplitAttributes(taskProperties,
-                placeholderRule, minDimensionsPair);
+                placeholderRule, placeholderRule.getDefaultSplitAttributes(), minDimensionsPair);
         if (!SplitPresenter.shouldShowSplit(splitAttributes)) {
             return false;
         }
@@ -1655,7 +1762,7 @@
             // The placeholder should remain after it was first shown.
             return false;
         }
-        final SplitAttributes splitAttributes = splitContainer.getSplitAttributes();
+        final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes();
         if (SplitPresenter.shouldShowSplit(splitAttributes)) {
             return false;
         }
@@ -1799,6 +1906,20 @@
 
     @Nullable
     @GuardedBy("mLock")
+    SplitContainer getSplitContainer(@NonNull IBinder token) {
+        for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
+            final List<SplitContainer> containers = mTaskContainers.valueAt(i).mSplitContainers;
+            for (SplitContainer container : containers) {
+                if (container.getToken().equals(token)) {
+                    return container;
+                }
+            }
+        }
+        return null;
+    }
+
+    @Nullable
+    @GuardedBy("mLock")
     TaskContainer getTaskContainer(int taskId) {
         return mTaskContainers.get(taskId);
     }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 2b93682..0408511 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -179,7 +179,7 @@
         final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(
                 primaryActivity, secondaryIntent);
         final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule,
-                minDimensionsPair);
+                rule.getDefaultSplitAttributes(), minDimensionsPair);
         final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
                 splitAttributes);
         final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
@@ -225,7 +225,7 @@
         final Pair<Size, Size> minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity,
                 secondaryActivity);
         final SplitAttributes splitAttributes = computeSplitAttributes(taskProperties, rule,
-                minDimensionsPair);
+                rule.getDefaultSplitAttributes(), minDimensionsPair);
         final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
                 splitAttributes);
         final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
@@ -334,11 +334,9 @@
     /**
      * Updates the positions of containers in an existing split.
      * @param splitContainer The split container to be updated.
-     * @param updatedContainer The task fragment that was updated and caused this split update.
      * @param wct WindowContainerTransaction that this update should be performed with.
      */
     void updateSplitContainer(@NonNull SplitContainer splitContainer,
-            @NonNull TaskFragmentContainer updatedContainer,
             @NonNull WindowContainerTransaction wct) {
         // Getting the parent configuration using the updated container - it will have the recent
         // value.
@@ -348,8 +346,9 @@
         if (activity == null) {
             return;
         }
-        final TaskProperties taskProperties = getTaskProperties(updatedContainer);
-        final SplitAttributes splitAttributes = splitContainer.getSplitAttributes();
+        final TaskContainer taskContainer = splitContainer.getTaskContainer();
+        final TaskProperties taskProperties = taskContainer.getTaskProperties();
+        final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes();
         final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
                 splitAttributes);
         final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties,
@@ -370,7 +369,6 @@
             // When placeholder is shown in split, we should keep the focus on the primary.
             wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
         }
-        final TaskContainer taskContainer = updatedContainer.getTaskContainer();
         final int windowingMode = taskContainer.getWindowingModeForSplitTaskFragment(
                 primaryRelBounds);
         updateTaskFragmentWindowingModeIfRegistered(wct, primaryContainer, windowingMode);
@@ -387,10 +385,9 @@
         // secondaryContainer could not be finished.
         boolean isStacked = !shouldShowSplit(splitAttributes);
         if (isStacked) {
-            setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
-                    null /* secondary */, null /* splitRule */);
+            clearAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken());
         } else {
-            setAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken(),
+            setAdjacentTaskFragmentsWithRule(wct, primaryContainer.getTaskFragmentToken(),
                     secondaryContainer.getTaskFragmentToken(), splitRule);
         }
         setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(),
@@ -427,7 +424,7 @@
                 fragmentOptions.getFragmentToken());
         if (container == null) {
             throw new IllegalStateException(
-                    "Creating a task fragment that is not registered with controller.");
+                    "Creating a TaskFragment that is not registered with controller.");
         }
 
         container.setLastRequestedBounds(fragmentOptions.getInitialRelativeBounds());
@@ -441,7 +438,7 @@
         TaskFragmentContainer container = mController.getContainer(fragmentToken);
         if (container == null) {
             throw new IllegalStateException(
-                    "Resizing a task fragment that is not registered with controller.");
+                    "Resizing a TaskFragment that is not registered with controller.");
         }
 
         if (container.areLastRequestedBoundsEqual(relBounds)) {
@@ -458,7 +455,7 @@
             @NonNull IBinder fragmentToken, @WindowingMode int windowingMode) {
         final TaskFragmentContainer container = mController.getContainer(fragmentToken);
         if (container == null) {
-            throw new IllegalStateException("Setting windowing mode for a task fragment that is"
+            throw new IllegalStateException("Setting windowing mode for a TaskFragment that is"
                     + " not registered with controller.");
         }
 
@@ -476,7 +473,7 @@
             @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) {
         final TaskFragmentContainer container = mController.getContainer(fragmentToken);
         if (container == null) {
-            throw new IllegalStateException("Setting animation params for a task fragment that is"
+            throw new IllegalStateException("Setting animation params for a TaskFragment that is"
                     + " not registered with controller.");
         }
 
@@ -489,6 +486,64 @@
         super.updateAnimationParams(wct, fragmentToken, animationParams);
     }
 
+    @Override
+    void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder primary, @NonNull IBinder secondary,
+            @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams) {
+        final TaskFragmentContainer primaryContainer = mController.getContainer(primary);
+        final TaskFragmentContainer secondaryContainer = mController.getContainer(secondary);
+        if (primaryContainer == null || secondaryContainer == null) {
+            throw new IllegalStateException("setAdjacentTaskFragments on TaskFragment that is"
+                    + " not registered with controller.");
+        }
+
+        if (primaryContainer.isLastAdjacentTaskFragmentEqual(secondary, adjacentParams)
+                && secondaryContainer.isLastAdjacentTaskFragmentEqual(primary, adjacentParams)) {
+            // Return early if the same adjacent TaskFragments were already requested
+            return;
+        }
+
+        primaryContainer.setLastAdjacentTaskFragment(secondary, adjacentParams);
+        secondaryContainer.setLastAdjacentTaskFragment(primary, adjacentParams);
+        super.setAdjacentTaskFragments(wct, primary, secondary, adjacentParams);
+    }
+
+    @Override
+    void clearAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
+            @NonNull IBinder fragmentToken) {
+        final TaskFragmentContainer container = mController.getContainer(fragmentToken);
+        if (container == null) {
+            throw new IllegalStateException("clearAdjacentTaskFragments on TaskFragment that is"
+                    + " not registered with controller.");
+        }
+
+        if (container.isLastAdjacentTaskFragmentEqual(null /* fragmentToken*/, null /* params */)) {
+            // Return early if no adjacent TaskFragment was yet requested
+            return;
+        }
+
+        container.clearLastAdjacentTaskFragment();
+        super.clearAdjacentTaskFragments(wct, fragmentToken);
+    }
+
+    @Override
+    void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary,
+            @Nullable IBinder secondary) {
+        final TaskFragmentContainer container = mController.getContainer(primary);
+        if (container == null) {
+            throw new IllegalStateException("setCompanionTaskFragment on TaskFragment that is"
+                    + " not registered with controller.");
+        }
+
+        if (container.isLastCompanionTaskFragmentEqual(secondary)) {
+            // Return early if the same companion TaskFragment was already requested
+            return;
+        }
+
+        container.setLastCompanionTaskFragment(secondary);
+        super.setCompanionTaskFragment(wct, primary, secondary);
+    }
+
     /**
      * Expands the split container if the current split bounds are smaller than the Activity or
      * Intent that is added to the container.
@@ -515,9 +570,9 @@
         // Expand the splitContainer if minimum dimensions are not satisfied.
         final TaskContainer taskContainer = splitContainer.getTaskContainer();
         final SplitAttributes splitAttributes = sanitizeSplitAttributes(
-                taskContainer.getTaskProperties(), splitContainer.getSplitAttributes(),
+                taskContainer.getTaskProperties(), splitContainer.getCurrentSplitAttributes(),
                 minDimensionsPair);
-        splitContainer.setSplitAttributes(splitAttributes);
+        splitContainer.updateCurrentSplitAttributes(splitAttributes);
         if (!shouldShowSplit(splitAttributes)) {
             // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment
             // bounds. Return failure to create a new SplitContainer which fills task bounds.
@@ -540,7 +595,7 @@
     }
 
     static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) {
-        return shouldShowSplit(splitContainer.getSplitAttributes());
+        return shouldShowSplit(splitContainer.getCurrentSplitAttributes());
     }
 
     static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) {
@@ -549,12 +604,12 @@
 
     @NonNull
     SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties,
-            @NonNull SplitRule rule, @Nullable Pair<Size, Size> minDimensionsPair) {
+            @NonNull SplitRule rule, @NonNull SplitAttributes defaultSplitAttributes,
+            @Nullable Pair<Size, Size> minDimensionsPair) {
         final Configuration taskConfiguration = taskProperties.getConfiguration();
         final WindowMetrics taskWindowMetrics = getTaskWindowMetrics(taskConfiguration);
         final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
                 mController.getSplitAttributesCalculator();
-        final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes();
         final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics);
         if (calculator == null) {
             if (!areDefaultConstraintsSatisfied) {
@@ -957,11 +1012,6 @@
     }
 
     @NonNull
-    static TaskProperties getTaskProperties(@NonNull TaskFragmentContainer container) {
-        return container.getTaskContainer().getTaskProperties();
-    }
-
-    @NonNull
     TaskProperties getTaskProperties(@NonNull Activity activity) {
         final TaskContainer taskContainer = mController.getTaskContainer(
                 mController.getTaskId(activity));
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index f41295b..4b15bb1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -108,12 +108,6 @@
     }
 
     @NonNull
-    Configuration getConfiguration() {
-        // Make a copy in case the config is updated unexpectedly.
-        return new Configuration(mConfiguration);
-    }
-
-    @NonNull
     TaskProperties getTaskProperties() {
         return new TaskProperties(mDisplayId, mConfiguration);
     }
@@ -157,7 +151,7 @@
 
     @WindowingMode
     private int getWindowingMode() {
-        return getConfiguration().windowConfiguration.getWindowingMode();
+        return mConfiguration.windowConfiguration.getWindowingMode();
     }
 
     /** Whether there is any {@link TaskFragmentContainer} below this Task. */
@@ -220,10 +214,7 @@
         }
     }
 
-    /**
-     * A wrapper class which contains the display ID and {@link Configuration} of a
-     * {@link TaskContainer}
-     */
+    /** A wrapper class which contains the information of {@link TaskContainer} */
     static final class TaskProperties {
         private final int mDisplayId;
         @NonNull
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 38ac719..b38f824 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -37,8 +37,10 @@
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Client-side container for a stack of activities. Corresponds to an instance of TaskFragment
@@ -116,6 +118,27 @@
     private TaskFragmentAnimationParams mLastAnimationParams = TaskFragmentAnimationParams.DEFAULT;
 
     /**
+     * TaskFragment token that was requested last via
+     * {@link android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS}.
+     */
+    @Nullable
+    private IBinder mLastAdjacentTaskFragment;
+
+    /**
+     * {@link WindowContainerTransaction.TaskFragmentAdjacentParams} token that was requested last
+     * via {@link android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS}.
+     */
+    @Nullable
+    private WindowContainerTransaction.TaskFragmentAdjacentParams mLastAdjacentParams;
+
+    /**
+     * TaskFragment token that was requested last via
+     * {@link android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT}.
+     */
+    @Nullable
+    private IBinder mLastCompanionTaskFragment;
+
+    /**
      * When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment
      * if it is still empty after the timeout.
      */
@@ -436,10 +459,17 @@
      * Removes a container that should be finished when this container is finished.
      */
     void removeContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToRemove) {
+        removeContainersToFinishOnExit(Collections.singletonList(containerToRemove));
+    }
+
+    /**
+     * Removes container list that should be finished when this container is finished.
+     */
+    void removeContainersToFinishOnExit(@NonNull List<TaskFragmentContainer> containersToRemove) {
         if (mIsFinished) {
             return;
         }
-        mContainersToFinishOnExit.remove(containerToRemove);
+        mContainersToFinishOnExit.removeAll(containersToRemove);
     }
 
     /**
@@ -478,6 +508,16 @@
     @GuardedBy("mController.mLock")
     void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter,
             @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) {
+        finish(shouldFinishDependent, presenter, wct, controller, true /* shouldRemoveRecord */);
+    }
+
+    /**
+     * Removes all activities that belong to this process and finishes other containers/activities
+     * configured to finish together.
+     */
+    void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter,
+            @NonNull WindowContainerTransaction wct, @NonNull SplitController controller,
+            boolean shouldRemoveRecord) {
         if (!mIsFinished) {
             mIsFinished = true;
             if (mAppearEmptyTimeout != null) {
@@ -494,8 +534,10 @@
 
         // Cleanup the visuals
         presenter.deleteTaskFragment(wct, getTaskFragmentToken());
-        // Cleanup the records
-        controller.removeContainer(this);
+        if (shouldRemoveRecord) {
+            // Cleanup the records
+            controller.removeContainer(this);
+        }
         // Clean up task fragment information
         mInfo = null;
     }
@@ -571,6 +613,7 @@
     /**
      * Checks if last requested bounds are equal to the provided value.
      * The requested bounds are relative bounds in parent coordinate.
+     * @see WindowContainerTransaction#setRelativeBounds
      */
     boolean areLastRequestedBoundsEqual(@Nullable Rect relBounds) {
         return (relBounds == null && mLastRequestedBounds.isEmpty())
@@ -580,6 +623,7 @@
     /**
      * Updates the last requested bounds.
      * The requested bounds are relative bounds in parent coordinate.
+     * @see WindowContainerTransaction#setRelativeBounds
      */
     void setLastRequestedBounds(@Nullable Rect relBounds) {
         if (relBounds == null) {
@@ -589,13 +633,9 @@
         }
     }
 
-    @NonNull
-    Rect getLastRequestedBounds() {
-        return mLastRequestedBounds;
-    }
-
     /**
      * Checks if last requested windowing mode is equal to the provided value.
+     * @see WindowContainerTransaction#setWindowingMode
      */
     boolean isLastRequestedWindowingModeEqual(@WindowingMode int windowingMode) {
         return mLastRequestedWindowingMode == windowingMode;
@@ -603,6 +643,7 @@
 
     /**
      * Updates the last requested windowing mode.
+     * @see WindowContainerTransaction#setWindowingMode
      */
     void setLastRequestedWindowingMode(@WindowingMode int windowingModes) {
         mLastRequestedWindowingMode = windowingModes;
@@ -610,6 +651,7 @@
 
     /**
      * Checks if last requested {@link TaskFragmentAnimationParams} are equal to the provided value.
+     * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ANIMATION_PARAMS
      */
     boolean areLastRequestedAnimationParamsEqual(
             @NonNull TaskFragmentAnimationParams animationParams) {
@@ -618,11 +660,66 @@
 
     /**
      * Updates the last requested {@link TaskFragmentAnimationParams}.
+     * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ANIMATION_PARAMS
      */
     void setLastRequestAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) {
         mLastAnimationParams = animationParams;
     }
 
+    /**
+     * Checks if last requested adjacent TaskFragment token and params are equal to the provided
+     * values.
+     * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS
+     * @see android.window.TaskFragmentOperation#OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS
+     */
+    boolean isLastAdjacentTaskFragmentEqual(@Nullable IBinder fragmentToken,
+            @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams params) {
+        return Objects.equals(mLastAdjacentTaskFragment, fragmentToken)
+                && Objects.equals(mLastAdjacentParams, params);
+    }
+
+    /**
+     * Updates the last requested adjacent TaskFragment token and params.
+     * @see android.window.TaskFragmentOperation#OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS
+     */
+    void setLastAdjacentTaskFragment(@NonNull IBinder fragmentToken,
+            @NonNull WindowContainerTransaction.TaskFragmentAdjacentParams params) {
+        mLastAdjacentTaskFragment = fragmentToken;
+        mLastAdjacentParams = params;
+    }
+
+    /**
+     * Clears the last requested adjacent TaskFragment token and params.
+     * @see android.window.TaskFragmentOperation#OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS
+     */
+    void clearLastAdjacentTaskFragment() {
+        final TaskFragmentContainer lastAdjacentTaskFragment = mLastAdjacentTaskFragment != null
+                ? mController.getContainer(mLastAdjacentTaskFragment)
+                : null;
+        mLastAdjacentTaskFragment = null;
+        mLastAdjacentParams = null;
+        if (lastAdjacentTaskFragment != null) {
+            // Clear the previous adjacent TaskFragment as well.
+            lastAdjacentTaskFragment.clearLastAdjacentTaskFragment();
+        }
+    }
+
+    /**
+     * Checks if last requested companion TaskFragment token is equal to the provided value.
+     * @see android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT
+     */
+    boolean isLastCompanionTaskFragmentEqual(@Nullable IBinder fragmentToken) {
+        return Objects.equals(mLastCompanionTaskFragment, fragmentToken);
+    }
+
+    /**
+     * Updates the last requested companion TaskFragment token.
+     * @see android.window.TaskFragmentOperation#OP_TYPE_SET_COMPANION_TASK_FRAGMENT
+     */
+    void setLastCompanionTaskFragment(@Nullable IBinder fragmentToken) {
+        mLastCompanionTaskFragment = fragmentToken;
+    }
+
     /** Gets the parent leaf Task id. */
     int getTaskId() {
         return mTaskContainer.getTaskId();
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index a26311e..17909d4 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -275,7 +275,8 @@
 
         assertNotNull(tf);
         assertNotNull(taskContainer);
-        assertEquals(TASK_BOUNDS, taskContainer.getConfiguration().windowConfiguration.getBounds());
+        assertEquals(TASK_BOUNDS, taskContainer.getTaskProperties().getConfiguration()
+                .windowConfiguration.getBounds());
     }
 
     @Test
@@ -288,7 +289,7 @@
         doReturn(true).when(tf).isEmpty();
         doReturn(true).when(mSplitController).launchPlaceholderIfNecessary(mTransaction,
                 mActivity, false /* isOnCreated */);
-        doNothing().when(mSplitPresenter).updateSplitContainer(any(), any(), any());
+        doNothing().when(mSplitPresenter).updateSplitContainer(any(), any());
 
         mSplitController.updateContainer(mTransaction, tf);
 
@@ -341,7 +342,7 @@
 
         mSplitController.updateContainer(mTransaction, tf);
 
-        verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any());
+        verify(mSplitPresenter, never()).updateSplitContainer(any(), any());
 
         // Verify if the top active split is updated if both of its containers are not finished.
         doReturn(false).when(mSplitController)
@@ -349,7 +350,7 @@
 
         mSplitController.updateContainer(mTransaction, tf);
 
-        verify(mSplitPresenter).updateSplitContainer(splitContainer, tf, mTransaction);
+        verify(mSplitPresenter).updateSplitContainer(splitContainer, mTransaction);
     }
 
     @Test
@@ -366,14 +367,14 @@
         doReturn(false).when(taskContainer).isVisible();
         mSplitController.updateContainer(mTransaction, taskFragmentContainer);
 
-        verify(mSplitPresenter, never()).updateSplitContainer(any(), any(), any());
+        verify(mSplitPresenter, never()).updateSplitContainer(any(), any());
 
         // Update the split when the Task is visible.
         doReturn(true).when(taskContainer).isVisible();
         mSplitController.updateContainer(mTransaction, taskFragmentContainer);
 
         verify(mSplitPresenter).updateSplitContainer(taskContainer.mSplitContainers.get(0),
-                taskFragmentContainer, mTransaction);
+                mTransaction);
     }
 
     @Test
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index a41e63f..8330156 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -177,6 +177,64 @@
     }
 
     @Test
+    public void testSetAdjacentTaskFragments() {
+        final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
+        final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+
+        mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(),
+                container1.getTaskFragmentToken(), null /* adjacentParams */);
+        verify(mTransaction).setAdjacentTaskFragments(container0.getTaskFragmentToken(),
+                container1.getTaskFragmentToken(), null /* adjacentParams */);
+
+        // No request to set the same adjacent TaskFragments.
+        clearInvocations(mTransaction);
+        mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(),
+                container1.getTaskFragmentToken(), null /* adjacentParams */);
+
+        verify(mTransaction, never()).setAdjacentTaskFragments(any(), any(), any());
+    }
+
+    @Test
+    public void testClearAdjacentTaskFragments() {
+        final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
+        final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+
+        // No request to clear as it is not set by default.
+        mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken());
+        verify(mTransaction, never()).clearAdjacentTaskFragments(any());
+
+        mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(),
+                container1.getTaskFragmentToken(), null /* adjacentParams */);
+        mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken());
+        verify(mTransaction).clearAdjacentTaskFragments(container0.getTaskFragmentToken());
+
+        // No request to clear on either of the previous cleared TasKFragments.
+        clearInvocations(mTransaction);
+        mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken());
+        mPresenter.clearAdjacentTaskFragments(mTransaction, container1.getTaskFragmentToken());
+
+        verify(mTransaction, never()).clearAdjacentTaskFragments(any());
+    }
+
+    @Test
+    public void testSetCompanionTaskFragment() {
+        final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
+        final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+
+        mPresenter.setCompanionTaskFragment(mTransaction, container0.getTaskFragmentToken(),
+                container1.getTaskFragmentToken());
+        verify(mTransaction).setCompanionTaskFragment(container0.getTaskFragmentToken(),
+                container1.getTaskFragmentToken());
+
+        // No request to set the same adjacent TaskFragments.
+        clearInvocations(mTransaction);
+        mPresenter.setCompanionTaskFragment(mTransaction, container0.getTaskFragmentToken(),
+                container1.getTaskFragmentToken());
+
+        verify(mTransaction, never()).setCompanionTaskFragment(any(), any());
+    }
+
+    @Test
     public void testUpdateAnimationParams() {
         final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
 
@@ -228,7 +286,7 @@
 
     @Test
     public void testGetRelBoundsForPosition_expandContainers() {
-        final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+        final TaskContainer.TaskProperties taskProperties = getTaskProperties();
         final SplitAttributes splitAttributes = new SplitAttributes.Builder()
                 .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType())
                 .build();
@@ -248,7 +306,7 @@
 
     @Test
     public void testGetRelBoundsForPosition_expandContainers_isRelativeToParent() {
-        final TaskContainer.TaskProperties taskProperties = getTaskProperty(
+        final TaskContainer.TaskProperties taskProperties = getTaskProperties(
                 new Rect(100, 100, 500, 1000));
         final SplitAttributes splitAttributes = new SplitAttributes.Builder()
                 .setSplitType(new SplitAttributes.SplitType.ExpandContainersSplitType())
@@ -273,7 +331,7 @@
                 false /* splitHorizontally */);
         final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
                 false /* splitHorizontally */);
-        final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+        final TaskContainer.TaskProperties taskProperties = getTaskProperties();
         SplitAttributes splitAttributes = new SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
                 .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
@@ -339,7 +397,7 @@
         // Offset TaskBounds to 100, 100. The returned rel bounds shouldn't be affected.
         final Rect taskBounds = new Rect(TASK_BOUNDS);
         taskBounds.offset(100, 100);
-        final TaskContainer.TaskProperties taskProperties = getTaskProperty(taskBounds);
+        final TaskContainer.TaskProperties taskProperties = getTaskProperties(taskBounds);
         SplitAttributes splitAttributes = new SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
                 .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
@@ -400,7 +458,7 @@
                 true /* splitHorizontally */);
         final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
                 true /* splitHorizontally */);
-        final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+        final TaskContainer.TaskProperties taskProperties = getTaskProperties();
         SplitAttributes splitAttributes = new SplitAttributes.Builder()
                 .setSplitType(SplitAttributes.SplitType.RatioSplitType.splitEqually())
                 .setLayoutDirection(SplitAttributes.LayoutDirection.TOP_TO_BOTTOM)
@@ -442,7 +500,7 @@
                 false /* splitHorizontally */);
         final Rect secondaryBounds = getSplitBounds(false /* isPrimary */,
                 false /* splitHorizontally */);
-        final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+        final TaskContainer.TaskProperties taskProperties = getTaskProperties();
         final SplitAttributes splitAttributes = new SplitAttributes.Builder()
                 .setSplitType(new SplitAttributes.SplitType.HingeSplitType(
                         SplitAttributes.SplitType.RatioSplitType.splitEqually()
@@ -506,7 +564,7 @@
 
     @Test
     public void testGetRelBoundsForPosition_fallbackToExpandContainers() {
-        final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+        final TaskContainer.TaskProperties taskProperties = getTaskProperties();
         final SplitAttributes splitAttributes = new SplitAttributes.Builder()
                 .setSplitType(new SplitAttributes.SplitType.HingeSplitType(
                         new SplitAttributes.SplitType.ExpandContainersSplitType()
@@ -528,7 +586,7 @@
 
     @Test
     public void testGetRelBoundsForPosition_useHingeSplitType() {
-        final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+        final TaskContainer.TaskProperties taskProperties = getTaskProperties();
         final SplitAttributes splitAttributes = new SplitAttributes.Builder()
                 .setSplitType(new SplitAttributes.SplitType.HingeSplitType(
                         new SplitAttributes.SplitType.ExpandContainersSplitType()
@@ -581,13 +639,13 @@
                 splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
         verify(mPresenter, never()).expandTaskFragment(any(), any());
 
-        splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
+        splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
         doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo();
         assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded(
                 mTransaction, splitContainer, mActivity, secondaryActivity,
                 null /* secondaryIntent */));
 
-        splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
+        splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
         primaryTf.setInfo(mTransaction, createMockTaskFragmentInfo(primaryTf, mActivity));
         secondaryTf.setInfo(mTransaction,
                 createMockTaskFragmentInfo(secondaryTf, secondaryActivity));
@@ -597,7 +655,7 @@
         verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
         verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
 
-        splitContainer.setSplitAttributes(SPLIT_ATTRIBUTES);
+        splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
         clearInvocations(mPresenter);
 
         assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
@@ -639,37 +697,35 @@
                 .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
                 .setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
                 .build();
-        final TaskContainer.TaskProperties taskProperties = getTaskProperty();
+        final TaskContainer.TaskProperties taskProperties = getTaskProperties();
 
         assertEquals(SPLIT_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
-                splitPairRule, null /* minDimensionsPair */));
+                splitPairRule, SPLIT_ATTRIBUTES,  null /* minDimensionsPair */));
 
         final Pair<Size, Size> minDimensionsPair = new Pair<>(
                 new Size(TASK_BOUNDS.width(), TASK_BOUNDS.height()), null);
 
         assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
-                splitPairRule, minDimensionsPair));
+                splitPairRule, SPLIT_ATTRIBUTES, minDimensionsPair));
 
         taskProperties.getConfiguration().windowConfiguration.setBounds(new Rect(
                 TASK_BOUNDS.left + 1, TASK_BOUNDS.top + 1, TASK_BOUNDS.right + 1,
                 TASK_BOUNDS.bottom + 1));
 
         assertEquals(EXPAND_CONTAINERS_ATTRIBUTES, mPresenter.computeSplitAttributes(taskProperties,
-                splitPairRule, null /* minDimensionsPair */));
+                splitPairRule, SPLIT_ATTRIBUTES, null /* minDimensionsPair */));
 
         final SplitAttributes splitAttributes = new SplitAttributes.Builder()
-                .setSplitType(
-                        new SplitAttributes.SplitType.HingeSplitType(
-                                SplitAttributes.SplitType.RatioSplitType.splitEqually()
-                        )
-                ).build();
+                .setSplitType(new SplitAttributes.SplitType.HingeSplitType(
+                        SplitAttributes.SplitType.RatioSplitType.splitEqually()))
+                .build();
         final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
                 params -> splitAttributes;
 
         mController.setSplitAttributesCalculator(calculator);
 
         assertEquals(splitAttributes, mPresenter.computeSplitAttributes(taskProperties,
-                splitPairRule, null /* minDimensionsPair */));
+                splitPairRule, SPLIT_ATTRIBUTES, null /* minDimensionsPair */));
     }
 
     @Test
@@ -696,14 +752,15 @@
         doReturn(activityConfig).when(mActivityResources).getConfiguration();
         doReturn(new ActivityInfo()).when(activity).getActivityInfo();
         doReturn(mock(IBinder.class)).when(activity).getActivityToken();
+        doReturn(TASK_ID).when(activity).getTaskId();
         return activity;
     }
 
-    private static TaskContainer.TaskProperties getTaskProperty() {
-        return getTaskProperty(TASK_BOUNDS);
+    private static TaskContainer.TaskProperties getTaskProperties() {
+        return getTaskProperties(TASK_BOUNDS);
     }
 
-    private static TaskContainer.TaskProperties getTaskProperty(@NonNull Rect taskBounds) {
+    private static TaskContainer.TaskProperties getTaskProperties(@NonNull Rect taskBounds) {
         final Configuration configuration = new Configuration();
         configuration.windowConfiguration.setBounds(taskBounds);
         return new TaskContainer.TaskProperties(DEFAULT_DISPLAY, configuration);
diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar
index 7a6f46c..378ad81 100644
--- a/libs/WindowManager/Jetpack/window-extensions-release.aar
+++ b/libs/WindowManager/Jetpack/window-extensions-release.aar
Binary files differ
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index 0a755f0..cbebae9 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -41,6 +41,7 @@
 #include <SkHighContrastFilter.h>
 #include <SkImageEncoder.h>
 #include <SkImagePriv.h>
+#include <SkJpegGainmapEncoder.h>
 #include <SkPixmap.h>
 #include <SkRect.h>
 #include <SkStream.h>
@@ -458,6 +459,16 @@
 }
 
 bool Bitmap::compress(JavaCompressFormat format, int32_t quality, SkWStream* stream) {
+#ifdef __ANDROID__  // TODO: This isn't built for host for some reason?
+    if (hasGainmap() && format == JavaCompressFormat::Jpeg) {
+        SkBitmap baseBitmap = getSkBitmap();
+        SkBitmap gainmapBitmap = gainmap()->bitmap->getSkBitmap();
+        SkJpegEncoder::Options options{.fQuality = quality};
+        return SkJpegGainmapEncoder::EncodeJpegR(stream, baseBitmap.pixmap(), options,
+                                                 gainmapBitmap.pixmap(), options, gainmap()->info);
+    }
+#endif
+
     SkBitmap skbitmap;
     getSkBitmap(&skbitmap);
     return compress(skbitmap, format, quality, stream);
diff --git a/media/java/android/media/tv/TvRecordingClient.java b/media/java/android/media/tv/TvRecordingClient.java
index b1356f5..cdeef2b 100644
--- a/media/java/android/media/tv/TvRecordingClient.java
+++ b/media/java/android/media/tv/TvRecordingClient.java
@@ -575,7 +575,7 @@
                 Log.w(TAG, "onError - session not created");
                 return;
             }
-            if (mCallback == null) {
+            if (mCallback != null) {
                 mCallback.onError(error);
             }
             if (mTvIAppView != null) {
diff --git a/packages/CredentialManager/Android.bp b/packages/CredentialManager/Android.bp
index 2cb3468..00d42bd 100644
--- a/packages/CredentialManager/Android.bp
+++ b/packages/CredentialManager/Android.bp
@@ -31,6 +31,7 @@
         "androidx.compose.ui_ui",
         "androidx.compose.ui_ui-tooling",
         "androidx.core_core-ktx",
+        "androidx.credentials_credentials",
         "androidx.lifecycle_lifecycle-extensions",
         "androidx.lifecycle_lifecycle-livedata",
         "androidx.lifecycle_lifecycle-runtime-ktx",
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 7b53e26..c66ea5e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -16,12 +16,10 @@
 
 package com.android.credentialmanager
 
-import android.app.PendingIntent
 import android.app.slice.Slice
 import android.app.slice.SliceSpec
 import android.content.Context
 import android.content.Intent
-import android.content.pm.SigningInfo
 import android.credentials.CreateCredentialRequest
 import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
 import android.credentials.CredentialOption
@@ -40,18 +38,15 @@
 import android.os.Binder
 import android.os.Bundle
 import android.os.ResultReceiver
-import android.service.credentials.CredentialProviderService
 import com.android.credentialmanager.createflow.DisabledProviderInfo
 import com.android.credentialmanager.createflow.EnabledProviderInfo
 import com.android.credentialmanager.createflow.RequestDisplayInfo
 import com.android.credentialmanager.getflow.GetCredentialUiState
-import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toCredentialDataBundle
-import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
-import com.android.credentialmanager.jetpack.provider.Action
-import com.android.credentialmanager.jetpack.provider.CreateEntry
-import com.android.credentialmanager.jetpack.provider.CredentialCountInformation
-import com.android.credentialmanager.jetpack.provider.CredentialEntry
+import androidx.credentials.CreateCredentialRequest.DisplayInfo
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CreatePasswordRequest
+
+import java.time.Instant
 
 // Consider repo per screen, similar to view model?
 class CredentialManagerRepo(
@@ -180,14 +175,14 @@
                 .Builder("io.enpass.app")
                 .setSaveEntries(
                     listOf<Entry>(
-                        newCreateEntry(
+                        CreateTestUtils.newCreateEntry(context,
                             "key1", "subkey-1", "elisa.beckett@gmail.com",
-                            20, 7, 27, 10L,
-                            "Optional footer description"
+                            20, 7, 27, Instant.ofEpochSecond(10L),
+                            "Legal note"
                         ),
-                        newCreateEntry(
+                        CreateTestUtils.newCreateEntry(context,
                             "key1", "subkey-2", "elisa.work@google.com",
-                            20, 7, 27, 12L,
+                            20, 7, 27, Instant.ofEpochSecond(12L),
                             null
                         ),
                     )
@@ -200,14 +195,14 @@
                 .Builder("com.dashlane")
                 .setSaveEntries(
                     listOf<Entry>(
-                        newCreateEntry(
+                        CreateTestUtils.newCreateEntry(context,
                             "key1", "subkey-3", "elisa.beckett@dashlane.com",
-                            20, 7, 27, 11L,
+                            20, 7, 27, Instant.ofEpochSecond(11L),
                             null
                         ),
-                        newCreateEntry(
+                        CreateTestUtils.newCreateEntry(context,
                             "key1", "subkey-4", "elisa.work@dashlane.com",
-                            20, 7, 27, 14L,
+                            20, 7, 27, Instant.ofEpochSecond(14L),
                             null
                         ),
                     )
@@ -228,33 +223,33 @@
             GetCredentialProviderData.Builder("io.enpass.app")
                 .setCredentialEntries(
                     listOf<Entry>(
-                        newGetEntry(
-                            "key1", "subkey-1", TYPE_PASSWORD_CREDENTIAL, "Password",
-                            "elisa.family@outlook.com", null, 3L
+                        GetTestUtils.newPasswordEntry(
+                            context, "key1", "subkey-1", "elisa.family@outlook.com", null,
+                            Instant.ofEpochSecond(8000L)
                         ),
-                        newGetEntry(
-                            "key1", "subkey-1", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
-                            "elisa.bakery@gmail.com", "Elisa Beckett", 0L
+                        GetTestUtils.newPasskeyEntry(
+                            context, "key1", "subkey-1", "elisa.bakery@gmail.com", "Elisa Beckett",
+                            null
                         ),
-                        newGetEntry(
-                            "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password",
-                            "elisa.bakery@gmail.com", null, 10L
+                        GetTestUtils.newPasswordEntry(
+                            context, "key1", "subkey-2", "elisa.bakery@gmail.com", null,
+                            Instant.ofEpochSecond(10000L)
                         ),
-                        newGetEntry(
-                            "key1", "subkey-3", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
-                            "elisa.family@outlook.com", "Elisa Beckett", 1L
+                        GetTestUtils.newPasskeyEntry(
+                            context, "key1", "subkey-3", "elisa.family@outlook.com",
+                            "Elisa Beckett", Instant.ofEpochSecond(500L)
                         ),
                     )
                 ).setAuthenticationEntry(
-                    newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
+                    GetTestUtils.newAuthenticationEntry(context, "key2", "subkey-1")
                 ).setActionChips(
                     listOf(
-                        newActionEntry(
-                            "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
+                        GetTestUtils.newActionEntry(
+                            context, "key3", "subkey-1",
                             "Open Google Password Manager", "elisa.beckett@gmail.com"
                         ),
-                        newActionEntry(
-                            "key3", "subkey-2", TYPE_PASSWORD_CREDENTIAL,
+                        GetTestUtils.newActionEntry(
+                            context, "key3", "subkey-2",
                             "Open Google Password Manager", "beckett-family@gmail.com"
                         ),
                     )
@@ -264,137 +259,29 @@
             GetCredentialProviderData.Builder("com.dashlane")
                 .setCredentialEntries(
                     listOf<Entry>(
-                        newGetEntry(
-                            "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password",
-                            "elisa.family@outlook.com", null, 4L
+                        GetTestUtils.newPasswordEntry(
+                            context, "key1", "subkey-2", "elisa.family@outlook.com", null,
+                            Instant.ofEpochSecond(9000L)
                         ),
-                        newGetEntry(
-                            "key1", "subkey-3", TYPE_PASSWORD_CREDENTIAL, "Password",
-                            "elisa.work@outlook.com", null, 11L
+                        GetTestUtils.newPasswordEntry(
+                            context, "key1", "subkey-3", "elisa.work@outlook.com", null,
+                            Instant.ofEpochSecond(11000L)
                         ),
                     )
                 ).setAuthenticationEntry(
-                    newAuthenticationEntry("key2", "subkey-1", TYPE_PASSWORD_CREDENTIAL)
+                    GetTestUtils.newAuthenticationEntry(context, "key2", "subkey-1")
                 ).setActionChips(
                     listOf(
-                        newActionEntry(
-                            "key3", "subkey-1", TYPE_PASSWORD_CREDENTIAL,
-                            "Open Enpass"
+                        GetTestUtils.newActionEntry(
+                            context, "key3", "subkey-1", "Open Enpass",
+                            "Manage passwords"
                         ),
                     )
                 ).build(),
         )
     }
 
-    private fun newActionEntry(
-        key: String,
-        subkey: String,
-        credentialType: String,
-        text: String,
-        subtext: String? = null,
-    ): Entry {
-        val action = Action(text, subtext, null)
 
-        return Entry(
-            key,
-            subkey,
-            Action.toSlice(action)
-        )
-    }
-
-    private fun newAuthenticationEntry(
-        key: String,
-        subkey: String,
-        credentialType: String,
-    ): Entry {
-        val slice = Slice.Builder(
-            Uri.EMPTY, SliceSpec(credentialType, 1)
-        )
-        return Entry(
-            key,
-            subkey,
-            slice.build()
-        )
-    }
-
-    private fun newGetEntry(
-        key: String,
-        subkey: String,
-        credentialType: String,
-        credentialTypeDisplayName: String,
-        userName: String,
-        userDisplayName: String?,
-        lastUsedTimeMillis: Long?,
-    ): Entry {
-        val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
-            .setPackage("com.androidauth.androidvault")
-        intent.putExtra("provider_extra_sample", "testprovider")
-
-        val pendingIntent = PendingIntent.getActivity(
-            context, 1,
-            intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
-                or PendingIntent.FLAG_ONE_SHOT)
-        )
-
-        val credentialEntry = CredentialEntry(
-            credentialType, credentialTypeDisplayName, userName,
-            userDisplayName, pendingIntent, lastUsedTimeMillis
-                ?: 0L, null, false
-        )
-
-        return Entry(
-            key,
-            subkey,
-            CredentialEntry.toSlice(credentialEntry),
-            Intent()
-        )
-    }
-
-    private fun newCreateEntry(
-        key: String,
-        subkey: String,
-        providerDisplayName: String,
-        passwordCount: Int,
-        passkeyCount: Int,
-        totalCredentialCount: Int,
-        lastUsedTimeMillis: Long,
-        footerDescription: String?,
-    ): Entry {
-        val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
-            .setPackage("com.androidauth.androidvault")
-        intent.putExtra("provider_extra_sample", "testprovider")
-        val pendingIntent = PendingIntent.getActivity(
-            context, 1,
-            intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
-                or PendingIntent.FLAG_ONE_SHOT)
-        )
-        val createPasswordRequest = android.service.credentials.CreateCredentialRequest(
-            android.service.credentials.CallingAppInfo(
-                context.applicationInfo.packageName, SigningInfo()
-            ),
-            TYPE_PASSWORD_CREDENTIAL,
-            toCredentialDataBundle("beckett-bakert@gmail.com", "password123")
-        )
-        val fillInIntent = Intent().putExtra(
-            CredentialProviderService.EXTRA_CREATE_CREDENTIAL_REQUEST,
-            createPasswordRequest
-        )
-
-        val createEntry = CreateEntry(
-            providerDisplayName, pendingIntent,
-            null, lastUsedTimeMillis,
-            listOf(
-                CredentialCountInformation.createPasswordCountInformation(passwordCount),
-                CredentialCountInformation.createPublicKeyCountInformation(passkeyCount),
-            ), footerDescription
-        )
-        return Entry(
-            key,
-            subkey,
-            CreateEntry.toSlice(createEntry),
-            fillInIntent
-        )
-    }
 
     private fun newRemoteEntry(
         key: String,
@@ -451,7 +338,7 @@
         return RequestInfo.newCreateRequestInfo(
             Binder(),
             CreateCredentialRequest(
-                TYPE_PUBLIC_KEY_CREDENTIAL,
+                "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL",
                 credentialData,
                 // TODO: populate with actual data
                 /*candidateQueryData=*/ Bundle(),
@@ -462,14 +349,13 @@
     }
 
     private fun testCreatePasswordRequestInfo(): RequestInfo {
-        val data = toCredentialDataBundle("beckett-bakert@gmail.com", "password123")
+        val request = CreatePasswordRequest("beckett-bakert@gmail.com", "password123")
         return RequestInfo.newCreateRequestInfo(
             Binder(),
             CreateCredentialRequest(
                 TYPE_PASSWORD_CREDENTIAL,
-                data,
-                // TODO: populate with actual data
-                /*candidateQueryData=*/ Bundle(),
+                request.credentialData,
+                request.candidateQueryData,
                 /*isSystemProviderRequired=*/ false
             ),
             "com.google.android.youtube"
@@ -478,6 +364,10 @@
 
     private fun testCreateOtherCredentialRequestInfo(): RequestInfo {
         val data = Bundle()
+        val displayInfo = DisplayInfo("my-username00", "Joe")
+        data.putBundle(
+            "androidx.credentials.BUNDLE_KEY_REQUEST_DISPLAY_INFO",
+            displayInfo.toBundle())
         return RequestInfo.newCreateRequestInfo(
             Binder(),
             CreateCredentialRequest(
@@ -498,7 +388,7 @@
             )
                 .addCredentialOption(
                     CredentialOption(
-                        TYPE_PUBLIC_KEY_CREDENTIAL,
+                        "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL",
                         Bundle(),
                         Bundle(), /*isSystemProviderRequired=*/
                         false
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 3f705d6..d8420cd 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -16,18 +16,22 @@
 
 package com.android.credentialmanager
 
+import android.app.slice.Slice
 import android.content.ComponentName
 import android.content.Context
 import android.content.pm.PackageManager
+import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
 import android.credentials.ui.CreateCredentialProviderData
 import android.credentials.ui.DisabledProviderData
 import android.credentials.ui.Entry
 import android.credentials.ui.GetCredentialProviderData
 import android.credentials.ui.RequestInfo
 import android.graphics.drawable.Drawable
+import android.service.credentials.CredentialEntry
 import android.text.TextUtils
 import android.util.Log
 import com.android.credentialmanager.common.Constants
+import com.android.credentialmanager.common.CredentialType
 import com.android.credentialmanager.createflow.ActiveEntry
 import com.android.credentialmanager.createflow.CreateCredentialUiState
 import com.android.credentialmanager.createflow.CreateOptionInfo
@@ -41,17 +45,22 @@
 import com.android.credentialmanager.getflow.CredentialEntryInfo
 import com.android.credentialmanager.getflow.ProviderInfo
 import com.android.credentialmanager.getflow.RemoteEntryInfo
-import com.android.credentialmanager.jetpack.developer.CreateCredentialRequest
-import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest
-import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
-import com.android.credentialmanager.jetpack.provider.Action
-import com.android.credentialmanager.jetpack.provider.AuthenticationAction
-import com.android.credentialmanager.jetpack.provider.CreateEntry
-import com.android.credentialmanager.jetpack.provider.CredentialCountInformation
-import com.android.credentialmanager.jetpack.provider.CredentialEntry
+import androidx.credentials.CreateCredentialRequest
+import androidx.credentials.CreateCustomCredentialRequest
+import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
+import androidx.credentials.provider.Action
+import androidx.credentials.provider.AuthenticationAction
+import androidx.credentials.provider.CreateEntry
+import androidx.credentials.provider.CustomCredentialEntry
+import androidx.credentials.provider.PasswordCredentialEntry
+import androidx.credentials.provider.PublicKeyCredentialEntry
+import androidx.credentials.provider.RemoteCreateEntry
+import androidx.credentials.provider.RemoteCredentialEntry
 import org.json.JSONObject
 
+// TODO: remove all !! checks
 private fun getAppLabel(
     pm: PackageManager,
     appPackageName: String
@@ -174,25 +183,76 @@
             credentialEntries: List<Entry>,
             context: Context,
         ): List<CredentialEntryInfo> {
-            return credentialEntries.map {
-                // TODO: handle NPE gracefully
-                val credentialEntry = CredentialEntry.fromSlice(it.slice)!!
+            val result: MutableList<CredentialEntryInfo> = mutableListOf()
+            credentialEntries.forEach {
+                val credentialEntry = parseCredentialEntryFromSlice(it.slice)
+                when (credentialEntry) {
+                    is PasswordCredentialEntry -> {
+                        result.add(CredentialEntryInfo(
+                            providerId = providerId,
+                            entryKey = it.key,
+                            entrySubkey = it.subkey,
+                            pendingIntent = credentialEntry.pendingIntent,
+                            fillInIntent = it.frameworkExtrasIntent,
+                            credentialType = CredentialType.PASSWORD,
+                            credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
+                            userName = credentialEntry.username.toString(),
+                            displayName = credentialEntry.displayName?.toString(),
+                            icon = credentialEntry.icon.loadDrawable(context),
+                            lastUsedTimeMillis = credentialEntry.lastUsedTime,
+                        ))
+                    }
+                    is PublicKeyCredentialEntry -> {
+                        result.add(CredentialEntryInfo(
+                            providerId = providerId,
+                            entryKey = it.key,
+                            entrySubkey = it.subkey,
+                            pendingIntent = credentialEntry.pendingIntent,
+                            fillInIntent = it.frameworkExtrasIntent,
+                            credentialType = CredentialType.PASSKEY,
+                            credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
+                            userName = credentialEntry.username.toString(),
+                            displayName = credentialEntry.displayName?.toString(),
+                            icon = credentialEntry.icon.loadDrawable(context),
+                            lastUsedTimeMillis = credentialEntry.lastUsedTime,
+                        ))
+                    }
+                    is CustomCredentialEntry -> {
+                        result.add(CredentialEntryInfo(
+                            providerId = providerId,
+                            entryKey = it.key,
+                            entrySubkey = it.subkey,
+                            pendingIntent = credentialEntry.pendingIntent,
+                            fillInIntent = it.frameworkExtrasIntent,
+                            credentialType = CredentialType.UNKNOWN,
+                            credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
+                            userName = credentialEntry.title.toString(),
+                            displayName = credentialEntry.subtitle?.toString(),
+                            icon = credentialEntry.icon.loadDrawable(context),
+                            lastUsedTimeMillis = credentialEntry.lastUsedTime,
+                        ))
+                    }
+                    else -> Log.d(
+                        Constants.LOG_TAG,
+                        "Encountered unrecognized credential entry ${it.slice.spec?.type}"
+                    )
+                }
+            }
+            // TODO: handle empty list due to parsing error.
+            return result
+        }
 
-                // Consider directly move the UI object into the class.
-                return@map CredentialEntryInfo(
-                    providerId = providerId,
-                    entryKey = it.key,
-                    entrySubkey = it.subkey,
-                    pendingIntent = credentialEntry.pendingIntent,
-                    fillInIntent = it.frameworkExtrasIntent,
-                    credentialType = credentialEntry.type,
-                    credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
-                    userName = credentialEntry.username.toString(),
-                    displayName = credentialEntry.displayName?.toString(),
-                    // TODO: proper fallback
-                    icon = credentialEntry.icon?.loadDrawable(context),
-                    lastUsedTimeMillis = credentialEntry.lastUsedTimeMillis,
-                )
+        private fun parseCredentialEntryFromSlice(slice: Slice): CredentialEntry? {
+            try {
+                when (slice.spec?.type) {
+                    TYPE_PASSWORD_CREDENTIAL -> return PasswordCredentialEntry.fromSlice(slice)!!
+                    TYPE_PUBLIC_KEY_CREDENTIAL -> return PublicKeyCredentialEntry.fromSlice(slice)!!
+                    else -> return CustomCredentialEntry.fromSlice(slice)!!
+                }
+            } catch (e: Exception) {
+                // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
+                // password / passkey parsing attempt.
+                return CustomCredentialEntry.fromSlice(slice)
             }
         }
 
@@ -205,18 +265,13 @@
             if (authEntry == null) {
                 return null
             }
-            val authStructuredEntry = AuthenticationAction.fromSlice(
-                authEntry!!.slice
-            )
-            if (authStructuredEntry == null) {
-                return null
-            }
-
+            val structuredAuthEntry =
+                AuthenticationAction.fromSlice(authEntry.slice) ?: return null
             return AuthenticationEntryInfo(
                 providerId = providerId,
                 entryKey = authEntry.key,
                 entrySubkey = authEntry.subkey,
-                pendingIntent = authStructuredEntry.pendingIntent,
+                pendingIntent = structuredAuthEntry.pendingIntent,
                 fillInIntent = authEntry.frameworkExtrasIntent,
                 title = providerDisplayName,
                 icon = providerIcon,
@@ -228,11 +283,13 @@
             if (remoteEntry == null) {
                 return null
             }
+            val structuredRemoteEntry = RemoteCredentialEntry.fromSlice(remoteEntry.slice)
+                ?: return null
             return RemoteEntryInfo(
                 providerId = providerId,
                 entryKey = remoteEntry.key,
                 entrySubkey = remoteEntry.subkey,
-                pendingIntent = remoteEntry.pendingIntent,
+                pendingIntent = structuredRemoteEntry.pendingIntent,
                 fillInIntent = remoteEntry.frameworkExtrasIntent,
             )
         }
@@ -242,22 +299,22 @@
             actionEntries: List<Entry>,
             providerIcon: Drawable,
         ): List<ActionEntryInfo> {
-            return actionEntries.map {
-                // TODO: handle NPE gracefully
-                val actionEntryUi = Action.fromSlice(it.slice)!!
-
-                return@map ActionEntryInfo(
+            val result: MutableList<ActionEntryInfo> = mutableListOf()
+            actionEntries.forEach {
+                val actionEntryUi = Action.fromSlice(it.slice) ?: return@forEach
+                result.add(ActionEntryInfo(
                     providerId = providerId,
                     entryKey = it.key,
                     entrySubkey = it.subkey,
                     pendingIntent = actionEntryUi.pendingIntent,
                     fillInIntent = it.frameworkExtrasIntent,
                     title = actionEntryUi.title.toString(),
-                    // TODO: gracefully fail
                     icon = providerIcon,
-                    subTitle = actionEntryUi.subTitle?.toString(),
-                )
+                    subTitle = actionEntryUi.subtitle?.toString(),
+                ))
             }
+            // TODO: handle empty list
+            return result
         }
     }
 }
@@ -316,22 +373,21 @@
         ): RequestDisplayInfo? {
             val appLabel = getAppLabel(context.packageManager, requestInfo.appPackageName)
                 ?: return null
-            val createCredentialRequest = requestInfo.createCredentialRequest
-            val createCredentialRequestJetpack = createCredentialRequest?.let {
-                CreateCredentialRequest.createFrom(
-                    it.type, it.credentialData, it.candidateQueryData, it.isSystemProviderRequired
+            val createCredentialRequest = requestInfo.createCredentialRequest ?: return null
+            val createCredentialRequestJetpack = CreateCredentialRequest.createFrom(
+                createCredentialRequest.type,
+                createCredentialRequest.credentialData,
+                createCredentialRequest.candidateQueryData,
+                createCredentialRequest.isSystemProviderRequired
+            )
+            return when (createCredentialRequestJetpack) {
+                is CreatePasswordRequest -> RequestDisplayInfo(
+                    createCredentialRequestJetpack.id,
+                    createCredentialRequestJetpack.password,
+                    CredentialType.PASSWORD,
+                    appLabel,
+                    context.getDrawable(R.drawable.ic_password)!!
                 )
-            }
-            when (createCredentialRequestJetpack) {
-                is CreatePasswordRequest -> {
-                    return RequestDisplayInfo(
-                        createCredentialRequestJetpack.id,
-                        createCredentialRequestJetpack.password,
-                        createCredentialRequestJetpack.type,
-                        appLabel,
-                        context.getDrawable(R.drawable.ic_password)!!
-                    )
-                }
                 is CreatePublicKeyCredentialRequest -> {
                     val requestJson = createCredentialRequestJetpack.requestJson
                     val json = JSONObject(requestJson)
@@ -342,24 +398,29 @@
                         name = user.getString("name")
                         displayName = user.getString("displayName")
                     }
-                    return RequestDisplayInfo(
+                    RequestDisplayInfo(
                         name,
                         displayName,
-                        createCredentialRequestJetpack.type,
+                        CredentialType.PASSKEY,
                         appLabel,
                         context.getDrawable(R.drawable.ic_passkey)!!
                     )
                 }
-                // TODO: correctly parsing for other sign-ins
-                else -> {
-                    return RequestDisplayInfo(
-                        "beckett-bakert@gmail.com",
-                        "Elisa Beckett",
-                        "other-sign-ins",
-                        appLabel.toString(),
-                        context.getDrawable(R.drawable.ic_other_sign_in)!!
+                is CreateCustomCredentialRequest -> {
+                    // TODO: directly use the display info once made public
+                    val displayInfo = CreateCredentialRequest.DisplayInfo
+                        .parseFromCredentialDataBundle(createCredentialRequest.credentialData)
+                        ?: return null
+                    RequestDisplayInfo(
+                        title = displayInfo.userId,
+                        subtitle = displayInfo.userDisplayName,
+                        type = CredentialType.UNKNOWN,
+                        appName = appLabel,
+                        typeIcon = displayInfo.credentialTypeIcon?.loadDrawable(context)
+                            ?: context.getDrawable(R.drawable.ic_other_sign_in)!!
                     )
                 }
+                else -> null
             }
         }
 
@@ -408,7 +469,7 @@
                 currentScreenState = initialScreenState,
                 requestDisplayInfo = requestDisplayInfo,
                 sortedCreateOptionsPairs = createOptionsPairs.sortedWith(
-                    compareByDescending { it.first.lastUsedTimeMillis }
+                    compareByDescending { it.first.lastUsedTime }
                 ),
                 hasDefaultProvider = defaultProvider != null,
                 activeEntry = toActiveEntry(
@@ -430,7 +491,7 @@
             isPasskeyFirstUse: Boolean,
         ): CreateScreenState? {
             return if (isPasskeyFirstUse && requestDisplayInfo.type ==
-                TYPE_PUBLIC_KEY_CREDENTIAL && !isOnPasskeyIntroStateAlready) {
+                CredentialType.PASSKEY && !isOnPasskeyIntroStateAlready) {
                 CreateScreenState.PASSKEY_INTRO
             } else if ((defaultProvider == null || defaultProvider.createOptions.isEmpty()) &&
                 createOptionSize > 1) {
@@ -479,12 +540,10 @@
             creationEntries: List<Entry>,
             context: Context,
         ): List<CreateOptionInfo> {
-            return creationEntries.map {
-                // TODO: handle NPE gracefully
-                val createEntry = CreateEntry.fromSlice(it.slice)!!
-
-                return@map CreateOptionInfo(
-                    // TODO: remove fallbacks
+            val result: MutableList<CreateOptionInfo> = mutableListOf()
+            creationEntries.forEach {
+                val createEntry = CreateEntry.fromSlice(it.slice) ?: return@forEach
+                result.add(CreateOptionInfo(
                     providerId = providerId,
                     entryKey = it.key,
                     entrySubkey = it.subkey,
@@ -492,32 +551,28 @@
                     fillInIntent = it.frameworkExtrasIntent,
                     userProviderDisplayName = createEntry.accountName.toString(),
                     profileIcon = createEntry.icon?.loadDrawable(context),
-                    passwordCount = CredentialCountInformation.getPasswordCount(
-                        createEntry.credentialCountInformationList
-                    ) ?: 0,
-                    passkeyCount = CredentialCountInformation.getPasskeyCount(
-                        createEntry.credentialCountInformationList
-                    ) ?: 0,
-                    totalCredentialCount = CredentialCountInformation.getTotalCount(
-                        createEntry.credentialCountInformationList
-                    ) ?: 0,
-                    lastUsedTimeMillis = createEntry.lastUsedTimeMillis ?: 0,
-                    footerDescription = createEntry.footerDescription?.toString()
-                )
+                    passwordCount = createEntry.getPasswordCredentialCount(),
+                    passkeyCount = createEntry.getPublicKeyCredentialCount(),
+                    totalCredentialCount = createEntry.getTotalCredentialCount(),
+                    lastUsedTime = createEntry.lastUsedTime,
+                    footerDescription = createEntry.description?.toString()
+                ))
             }
+            return result
         }
 
         private fun toRemoteInfo(
             providerId: String,
             remoteEntry: Entry?,
         ): RemoteInfo? {
-            // TODO: should also call fromSlice after getting the official jetpack code.
             return if (remoteEntry != null) {
+                val structuredRemoteEntry = RemoteCreateEntry.fromSlice(remoteEntry.slice)
+                    ?: return null
                 RemoteInfo(
                     providerId = providerId,
                     entryKey = remoteEntry.key,
                     entrySubkey = remoteEntry.subkey,
-                    pendingIntent = remoteEntry.pendingIntent,
+                    pendingIntent = structuredRemoteEntry.pendingIntent,
                     fillInIntent = remoteEntry.frameworkExtrasIntent,
                 )
             } else null
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt
new file mode 100644
index 0000000..e3bbaeb
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt
@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager
+
+import android.app.PendingIntent
+import android.app.slice.Slice
+import android.app.slice.SliceSpec
+import android.content.Context
+import android.content.Intent
+import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
+import android.credentials.ui.Entry
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.provider.Settings
+import androidx.credentials.provider.CreateEntry
+
+import java.time.Instant
+
+// TODO: remove once testing is complete
+class GetTestUtils {
+    companion object {
+        internal fun newAuthenticationEntry(
+            context: Context,
+            key: String,
+            subkey: String,
+        ): Entry {
+            val slice = Slice.Builder(
+                Uri.EMPTY, SliceSpec("AuthenticationAction", 0)
+            )
+            val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
+            val pendingIntent =
+                PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+            slice.addAction(
+                pendingIntent,
+                Slice.Builder(slice)
+                    .addHints(listOf("androidx.credentials.provider.authenticationAction" +
+                        ".SLICE_HINT_PENDING_INTENT"))
+                    .build(),
+                /*subType=*/null
+            )
+            return Entry(
+                key,
+                subkey,
+                slice.build()
+            )
+        }
+
+        internal fun newActionEntry(
+            context: Context,
+            key: String,
+            subkey: String,
+            text: String,
+            subtext: String? = null,
+        ): Entry {
+            val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
+            val pendingIntent =
+                PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
+            val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("Action", 0))
+                .addText(
+                    text, /*subType=*/null,
+                    listOf("androidx.credentials.provider.action.HINT_ACTION_TITLE")
+                )
+                .addText(
+                    subtext, /*subType=*/null,
+                    listOf("androidx.credentials.provider.action.HINT_ACTION_SUBTEXT")
+                )
+            sliceBuilder.addAction(
+                pendingIntent,
+                Slice.Builder(sliceBuilder)
+                    .addHints(
+                        listOf("androidx.credentials.provider.action.SLICE_HINT_PENDING_INTENT")
+                    )
+                    .build(),
+                /*subType=*/null
+            )
+            return Entry(
+                key,
+                subkey,
+                sliceBuilder.build()
+            )
+        }
+
+        private const val SLICE_HINT_TYPE_DISPLAY_NAME =
+            "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+        private const val SLICE_HINT_TITLE =
+            "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_USER_NAME"
+        private const val SLICE_HINT_SUBTITLE =
+            "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+        private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+            "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+        private const val SLICE_HINT_ICON =
+            "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_PROFILE_ICON"
+        private const val SLICE_HINT_PENDING_INTENT =
+            "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_PENDING_INTENT"
+        private const val SLICE_HINT_AUTO_ALLOWED =
+            "androidx.credentials.provider.passwordCredentialEntry.SLICE_HINT_AUTO_ALLOWED"
+        private const val AUTO_SELECT_TRUE_STRING = "true"
+        private const val AUTO_SELECT_FALSE_STRING = "false"
+
+        internal fun newPasswordEntry(
+            context: Context,
+            key: String,
+            subkey: String,
+            userName: String,
+            userDisplayName: String?,
+            lastUsedTime: Instant?,
+        ): Entry {
+            val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
+                .setPackage("com.androidauth.androidvault")
+            intent.putExtra("provider_extra_sample", "testprovider")
+            val pendingIntent = PendingIntent.getActivity(
+                context, 1,
+                intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+                    or PendingIntent.FLAG_ONE_SHOT)
+            )
+            return Entry(
+                key,
+                subkey,
+                toPasswordSlice(userName, userDisplayName, pendingIntent, lastUsedTime),
+                Intent()
+            )
+        }
+
+        private fun toPasswordSlice(
+            title: CharSequence,
+            subTitle: CharSequence?,
+            pendingIntent: PendingIntent,
+            lastUsedTime: Instant?,
+            icon: Icon? = null,
+            isAutoSelectAllowed: Boolean = true
+        ): Slice {
+            val type = TYPE_PASSWORD_CREDENTIAL
+            val autoSelectAllowed = if (isAutoSelectAllowed) {
+                AUTO_SELECT_TRUE_STRING
+            } else {
+                AUTO_SELECT_FALSE_STRING
+            }
+            val sliceBuilder = Slice.Builder(
+                Uri.EMPTY, SliceSpec(
+                    type, 1
+                )
+            )
+                .addText(
+                    "Password", /*subType=*/null,
+                    listOf(SLICE_HINT_TYPE_DISPLAY_NAME)
+                )
+                .addText(
+                    title, /*subType=*/null,
+                    listOf(SLICE_HINT_TITLE)
+                )
+                .addText(
+                    subTitle, /*subType=*/null,
+                    listOf(SLICE_HINT_SUBTITLE)
+                )
+                .addText(
+                    autoSelectAllowed, /*subType=*/null,
+                    listOf(SLICE_HINT_AUTO_ALLOWED)
+                )
+            if (lastUsedTime != null) {
+                sliceBuilder.addLong(
+                    lastUsedTime.toEpochMilli(),
+                    /*subType=*/null,
+                    listOf(SLICE_HINT_LAST_USED_TIME_MILLIS)
+                )
+            }
+            if (icon != null) {
+                sliceBuilder.addIcon(
+                    icon, /*subType=*/null,
+                    listOf(SLICE_HINT_ICON)
+                )
+            }
+            sliceBuilder.addAction(
+                pendingIntent,
+                Slice.Builder(sliceBuilder)
+                    .addHints(listOf(SLICE_HINT_PENDING_INTENT))
+                    .build(),
+                /*subType=*/null
+            )
+            return sliceBuilder.build()
+        }
+
+
+        private const val PASSKEY_SLICE_HINT_TYPE_DISPLAY_NAME =
+            "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+        private const val PASSKEY_SLICE_HINT_TITLE =
+            "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_USER_NAME"
+        private const val PASSKEY_SLICE_HINT_SUBTITLE =
+            "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
+        private const val PASSKEY_SLICE_HINT_LAST_USED_TIME_MILLIS =
+            "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+        private const val PASSKEY_SLICE_HINT_ICON =
+            "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_PROFILE_ICON"
+        private const val PASSKEY_SLICE_HINT_PENDING_INTENT =
+            "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_PENDING_INTENT"
+        private const val PASSKEY_SLICE_HINT_AUTO_ALLOWED =
+            "androidx.credentials.provider.publicKeyCredEntry.SLICE_HINT_AUTO_ALLOWED"
+
+        internal fun newPasskeyEntry(
+            context: Context,
+            key: String,
+            subkey: String,
+            userName: String,
+            userDisplayName: String?,
+            lastUsedTime: Instant?,
+        ): Entry {
+            val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
+                .setPackage("com.androidauth.androidvault")
+            intent.putExtra("provider_extra_sample", "testprovider")
+            val pendingIntent = PendingIntent.getActivity(
+                context, 1,
+                intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+                    or PendingIntent.FLAG_ONE_SHOT)
+            )
+            return Entry(
+                key, subkey, toPasskeySlice(
+                    userName, userDisplayName, pendingIntent, lastUsedTime
+                ),
+                Intent()
+            )
+        }
+
+        private fun toPasskeySlice(
+            title: CharSequence,
+            subTitle: CharSequence?,
+            pendingIntent: PendingIntent,
+            lastUsedTime: Instant?,
+            icon: Icon? = null,
+            isAutoSelectAllowed: Boolean = true
+        ): Slice {
+            val type = "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
+            val autoSelectAllowed = if (isAutoSelectAllowed) {
+                AUTO_SELECT_TRUE_STRING
+            } else {
+                AUTO_SELECT_FALSE_STRING
+            }
+            val sliceBuilder = Slice.Builder(
+                Uri.EMPTY, SliceSpec(
+                    type, 1
+                )
+            )
+                .addText(
+                    "Passkey", /*subType=*/null,
+                    listOf(PASSKEY_SLICE_HINT_TYPE_DISPLAY_NAME)
+                )
+                .addText(
+                    title, /*subType=*/null,
+                    listOf(PASSKEY_SLICE_HINT_TITLE)
+                )
+                .addText(
+                    subTitle, /*subType=*/null,
+                    listOf(PASSKEY_SLICE_HINT_SUBTITLE)
+                )
+                .addText(
+                    autoSelectAllowed, /*subType=*/null,
+                    listOf(PASSKEY_SLICE_HINT_AUTO_ALLOWED)
+                )
+            if (lastUsedTime != null) {
+                sliceBuilder.addLong(
+                    lastUsedTime.toEpochMilli(),
+                    /*subType=*/null,
+                    listOf(PASSKEY_SLICE_HINT_LAST_USED_TIME_MILLIS)
+                )
+            }
+            if (icon != null) {
+                sliceBuilder.addIcon(
+                    icon, /*subType=*/null,
+                    listOf(PASSKEY_SLICE_HINT_ICON)
+                )
+            }
+            sliceBuilder.addAction(
+                pendingIntent,
+                Slice.Builder(sliceBuilder)
+                    .addHints(listOf(PASSKEY_SLICE_HINT_PENDING_INTENT))
+                    .build(),
+                /*subType=*/null
+            )
+            return sliceBuilder.build()
+        }
+    }
+}
+
+class CreateTestUtils {
+    companion object {
+        private const val TYPE_TOTAL_CREDENTIAL = "TOTAL_CREDENTIAL_COUNT_TYPE"
+        private const val SLICE_HINT_ACCOUNT_NAME =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_USER_PROVIDER_ACCOUNT_NAME"
+        private const val SLICE_HINT_NOTE =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_NOTE"
+        private const val SLICE_HINT_ICON =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_PROFILE_ICON"
+        private const val SLICE_HINT_CREDENTIAL_COUNT_INFORMATION =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_CREDENTIAL_COUNT_INFORMATION"
+        private const val SLICE_HINT_LAST_USED_TIME_MILLIS =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
+        private const val SLICE_HINT_PENDING_INTENT =
+            "androidx.credentials.provider.createEntry.SLICE_HINT_PENDING_INTENT"
+
+        internal fun newCreateEntry(
+            context: Context,
+            key: String,
+            subkey: String,
+            providerUserDisplayName: String,
+            passwordCount: Int?,
+            passkeyCount: Int?,
+            totalCredentialCount: Int?,
+            lastUsedTime: Instant?,
+            footerDescription: String?,
+        ): Entry {
+            val intent = Intent("com.androidauth.androidvault.CONFIRM_PASSWORD")
+                .setPackage("com.androidauth.androidvault")
+            intent.putExtra("provider_extra_sample", "testprovider")
+            val pendingIntent = PendingIntent.getActivity(
+                context, 1,
+                intent, (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+                    or PendingIntent.FLAG_ONE_SHOT)
+            )
+            val credCountMap = mutableMapOf<String, Int>()
+            passwordCount?.let { credCountMap.put(TYPE_PASSWORD_CREDENTIAL, it) }
+            passkeyCount?.let {
+                credCountMap.put("androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL", it)
+            }
+            totalCredentialCount?.let { credCountMap.put(TYPE_TOTAL_CREDENTIAL, it) }
+            return Entry(
+                key,
+                subkey,
+                CreateEntry.toSlice(
+                    providerUserDisplayName,
+                    null,
+                    footerDescription,
+                    lastUsedTime,
+                    credCountMap,
+                    pendingIntent
+                ),
+                Intent()
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/CredentialType.kt
similarity index 64%
rename from packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
rename to packages/CredentialManager/src/com/android/credentialmanager/common/CredentialType.kt
index 497c272..cc92f60 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/FrameworkClassParsingException.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/CredentialType.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,12 +14,8 @@
  * limitations under the License.
  */
 
-package com.android.credentialmanager.jetpack.developer
+package com.android.credentialmanager.common
 
-/**
- * Internal exception used to indicate a parsing error while converting from a framework type to
- * a jetpack type.
- *
- * @hide
- */
-internal class FrameworkClassParsingException : Exception()
\ No newline at end of file
+enum class CredentialType {
+    UNKNOWN, PASSKEY, PASSWORD,
+}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index 5e432b9..f8d008e 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -2,7 +2,6 @@
 
 package com.android.credentialmanager.createflow
 
-import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
 import androidx.activity.compose.ManagedActivityResultLauncher
 import androidx.activity.result.ActivityResult
 import androidx.activity.result.IntentSenderRequest
@@ -37,6 +36,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.core.graphics.drawable.toBitmap
 import com.android.credentialmanager.R
+import com.android.credentialmanager.common.CredentialType
 import com.android.credentialmanager.common.ProviderActivityState
 import com.android.credentialmanager.common.material.ModalBottomSheetLayout
 import com.android.credentialmanager.common.material.ModalBottomSheetValue
@@ -50,7 +50,6 @@
 import com.android.credentialmanager.common.ui.ContainerCard
 import com.android.credentialmanager.ui.theme.EntryShape
 import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -288,11 +287,11 @@
                 text = stringResource(
                     R.string.choose_provider_title,
                     when (requestDisplayInfo.type) {
-                        TYPE_PUBLIC_KEY_CREDENTIAL ->
+                        CredentialType.PASSKEY ->
                             stringResource(R.string.passkeys)
-                        TYPE_PASSWORD_CREDENTIAL ->
+                        CredentialType.PASSWORD ->
                             stringResource(R.string.passwords)
-                        else -> stringResource(R.string.sign_in_info)
+                        CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info)
                     }),
                 style = MaterialTheme.typography.titleMedium,
                 modifier = Modifier.padding(horizontal = 24.dp)
@@ -398,11 +397,11 @@
                         stringResource(
                             R.string.save_credential_to_title,
                             when (requestDisplayInfo.type) {
-                                TYPE_PUBLIC_KEY_CREDENTIAL ->
+                                CredentialType.PASSKEY ->
                                     stringResource(R.string.passkey)
-                                TYPE_PASSWORD_CREDENTIAL ->
+                                CredentialType.PASSWORD ->
                                     stringResource(R.string.password)
-                                else -> stringResource(R.string.sign_in_info)
+                                CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info)
                             }),
                         style = MaterialTheme.typography.titleMedium,
                     )
@@ -571,15 +570,15 @@
             )
             TextOnSurface(
                 text = when (requestDisplayInfo.type) {
-                    TYPE_PUBLIC_KEY_CREDENTIAL -> stringResource(
+                    CredentialType.PASSKEY -> stringResource(
                         R.string.choose_create_option_passkey_title,
                         requestDisplayInfo.appName
                     )
-                    TYPE_PASSWORD_CREDENTIAL -> stringResource(
+                    CredentialType.PASSWORD -> stringResource(
                         R.string.choose_create_option_password_title,
                         requestDisplayInfo.appName
                     )
-                    else -> stringResource(
+                    CredentialType.UNKNOWN -> stringResource(
                         R.string.choose_create_option_sign_in_title,
                         requestDisplayInfo.appName
                     )
@@ -828,7 +827,7 @@
             Column() {
                 // TODO: Add the function to hide/view password when the type is create password
                 when (requestDisplayInfo.type) {
-                    TYPE_PUBLIC_KEY_CREDENTIAL -> {
+                    CredentialType.PASSKEY -> {
                         TextOnSurfaceVariant(
                             text = requestDisplayInfo.title,
                             style = MaterialTheme.typography.titleLarge,
@@ -846,7 +845,7 @@
                             modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
                         )
                     }
-                    TYPE_PASSWORD_CREDENTIAL -> {
+                    CredentialType.PASSWORD -> {
                         TextOnSurfaceVariant(
                             text = requestDisplayInfo.title,
                             style = MaterialTheme.typography.titleLarge,
@@ -859,7 +858,7 @@
                             modifier = Modifier.padding(bottom = 16.dp, start = 5.dp),
                         )
                     }
-                    else -> {
+                    CredentialType.UNKNOWN -> {
                         if (requestDisplayInfo.subtitle != null) {
                             TextOnSurfaceVariant(
                                 text = requestDisplayInfo.title,
@@ -917,8 +916,8 @@
                         modifier = Modifier.padding(start = 5.dp),
                     )
                 }
-                if (requestDisplayInfo.type == TYPE_PUBLIC_KEY_CREDENTIAL ||
-                    requestDisplayInfo.type == TYPE_PASSWORD_CREDENTIAL) {
+                if (requestDisplayInfo.type == CredentialType.PASSKEY ||
+                    requestDisplayInfo.type == CredentialType.PASSWORD) {
                     if (createOptionInfo.passwordCount != null &&
                         createOptionInfo.passkeyCount != null
                     ) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 12a5085..05be0a6 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -21,6 +21,8 @@
 import android.graphics.drawable.Drawable
 import com.android.credentialmanager.common.DialogState
 import com.android.credentialmanager.common.ProviderActivityState
+import com.android.credentialmanager.common.CredentialType
+import java.time.Instant
 
 data class CreateCredentialUiState(
   val enabledProviders: List<EnabledProviderInfo>,
@@ -68,18 +70,18 @@
 )
 
 class CreateOptionInfo(
-  providerId: String,
-  entryKey: String,
-  entrySubkey: String,
-  pendingIntent: PendingIntent?,
-  fillInIntent: Intent?,
-  val userProviderDisplayName: String?,
-  val profileIcon: Drawable?,
-  val passwordCount: Int?,
-  val passkeyCount: Int?,
-  val totalCredentialCount: Int?,
-  val lastUsedTimeMillis: Long?,
-  val footerDescription: String?,
+    providerId: String,
+    entryKey: String,
+    entrySubkey: String,
+    pendingIntent: PendingIntent?,
+    fillInIntent: Intent?,
+    val userProviderDisplayName: String?,
+    val profileIcon: Drawable?,
+    val passwordCount: Int?,
+    val passkeyCount: Int?,
+    val totalCredentialCount: Int?,
+    val lastUsedTime: Instant?,
+    val footerDescription: String?,
 ) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent)
 
 class RemoteInfo(
@@ -93,7 +95,7 @@
 data class RequestDisplayInfo(
   val title: String,
   val subtitle: String?,
-  val type: String,
+  val type: CredentialType,
   val appName: String,
   val typeIcon: Drawable,
 )
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
index b30d1ec..a5e19b6 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt
@@ -16,7 +16,6 @@
 
 package com.android.credentialmanager.getflow
 
-import android.credentials.Credential
 import android.text.TextUtils
 import androidx.activity.compose.ManagedActivityResultLauncher
 import androidx.activity.result.ActivityResult
@@ -59,6 +58,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.core.graphics.drawable.toBitmap
 import com.android.credentialmanager.R
+import com.android.credentialmanager.common.CredentialType
 import com.android.credentialmanager.common.ProviderActivityState
 import com.android.credentialmanager.common.material.ModalBottomSheetLayout
 import com.android.credentialmanager.common.material.ModalBottomSheetValue
@@ -71,7 +71,6 @@
 import com.android.credentialmanager.common.ui.TextOnSurfaceVariant
 import com.android.credentialmanager.common.ui.ContainerCard
 import com.android.credentialmanager.common.ui.TransparentBackgroundEntry
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
 import com.android.credentialmanager.ui.theme.EntryShape
 import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme
 
@@ -171,7 +170,7 @@
                     ) {
                         if (sortedUserNameToCredentialEntryList.first()
                                 .sortedCredentialEntryList.first().credentialType
-                            == PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL
+                            == CredentialType.PASSKEY
                         ) R.string.get_dialog_title_use_passkey_for
                         else R.string.get_dialog_title_use_sign_in_for
                     } else if (
@@ -534,7 +533,7 @@
                 )
                 TextSecondary(
                     text = if (
-                        credentialEntryInfo.credentialType == Credential.TYPE_PASSWORD_CREDENTIAL) {
+                        credentialEntryInfo.credentialType == CredentialType.PASSWORD) {
                         "••••••••••••"
                     } else {
                         if (TextUtils.isEmpty(credentialEntryInfo.displayName))
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
index 7d2f0da..5741f36 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt
@@ -26,11 +26,11 @@
 import androidx.compose.runtime.setValue
 import androidx.lifecycle.ViewModel
 import com.android.credentialmanager.CredentialManagerRepo
+import com.android.credentialmanager.common.CredentialType
 import com.android.credentialmanager.common.Constants
 import com.android.credentialmanager.common.DialogState
 import com.android.credentialmanager.common.ProviderActivityResult
 import com.android.credentialmanager.common.ProviderActivityState
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
 import com.android.internal.util.Preconditions
 
 data class GetCredentialUiState(
@@ -258,9 +258,9 @@
     override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
         // First prefer passkey type for its security benefits
         if (p0.credentialType != p1.credentialType) {
-            if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p0.credentialType) {
+            if (CredentialType.PASSKEY == p0.credentialType) {
                 return -1
-            } else if (PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL == p1.credentialType) {
+            } else if (CredentialType.PASSKEY == p1.credentialType) {
                 return 1
             }
         }
@@ -272,9 +272,9 @@
             } else if (p0.lastUsedTimeMillis > p1.lastUsedTimeMillis) {
                 return -1
             }
-        } else if (p0.lastUsedTimeMillis != null && p0.lastUsedTimeMillis > 0) {
+        } else if (p0.lastUsedTimeMillis != null) {
             return -1
-        } else if (p1.lastUsedTimeMillis != null && p1.lastUsedTimeMillis > 0) {
+        } else if (p1.lastUsedTimeMillis != null) {
             return 1
         }
         return 0
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
index 8ce31bd..4c05dea 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt
@@ -19,6 +19,9 @@
 import android.app.PendingIntent
 import android.content.Intent
 import android.graphics.drawable.Drawable
+import com.android.credentialmanager.common.CredentialType
+
+import java.time.Instant
 
 data class ProviderInfo(
   /**
@@ -62,13 +65,13 @@
   pendingIntent: PendingIntent?,
   fillInIntent: Intent?,
   /** Type of this credential used for sorting. Not localized so must not be directly displayed. */
-  val credentialType: String,
+  val credentialType: CredentialType,
   /** Localized type value of this credential used for display purpose. */
   val credentialTypeDisplayName: String,
   val userName: String,
   val displayName: String?,
   val icon: Drawable?,
-  val lastUsedTimeMillis: Long?,
+  val lastUsedTimeMillis: Instant?,
 ) : EntryInfo(providerId, entryKey, entrySubkey, pendingIntent, fillInIntent)
 
 class AuthenticationEntryInfo(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt
deleted file mode 100644
index eaa2ad4..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCredentialRequest.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.credentials.Credential
-import android.os.Bundle
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
-
-/**
- * Base request class for registering a credential.
- *
- * @property type the credential type
- * @property data the request data in the [Bundle] format
- * @property requireSystemProvider true if must only be fulfilled by a system provider and false
- *                              otherwise
- */
-open class CreateCredentialRequest(
-    open val type: String,
-    open val credentialData: Bundle,
-    open val candidateQueryData: Bundle,
-    open val requireSystemProvider: Boolean
-) {
-    companion object {
-        @JvmStatic
-        fun createFrom(
-            type: String,
-            credentialData: Bundle,
-            candidateQueryData: Bundle,
-            requireSystemProvider: Boolean
-        ): CreateCredentialRequest {
-            return try {
-                when (type) {
-                    Credential.TYPE_PASSWORD_CREDENTIAL ->
-                        CreatePasswordRequest.createFrom(credentialData)
-                    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
-                        when (credentialData.getString(BUNDLE_KEY_SUBTYPE)) {
-                            CreatePublicKeyCredentialRequest
-                                .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST ->
-                                CreatePublicKeyCredentialRequest.createFrom(credentialData)
-                            CreatePublicKeyCredentialRequestPrivileged
-                                .BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV ->
-                                CreatePublicKeyCredentialRequestPrivileged
-                                    .createFrom(credentialData)
-                            else -> throw FrameworkClassParsingException()
-                        }
-                    else -> throw FrameworkClassParsingException()
-                }
-            } catch (e: FrameworkClassParsingException) {
-                // Parsing failed but don't crash the process. Instead just output a request with
-                // the raw framework values.
-                CreateCustomCredentialRequest(
-                    type,
-                    credentialData,
-                    candidateQueryData,
-                    requireSystemProvider
-                )
-            }
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCustomCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCustomCredentialRequest.kt
deleted file mode 100644
index 50da9a1..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreateCustomCredentialRequest.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * Base custom create request class for registering a credential.
- *
- * An application can construct a subtype custom request and call
- * [CredentialManager.executeCreateCredential] to launch framework UI flows to collect consent and
- * any other metadata needed from the user to register a new user credential.
- *
- * @property type the credential type determined by the credential-type-specific subclass for custom
- * use cases
- * @property credentialData the full credential creation request data in the [Bundle] format for
- * custom use cases
- * @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
- * the provider during the initial candidate query stage, which should not contain sensitive user
- * credential information
- * @property requireSystemProvider true if must only be fulfilled by a system provider and false
- * otherwise
- * @throws IllegalArgumentException If [type] is empty
- * @throws NullPointerException If [type] or [credentialData] are null
- */
-open class CreateCustomCredentialRequest(
-    final override val type: String,
-    final override val credentialData: Bundle,
-    final override val candidateQueryData: Bundle,
-    @get:JvmName("requireSystemProvider")
-    final override val requireSystemProvider: Boolean
-) : CreateCredentialRequest(type, credentialData, candidateQueryData, requireSystemProvider) {
-    init {
-        require(type.isNotEmpty()) { "type should not be empty" }
-    }
-}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt
deleted file mode 100644
index bf0aa8a..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePasswordRequest.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.credentials.Credential
-import android.os.Bundle
-
-/**
- * A request to save the user password credential with their password provider.
- *
- * @property id the user id associated with the password
- * @property password the password
- * @throws NullPointerException If [id] is null
- * @throws NullPointerException If [password] is null
- * @throws IllegalArgumentException If [password] is empty
- */
-class CreatePasswordRequest constructor(
-        val id: String,
-        val password: String,
-) : CreateCredentialRequest(
-        type = Credential.TYPE_PASSWORD_CREDENTIAL,
-        credentialData = toCredentialDataBundle(id, password),
-        // No credential data should be sent during the query phase.
-        candidateQueryData = Bundle(),
-        requireSystemProvider = false,
-) {
-
-    init {
-        require(password.isNotEmpty()) { "password should not be empty" }
-    }
-
-    companion object {
-        const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
-        const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
-
-        @JvmStatic
-        internal fun toCredentialDataBundle(id: String, password: String): Bundle {
-            val bundle = Bundle()
-            bundle.putString(BUNDLE_KEY_ID, id)
-            bundle.putString(BUNDLE_KEY_PASSWORD, password)
-            return bundle
-        }
-
-        @JvmStatic
-        internal fun toCandidateDataBundle(id: String): Bundle {
-            val bundle = Bundle()
-            bundle.putString(BUNDLE_KEY_ID, id)
-            return bundle
-        }
-
-        @JvmStatic
-        internal fun createFrom(data: Bundle): CreatePasswordRequest {
-            try {
-                val id = data.getString(BUNDLE_KEY_ID)
-                val password = data.getString(BUNDLE_KEY_PASSWORD)
-                return CreatePasswordRequest(id!!, password!!)
-            } catch (e: Exception) {
-                throw FrameworkClassParsingException()
-            }
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt
deleted file mode 100644
index f3d402a..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequest.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
-
-/**
-* A request to register a passkey from the user's public key credential provider.
-*
-* @property requestJson the privileged request in JSON format in the standard webauthn web json
-* shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
-* @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
-* immediately when there is no available passkey registration offering instead of falling back to
-* discovering remote options, and false (default) otherwise
-* @throws NullPointerException If [requestJson] is null
-* @throws IllegalArgumentException If [requestJson] is empty
-*/
-class CreatePublicKeyCredentialRequest @JvmOverloads constructor(
-    val requestJson: String,
-    @get:JvmName("preferImmediatelyAvailableCredentials")
-    val preferImmediatelyAvailableCredentials: Boolean = false
-) : CreateCredentialRequest(
-    type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    credentialData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
-    // The whole request data should be passed during the query phase.
-    candidateQueryData = toCredentialDataBundle(requestJson, preferImmediatelyAvailableCredentials),
-    requireSystemProvider = false,
-) {
-
-    init {
-        require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
-    }
-
-    /** @hide */
-    companion object {
-        const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
-            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
-        internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-        internal const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST =
-            "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST"
-
-        @JvmStatic
-        internal fun toCredentialDataBundle(
-            requestJson: String,
-            preferImmediatelyAvailableCredentials: Boolean
-        ): Bundle {
-            val bundle = Bundle()
-            bundle.putString(BUNDLE_KEY_SUBTYPE,
-                BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST)
-            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
-            bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
-                preferImmediatelyAvailableCredentials)
-            return bundle
-        }
-
-        @JvmStatic
-        internal fun toCandidateDataBundle(
-            requestJson: String,
-            preferImmediatelyAvailableCredentials: Boolean
-        ): Bundle {
-            val bundle = Bundle()
-            bundle.putString(BUNDLE_KEY_SUBTYPE,
-                BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST)
-            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
-            bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
-                preferImmediatelyAvailableCredentials)
-            return bundle
-        }
-
-        @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
-        // boolean value from being returned.
-        @JvmStatic
-        internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequest {
-            try {
-                val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
-                val preferImmediatelyAvailableCredentials =
-                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
-                return CreatePublicKeyCredentialRequest(requestJson!!,
-                    (preferImmediatelyAvailableCredentials!!) as Boolean)
-            } catch (e: Exception) {
-                throw FrameworkClassParsingException()
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt
deleted file mode 100644
index 85ab2d2..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/CreatePublicKeyCredentialRequestPrivileged.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.BUNDLE_KEY_SUBTYPE
-
-/**
- * A privileged request to register a passkey from the user’s public key credential provider, where
- * the caller can modify the rp. Only callers with privileged permission, e.g. user’s default
- * brower, caBLE, can use this. These permissions will be introduced in an upcoming release.
- * TODO("Add specific permission info/annotation")
- *
- * @property requestJson the privileged request in JSON format in the standard webauthn web json
- * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
- * immediately when there is no available passkey registration offering instead of falling back to
- * discovering remote options, and false (default) otherwise
- * @property relyingParty the expected true RP ID which will override the one in the [requestJson],
- * where rp is defined [here](https://w3c.github.io/webauthn/#rp-id)
- * @property clientDataHash a hash that is used to verify the [relyingParty] Identity
- * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash] is
- * null
- * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is
- * empty
- */
-class CreatePublicKeyCredentialRequestPrivileged @JvmOverloads constructor(
-    val requestJson: String,
-    val relyingParty: String,
-    val clientDataHash: String,
-    @get:JvmName("preferImmediatelyAvailableCredentials")
-    val preferImmediatelyAvailableCredentials: Boolean = false
-) : CreateCredentialRequest(
-    type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    credentialData = toCredentialDataBundle(
-        requestJson,
-        relyingParty,
-        clientDataHash,
-        preferImmediatelyAvailableCredentials
-    ),
-    // The whole request data should be passed during the query phase.
-    candidateQueryData = toCredentialDataBundle(
-        requestJson, relyingParty, clientDataHash, preferImmediatelyAvailableCredentials
-    ),
-    requireSystemProvider = false,
-) {
-
-    init {
-        require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
-        require(relyingParty.isNotEmpty()) { "rp must not be empty" }
-        require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" }
-    }
-
-    /** A builder for [CreatePublicKeyCredentialRequestPrivileged]. */
-    class Builder(
-        private var requestJson: String,
-        private var relyingParty: String,
-        private var clientDataHash: String
-    ) {
-
-        private var preferImmediatelyAvailableCredentials: Boolean = false
-
-        /**
-         * Sets the privileged request in JSON format.
-         */
-        fun setRequestJson(requestJson: String): Builder {
-            this.requestJson = requestJson
-            return this
-        }
-
-        /**
-         * Sets to true if you prefer the operation to return immediately when there is no available
-         * passkey registration offering instead of falling back to discovering remote options, and
-         * false otherwise.
-         *
-         * The default value is false.
-         */
-        @Suppress("MissingGetterMatchingBuilder")
-        fun setPreferImmediatelyAvailableCredentials(
-            preferImmediatelyAvailableCredentials: Boolean
-        ): Builder {
-            this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
-            return this
-        }
-
-        /**
-         * Sets the expected true RP ID which will override the one in the [requestJson].
-         */
-        fun setRelyingParty(relyingParty: String): Builder {
-            this.relyingParty = relyingParty
-            return this
-        }
-
-        /**
-         * Sets a hash that is used to verify the [relyingParty] Identity.
-         */
-        fun setClientDataHash(clientDataHash: String): Builder {
-            this.clientDataHash = clientDataHash
-            return this
-        }
-
-        /** Builds a [CreatePublicKeyCredentialRequestPrivileged]. */
-        fun build(): CreatePublicKeyCredentialRequestPrivileged {
-            return CreatePublicKeyCredentialRequestPrivileged(
-                this.requestJson,
-                this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
-            )
-        }
-    }
-
-    /** @hide */
-    companion object {
-        internal const val BUNDLE_KEY_RELYING_PARTY =
-            "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
-        internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
-            "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
-        internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
-            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
-
-        internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-
-        internal const val BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV =
-            "androidx.credentials.BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_" +
-                    "PRIVILEGED"
-
-        @JvmStatic
-        internal fun toCredentialDataBundle(
-            requestJson: String,
-            relyingParty: String,
-            clientDataHash: String,
-            preferImmediatelyAvailableCredentials: Boolean
-        ): Bundle {
-            val bundle = Bundle()
-            bundle.putString(
-                PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
-                BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST_PRIV
-            )
-            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
-            bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
-            bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
-            bundle.putBoolean(
-                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
-                preferImmediatelyAvailableCredentials
-            )
-            return bundle
-        }
-
-        @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
-        // boolean value from being returned.
-        @JvmStatic
-        internal fun createFrom(data: Bundle): CreatePublicKeyCredentialRequestPrivileged {
-            try {
-                val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
-                val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
-                val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
-                val preferImmediatelyAvailableCredentials =
-                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
-                return CreatePublicKeyCredentialRequestPrivileged(
-                    requestJson!!,
-                    rp!!,
-                    clientDataHash!!,
-                    (preferImmediatelyAvailableCredentials!!) as Boolean,
-                )
-            } catch (e: Exception) {
-                throw FrameworkClassParsingException()
-            }
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt
deleted file mode 100644
index ee08e9e..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/Credential.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * Base class for a credential with which the user consented to authenticate to the app.
- *
- * @property type the credential type
- * @property data the credential data in the [Bundle] format.
- */
-open class Credential(val type: String, val data: Bundle)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt
deleted file mode 100644
index fc7b7de..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialOption.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.credentials.Credential
-import android.os.Bundle
-
-/**
- * Base request class for getting a registered credential.
- *
- * @property type the credential type
- * @property data the request data in the [Bundle] format
- * @property requireSystemProvider true if must only be fulfilled by a system provider and false
- *                              otherwise
- */
-open class GetCredentialOption(
-    open val type: String,
-    open val requestData: Bundle,
-    open val candidateQueryData: Bundle,
-    open val requireSystemProvider: Boolean,
-) {
-    companion object {
-        @JvmStatic
-        fun createFrom(
-            type: String,
-            requestData: Bundle,
-            candidateQueryData: Bundle,
-            requireSystemProvider: Boolean
-        ): GetCredentialOption {
-            return try {
-                when (type) {
-                    Credential.TYPE_PASSWORD_CREDENTIAL ->
-                        GetPasswordOption.createFrom(requestData)
-                    PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL ->
-                        when (requestData.getString(PublicKeyCredential.BUNDLE_KEY_SUBTYPE)) {
-                            GetPublicKeyCredentialOption
-                                .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION ->
-                                GetPublicKeyCredentialOption.createFrom(requestData)
-                            GetPublicKeyCredentialOptionPrivileged
-                                .BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED ->
-                                GetPublicKeyCredentialOptionPrivileged.createFrom(requestData)
-                            else -> throw FrameworkClassParsingException()
-                        }
-                    else -> throw FrameworkClassParsingException()
-                }
-            } catch (e: FrameworkClassParsingException) {
-                // Parsing failed but don't crash the process. Instead just output a request with
-                // the raw framework values.
-                GetCustomCredentialOption(
-                    type, requestData, candidateQueryData, requireSystemProvider)
-            }
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt
deleted file mode 100644
index fdd57ff..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCredentialRequest.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-/**
- * Encapsulates a request to get a user credential.
- *
- * @property getCredentialOptions the list of [GetCredentialOption] from which the user can choose
- * one to authenticate to the app
- * @throws IllegalArgumentException If [getCredentialOptions] is empty
- */
-class GetCredentialRequest constructor(
-    val getCredentialOptions: List<GetCredentialOption>,
-) {
-
-    init {
-        require(getCredentialOptions.isNotEmpty()) { "credentialRequests should not be empty" }
-    }
-
-    /** A builder for [GetCredentialRequest]. */
-    class Builder {
-        private var getCredentialOptions: MutableList<GetCredentialOption> = mutableListOf()
-
-        /** Adds a specific type of [GetCredentialOption]. */
-        fun addGetCredentialOption(getCredentialOption: GetCredentialOption): Builder {
-            getCredentialOptions.add(getCredentialOption)
-            return this
-        }
-
-        /** Sets the list of [GetCredentialOption]. */
-        fun setGetCredentialOptions(getCredentialOptions: List<GetCredentialOption>): Builder {
-            this.getCredentialOptions = getCredentialOptions.toMutableList()
-            return this
-        }
-
-        /**
-         * Builds a [GetCredentialRequest].
-         *
-         * @throws IllegalArgumentException If [getCredentialOptions] is empty
-         */
-        fun build(): GetCredentialRequest {
-            return GetCredentialRequest(getCredentialOptions.toList())
-        }
-    }
-
-    companion object {
-        @JvmStatic
-        fun createFrom(from: android.credentials.GetCredentialRequest): GetCredentialRequest {
-            return GetCredentialRequest(
-                from.credentialOptions.map {
-                    GetCredentialOption.createFrom(
-                        it.type,
-                        it.credentialRetrievalData,
-                        it.candidateQueryData,
-                        it.isSystemProviderRequired()
-                    )
-                }
-            )
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCustomCredentialOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCustomCredentialOption.kt
deleted file mode 100644
index 803885c..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetCustomCredentialOption.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * Allows extending custom versions of GetCredentialOptions for unique use cases.
- *
- * @property type the credential type determined by the credential-type-specific subclass
- * generated for custom use cases
- * @property requestData the request data in the [Bundle] format, generated for custom use cases
- * @property candidateQueryData the partial request data in the [Bundle] format that will be sent to
- * the provider during the initial candidate query stage, which should not contain sensitive user
- * information
- * @property requireSystemProvider true if must only be fulfilled by a system provider and false
- * otherwise
- * @throws IllegalArgumentException If [type] is empty
- * @throws NullPointerException If [requestData] or [type] is null
- */
-open class GetCustomCredentialOption(
-    final override val type: String,
-    final override val requestData: Bundle,
-    final override val candidateQueryData: Bundle,
-    @get:JvmName("requireSystemProvider")
-    final override val requireSystemProvider: Boolean
-) : GetCredentialOption(
-    type,
-    requestData,
-    candidateQueryData,
-    requireSystemProvider
-) {
-    init {
-        require(type.isNotEmpty()) { "type should not be empty" }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt
deleted file mode 100644
index 2b9cfa3..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPasswordOption.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.credentials.Credential
-import android.os.Bundle
-
-/** A request to retrieve the user's saved application password from their password provider. */
-class GetPasswordOption : GetCredentialOption(
-        Credential.TYPE_PASSWORD_CREDENTIAL,
-        Bundle(),
-        Bundle(),
-        false,
-) {
-    companion object {
-        @JvmStatic
-        fun createFrom(data: Bundle): GetPasswordOption {
-            return GetPasswordOption()
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt
deleted file mode 100644
index 2f9b249..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOption.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * A request to get passkeys from the user's public key credential provider.
- *
- * @property requestJson the privileged request in JSON format in the standard webauthn web json
- * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
- * immediately when there is no available credential instead of falling back to discovering remote
- * credentials, and false (default) otherwise
- * @throws NullPointerException If [requestJson] is null
- * @throws IllegalArgumentException If [requestJson] is empty
- */
-class GetPublicKeyCredentialOption @JvmOverloads constructor(
-    val requestJson: String,
-    @get:JvmName("preferImmediatelyAvailableCredentials")
-    val preferImmediatelyAvailableCredentials: Boolean = false,
-) : GetCredentialOption(
-    type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    requestData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
-    candidateQueryData = toRequestDataBundle(requestJson, preferImmediatelyAvailableCredentials),
-    requireSystemProvider = false
-) {
-    init {
-        require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
-    }
-
-    /** @hide */
-    companion object {
-        internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
-            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
-        internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-        internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION =
-            "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION"
-
-        @JvmStatic
-        internal fun toRequestDataBundle(
-            requestJson: String,
-            preferImmediatelyAvailableCredentials: Boolean
-        ): Bundle {
-            val bundle = Bundle()
-            bundle.putString(
-                PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
-                BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION
-            )
-            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
-            bundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
-                preferImmediatelyAvailableCredentials)
-            return bundle
-        }
-
-        @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
-        // boolean value from being returned.
-        @JvmStatic
-        internal fun createFrom(data: Bundle): GetPublicKeyCredentialOption {
-            try {
-                val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
-                val preferImmediatelyAvailableCredentials =
-                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
-                return GetPublicKeyCredentialOption(requestJson!!,
-                    (preferImmediatelyAvailableCredentials!!) as Boolean)
-            } catch (e: Exception) {
-                throw FrameworkClassParsingException()
-            }
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt
deleted file mode 100644
index 6f4782a..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/GetPublicKeyCredentialOptionPrivileged.kt
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * A privileged request to get passkeys from the user's public key credential provider. The caller
- * can modify the RP. Only callers with privileged permission (e.g. user's public browser or caBLE)
- * can use this. These permissions will be introduced in an upcoming release.
- * TODO("Add specific permission info/annotation")
- *
- * @property requestJson the privileged request in JSON format in the standard webauthn web json
- * shown [here](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson).
- * @property preferImmediatelyAvailableCredentials true if you prefer the operation to return
- * immediately when there is no available credential instead of falling back to discovering remote
- * credentials, and false (default) otherwise
- * @property relyingParty the expected true RP ID which will override the one in the [requestJson],
- * where relyingParty is defined [here](https://w3c.github.io/webauthn/#rp-id) in more detail
- * @property clientDataHash a hash that is used to verify the [relyingParty] Identity
- * @throws NullPointerException If any of [requestJson], [relyingParty], or [clientDataHash]
- * is null
- * @throws IllegalArgumentException If any of [requestJson], [relyingParty], or [clientDataHash] is
- * empty
- */
-class GetPublicKeyCredentialOptionPrivileged @JvmOverloads constructor(
-    val requestJson: String,
-    val relyingParty: String,
-    val clientDataHash: String,
-    @get:JvmName("preferImmediatelyAvailableCredentials")
-    val preferImmediatelyAvailableCredentials: Boolean = false
-) : GetCredentialOption(
-    type = PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL,
-    requestData = toBundle(
-        requestJson,
-        relyingParty,
-        clientDataHash,
-        preferImmediatelyAvailableCredentials
-    ),
-    candidateQueryData = toBundle(
-        requestJson,
-        relyingParty,
-        clientDataHash,
-        preferImmediatelyAvailableCredentials
-    ),
-    requireSystemProvider = false,
-) {
-
-    init {
-        require(requestJson.isNotEmpty()) { "requestJson must not be empty" }
-        require(relyingParty.isNotEmpty()) { "rp must not be empty" }
-        require(clientDataHash.isNotEmpty()) { "clientDataHash must not be empty" }
-    }
-
-    /** A builder for [GetPublicKeyCredentialOptionPrivileged]. */
-    class Builder(
-        private var requestJson: String,
-        private var relyingParty: String,
-        private var clientDataHash: String
-    ) {
-
-        private var preferImmediatelyAvailableCredentials: Boolean = false
-
-        /**
-         * Sets the privileged request in JSON format.
-         */
-        fun setRequestJson(requestJson: String): Builder {
-            this.requestJson = requestJson
-            return this
-        }
-
-        /**
-         * Sets to true if you prefer the operation to return immediately when there is no available
-         * credential instead of falling back to discovering remote credentials, and false
-         * otherwise.
-         *
-         * The default value is false.
-         */
-        @Suppress("MissingGetterMatchingBuilder")
-        fun setPreferImmediatelyAvailableCredentials(
-            preferImmediatelyAvailableCredentials: Boolean
-        ): Builder {
-            this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials
-            return this
-        }
-
-        /**
-         * Sets the expected true RP ID which will override the one in the [requestJson].
-         */
-        fun setRelyingParty(relyingParty: String): Builder {
-            this.relyingParty = relyingParty
-            return this
-        }
-
-        /**
-         * Sets a hash that is used to verify the [relyingParty] Identity.
-         */
-        fun setClientDataHash(clientDataHash: String): Builder {
-            this.clientDataHash = clientDataHash
-            return this
-        }
-
-        /** Builds a [GetPublicKeyCredentialOptionPrivileged]. */
-        fun build(): GetPublicKeyCredentialOptionPrivileged {
-            return GetPublicKeyCredentialOptionPrivileged(
-                this.requestJson,
-                this.relyingParty, this.clientDataHash, this.preferImmediatelyAvailableCredentials
-            )
-        }
-    }
-
-    /** @hide */
-    companion object {
-        internal const val BUNDLE_KEY_RELYING_PARTY =
-            "androidx.credentials.BUNDLE_KEY_RELYING_PARTY"
-        internal const val BUNDLE_KEY_CLIENT_DATA_HASH =
-            "androidx.credentials.BUNDLE_KEY_CLIENT_DATA_HASH"
-        internal const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
-            "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS"
-        internal const val BUNDLE_KEY_REQUEST_JSON = "androidx.credentials.BUNDLE_KEY_REQUEST_JSON"
-        internal const val BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED =
-            "androidx.credentials.BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION" +
-                    "_PRIVILEGED"
-
-        @JvmStatic
-        internal fun toBundle(
-            requestJson: String,
-            relyingParty: String,
-            clientDataHash: String,
-            preferImmediatelyAvailableCredentials: Boolean
-        ): Bundle {
-            val bundle = Bundle()
-            bundle.putString(
-                PublicKeyCredential.BUNDLE_KEY_SUBTYPE,
-                BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION_PRIVILEGED
-            )
-            bundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJson)
-            bundle.putString(BUNDLE_KEY_RELYING_PARTY, relyingParty)
-            bundle.putString(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash)
-            bundle.putBoolean(
-                BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS,
-                preferImmediatelyAvailableCredentials
-            )
-            return bundle
-        }
-
-        @Suppress("deprecation") // bundle.get() used for boolean value to prevent default
-        // boolean value from being returned.
-        @JvmStatic
-        internal fun createFrom(data: Bundle): GetPublicKeyCredentialOptionPrivileged {
-            try {
-                val requestJson = data.getString(BUNDLE_KEY_REQUEST_JSON)
-                val rp = data.getString(BUNDLE_KEY_RELYING_PARTY)
-                val clientDataHash = data.getString(BUNDLE_KEY_CLIENT_DATA_HASH)
-                val preferImmediatelyAvailableCredentials =
-                    data.get(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS)
-                return GetPublicKeyCredentialOptionPrivileged(
-                    requestJson!!,
-                    rp!!,
-                    clientDataHash!!,
-                    (preferImmediatelyAvailableCredentials!!) as Boolean,
-                )
-            } catch (e: Exception) {
-                throw FrameworkClassParsingException()
-            }
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PasswordCredential.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PasswordCredential.kt
deleted file mode 100644
index 1658858..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PasswordCredential.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-class PasswordCredential constructor(
-        val id: String,
-        val password: String,
-) : Credential(android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL, toBundle(id, password)) {
-
-    init {
-        require(password.isNotEmpty()) { "password should not be empty" }
-    }
-
-    /** @hide */
-    companion object {
-
-        const val BUNDLE_KEY_ID = "androidx.credentials.BUNDLE_KEY_ID"
-        const val BUNDLE_KEY_PASSWORD = "androidx.credentials.BUNDLE_KEY_PASSWORD"
-
-        @JvmStatic
-        internal fun toBundle(id: String, password: String): Bundle {
-            val bundle = Bundle()
-            bundle.putString(BUNDLE_KEY_ID, id)
-            bundle.putString(BUNDLE_KEY_PASSWORD, password)
-            return bundle
-        }
-
-        @JvmStatic
-        internal fun createFrom(data: Bundle): PasswordCredential {
-            try {
-                val id = data.getString(BUNDLE_KEY_ID)
-                val password = data.getString(BUNDLE_KEY_PASSWORD)
-                return PasswordCredential(id!!, password!!)
-            } catch (e: Exception) {
-                throw FrameworkClassParsingException()
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt
deleted file mode 100644
index 6a81167..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/developer/PublicKeyCredential.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.developer
-
-import android.os.Bundle
-
-/**
- * Represents the user's passkey credential granted by the user for app sign-in.
- *
- * @property authenticationResponseJson the public key credential authentication response in
- * JSON format that follows the standard webauthn json format shown at
- * [this w3c link](https://w3c.github.io/webauthn/#dictdef-authenticationresponsejson)
- * @throws NullPointerException If [authenticationResponseJson] is null. This is handled by the
- * kotlin runtime
- * @throws IllegalArgumentException If [authenticationResponseJson] is empty
- *
- * @hide
- */
-class PublicKeyCredential constructor(
-        val authenticationResponseJson: String
-) : Credential(
-        TYPE_PUBLIC_KEY_CREDENTIAL,
-        toBundle(authenticationResponseJson)
-) {
-
-    init {
-        require(authenticationResponseJson.isNotEmpty()) {
-            "authentication response JSON must not be empty" }
-    }
-    companion object {
-        /** The type value for public key credential related operations. */
-        const val TYPE_PUBLIC_KEY_CREDENTIAL: String =
-                "androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"
-        /** The Bundle key value for the public key credential subtype (privileged or regular). */
-        internal const val BUNDLE_KEY_SUBTYPE = "androidx.credentials.BUNDLE_KEY_SUBTYPE"
-        const val BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON =
-                "androidx.credentials.BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON"
-
-        @JvmStatic
-        internal fun toBundle(authenticationResponseJson: String): Bundle {
-            val bundle = Bundle()
-            bundle.putString(BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON, authenticationResponseJson)
-            return bundle
-        }
-    }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/Action.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/Action.kt
deleted file mode 100644
index 1abf911..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/Action.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.provider
-
-import android.annotation.SuppressLint
-import android.app.PendingIntent
-import android.app.slice.Slice
-import android.app.slice.SliceSpec
-import android.net.Uri
-import android.util.Log
-import java.util.Collections
-
-/**
- * UI representation for a credential entry used during the get credential flow.
- *
- * TODO: move to jetpack.
- */
-class Action constructor(
-        val title: CharSequence,
-        val subTitle: CharSequence?,
-        val pendingIntent: PendingIntent?,
-) {
-
-  init {
-    require(title.isNotEmpty()) { "title must not be empty" }
-  }
-
-  companion object {
-    private const val TAG = "Action"
-    internal const val SLICE_HINT_TITLE =
-            "androidx.credentials.provider.action.HINT_ACTION_TITLE"
-    internal const val SLICE_HINT_SUBTITLE =
-            "androidx.credentials.provider.action.HINT_ACTION_SUBTEXT"
-    internal const val SLICE_HINT_PENDING_INTENT =
-            "androidx.credentials.provider.action.SLICE_HINT_PENDING_INTENT"
-
-    @JvmStatic
-    fun toSlice(action: Action): Slice {
-      // TODO("Put the right spec and version value")
-      val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))
-              .addText(action.title, /*subType=*/null,
-                      listOf(SLICE_HINT_TITLE))
-              .addText(action.subTitle, /*subType=*/null,
-                      listOf(SLICE_HINT_SUBTITLE))
-      if (action.pendingIntent != null) {
-        sliceBuilder.addAction(action.pendingIntent,
-                Slice.Builder(sliceBuilder)
-                        .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
-                        .build(),
-                /*subType=*/null)
-      }
-      return sliceBuilder.build()
-    }
-
-    /**
-     * Returns an instance of [Action] derived from a [Slice] object.
-     *
-     * @param slice the [Slice] object constructed through [toSlice]
-     */
-    @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
-    @JvmStatic
-    fun fromSlice(slice: Slice): Action? {
-      // TODO("Put the right spec and version value")
-      var title: CharSequence = ""
-      var subTitle: CharSequence? = null
-      var pendingIntent: PendingIntent? = null
-
-      slice.items.forEach {
-        if (it.hasHint(SLICE_HINT_TITLE)) {
-          title = it.text
-        } else if (it.hasHint(SLICE_HINT_SUBTITLE)) {
-          subTitle = it.text
-        } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
-          pendingIntent = it.action
-        }
-      }
-
-      return try {
-        Action(title, subTitle, pendingIntent)
-      } catch (e: Exception) {
-        Log.i(TAG, "fromSlice failed with: " + e.message)
-        null
-      }
-    }
-  }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt
deleted file mode 100644
index 283c7ba..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.provider
-
-import android.app.PendingIntent
-import android.app.slice.Slice
-import android.util.Log
-import androidx.annotation.VisibleForTesting
-
-/**
- * UI representation for a credential entry used during the get credential flow.
- *
- * TODO: move to jetpack.
- */
-class AuthenticationAction constructor(
-        val pendingIntent: PendingIntent
-) {
-
-
-  companion object {
-    private const val TAG = "AuthenticationAction"
-    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    internal const val SLICE_HINT_PENDING_INTENT =
-            "androidx.credentials.provider.authenticationAction.SLICE_HINT_PENDING_INTENT"
-
-    @JvmStatic
-    fun fromSlice(slice: Slice): AuthenticationAction? {
-      slice.items.forEach {
-        if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
-          return try {
-            AuthenticationAction(it.action)
-          } catch (e: Exception) {
-            Log.i(TAG, "fromSlice failed with: " + e.message)
-            null
-          }
-        }
-      }
-      return null
-    }
-  }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CreateEntry.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CreateEntry.kt
deleted file mode 100644
index 0ec91d6..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CreateEntry.kt
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.provider
-
-import android.annotation.SuppressLint
-import android.app.PendingIntent
-import android.app.slice.Slice
-import android.app.slice.SliceSpec
-import android.graphics.drawable.Icon
-import android.net.Uri
-import android.os.Bundle
-import android.util.Log
-import java.util.Collections
-
-/**
- * UI representation for a save entry used during the create credential flow.
- *
- * TODO: move to jetpack.
- */
-class CreateEntry internal constructor(
-        val accountName: CharSequence,
-        val pendingIntent: PendingIntent?,
-        val icon: Icon?,
-        val lastUsedTimeMillis: Long,
-        val credentialCountInformationList: List<CredentialCountInformation>,
-        val footerDescription: CharSequence?,
-) {
-
-  init {
-    require(accountName.isNotEmpty()) { "accountName must not be empty" }
-  }
-
-  /**
-   * A builder for [CreateEntry]
-   *
-   * @property accountName the name of the account where the credential will be registered
-   * @property pendingIntent the [PendingIntent] that will be fired when the user selects
-   * this entry
-   *
-   * @hide
-   */
-  class Builder constructor(
-          private val accountName: CharSequence,
-          private val pendingIntent: PendingIntent? = null
-  ) {
-
-    private var credentialCountInformationList: MutableList<CredentialCountInformation> =
-            mutableListOf()
-    private var icon: Icon? = null
-    private var lastUsedTimeMillis: Long = 0
-    private var footerDescription: CharSequence? = null
-
-    /** Adds a [CredentialCountInformation] denoting a given credential
-     * type and the count of credentials that the provider has stored for that
-     * credential type.
-     *
-     * This information will be displayed on the [CreateEntry] to help the user
-     * make a choice.
-     */
-    @Suppress("MissingGetterMatchingBuilder")
-    fun addCredentialCountInformation(info: CredentialCountInformation): Builder {
-      credentialCountInformationList.add(info)
-      return this
-    }
-
-    /** Sets a list of [CredentialCountInformation]. Each item in the list denotes a given
-     * credential type and the count of credentials that the provider has stored of that
-     * credential type.
-     *
-     * This information will be displayed on the [CreateEntry] to help the user
-     * make a choice.
-     */
-    fun setCredentialCountInformationList(infoList: List<CredentialCountInformation>): Builder {
-      credentialCountInformationList = infoList as MutableList<CredentialCountInformation>
-      return this
-    }
-
-    /** Sets an icon to be displayed with the entry on the UI */
-    fun setIcon(icon: Icon?): Builder {
-      this.icon = icon
-      return this
-    }
-
-    /** Sets the last time this account was used */
-    fun setLastUsedTimeMillis(lastUsedTimeMillis: Long): Builder {
-      this.lastUsedTimeMillis = lastUsedTimeMillis
-      return this
-    }
-
-    /** Sets the footer description of this */
-    fun setFooterDescription(footerDescription: CharSequence): Builder {
-      this.footerDescription = footerDescription
-      return this
-    }
-
-    /**
-     * Builds an instance of [CreateEntry]
-     *
-     * @throws IllegalArgumentException If [accountName] is empty
-     */
-    fun build(): CreateEntry {
-      return CreateEntry(accountName, pendingIntent, icon, lastUsedTimeMillis,
-              credentialCountInformationList, footerDescription)
-    }
-  }
-
-  companion object {
-    private const val TAG = "CreateEntry"
-    internal const val SLICE_HINT_ACCOUNT_NAME =
-            "androidx.credentials.provider.createEntry.SLICE_HINT_USER_PROVIDER_ACCOUNT_NAME"
-    internal const val SLICE_HINT_ICON =
-            "androidx.credentials.provider.createEntry.SLICE_HINT_PROFILE_ICON"
-    internal const val SLICE_HINT_CREDENTIAL_COUNT_INFORMATION =
-            "androidx.credentials.provider.createEntry.SLICE_HINT_CREDENTIAL_COUNT_INFORMATION"
-    internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
-            "androidx.credentials.provider.createEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
-    internal const val SLICE_HINT_PENDING_INTENT =
-            "androidx.credentials.provider.createEntry.SLICE_HINT_PENDING_INTENT"
-    internal const val SLICE_HINT_FOOTER_DESCRIPTION =
-            "androidx.credentials.provider.createEntry.SLICE_HINT_FOOTER_DESCRIPTION"
-
-    @JvmStatic
-    fun toSlice(createEntry: CreateEntry): Slice {
-      // TODO("Use the right type and revision")
-      val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec("type", 1))
-      sliceBuilder.addText(createEntry.accountName, /*subType=*/null,
-              listOf(SLICE_HINT_ACCOUNT_NAME))
-              .addLong(createEntry.lastUsedTimeMillis, /*subType=*/null, listOf(
-                      SLICE_HINT_LAST_USED_TIME_MILLIS))
-      if (createEntry.icon != null) {
-        sliceBuilder.addIcon(createEntry.icon, /*subType=*/null,
-                listOf(SLICE_HINT_ICON))
-      }
-
-      val credentialCountBundle = convertCredentialCountInfoToBundle(
-              createEntry.credentialCountInformationList)
-      if (credentialCountBundle != null) {
-        sliceBuilder.addBundle(convertCredentialCountInfoToBundle(
-                createEntry.credentialCountInformationList), null, listOf(
-                SLICE_HINT_CREDENTIAL_COUNT_INFORMATION))
-      }
-      if (createEntry.pendingIntent != null) {
-        sliceBuilder.addAction(createEntry.pendingIntent,
-                Slice.Builder(sliceBuilder)
-                        .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
-                        .build(),
-                /*subType=*/null)
-      }
-      if (createEntry.footerDescription != null) {
-        sliceBuilder.addText(createEntry.footerDescription, /*subType=*/null,
-                listOf(SLICE_HINT_FOOTER_DESCRIPTION))
-      }
-      return sliceBuilder.build()
-    }
-
-    /**
-     * Returns an instance of [CreateEntry] derived from a [Slice] object.
-     *
-     * @param slice the [Slice] object constructed through [toSlice]
-     */
-    @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
-    @JvmStatic
-    fun fromSlice(slice: Slice): CreateEntry? {
-      // TODO("Put the right spec and version value")
-      var accountName: CharSequence = ""
-      var icon: Icon? = null
-      var pendingIntent: PendingIntent? = null
-      var credentialCountInfo: List<CredentialCountInformation> = listOf()
-      var lastUsedTimeMillis: Long = 0
-      var footerDescription: CharSequence? = null
-
-      slice.items.forEach {
-        if (it.hasHint(SLICE_HINT_ACCOUNT_NAME)) {
-          accountName = it.text
-        } else if (it.hasHint(SLICE_HINT_ICON)) {
-          icon = it.icon
-        } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
-          pendingIntent = it.action
-        } else if (it.hasHint(SLICE_HINT_CREDENTIAL_COUNT_INFORMATION)) {
-          credentialCountInfo = convertBundleToCredentialCountInfo(it.bundle)
-        } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
-          lastUsedTimeMillis = it.long
-        } else if (it.hasHint(SLICE_HINT_FOOTER_DESCRIPTION)) {
-          footerDescription = it.text
-        }
-      }
-
-      return try {
-        CreateEntry(accountName, pendingIntent, icon,
-                lastUsedTimeMillis, credentialCountInfo, footerDescription)
-      } catch (e: Exception) {
-        Log.i(TAG, "fromSlice failed with: " + e.message)
-        null
-      }
-    }
-
-    @JvmStatic
-    internal fun convertBundleToCredentialCountInfo(bundle: Bundle?):
-            List<CredentialCountInformation> {
-      val credentialCountList = ArrayList<CredentialCountInformation>()
-      if (bundle == null) {
-        return credentialCountList
-      }
-      bundle.keySet().forEach {
-        try {
-          credentialCountList.add(
-                  CredentialCountInformation(it, bundle.getInt(it)))
-        } catch (e: Exception) {
-          Log.i(TAG, "Issue unpacking credential count info bundle: " + e.message)
-        }
-      }
-      return credentialCountList
-    }
-
-    @JvmStatic
-    internal fun convertCredentialCountInfoToBundle(
-            credentialCountInformationList: List<CredentialCountInformation>
-    ): Bundle? {
-      if (credentialCountInformationList.isEmpty()) {
-        return null
-      }
-      val bundle = Bundle()
-      credentialCountInformationList.forEach {
-        bundle.putInt(it.type, it.count)
-      }
-      return bundle
-    }
-  }
-}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialCountInformation.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialCountInformation.kt
deleted file mode 100644
index aa77b74..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialCountInformation.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.provider
-
-import android.credentials.Credential
-import com.android.credentialmanager.jetpack.developer.PublicKeyCredential
-
-class CredentialCountInformation constructor(
-        val type: String,
-        val count: Int
-) {
-    companion object {
-        @JvmStatic
-        fun createPasswordCountInformation(count: Int): CredentialCountInformation {
-            return CredentialCountInformation(Credential.TYPE_PASSWORD_CREDENTIAL, count)
-        }
-
-        @JvmStatic
-        fun getPasswordCount(infos: List<CredentialCountInformation>): Int? {
-            return getCountForType(infos, Credential.TYPE_PASSWORD_CREDENTIAL)
-        }
-
-        @JvmStatic
-        fun createPublicKeyCountInformation(count: Int): CredentialCountInformation {
-            return CredentialCountInformation(PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL, count)
-        }
-
-        @JvmStatic
-        fun getPasskeyCount(infos: List<CredentialCountInformation>): Int? {
-            return getCountForType(infos, PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL)
-        }
-
-        @JvmStatic
-        fun createTotalCountInformation(count: Int): CredentialCountInformation {
-            return CredentialCountInformation("TOTAL_COUNT", count)
-        }
-
-        @JvmStatic
-        fun getTotalCount(infos: List<CredentialCountInformation>): Int? {
-            return getCountForType(infos, "TOTAL_COUNT")
-        }
-
-        private fun getCountForType(infos: List<CredentialCountInformation>, type: String): Int? {
-            return infos.firstOrNull { info -> info.type == type }?.count
-        }
-    }
-}
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntry.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntry.kt
deleted file mode 100644
index 61a104b..0000000
--- a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/CredentialEntry.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.credentialmanager.jetpack.provider
-
-import android.annotation.SuppressLint
-import android.app.PendingIntent
-import android.app.slice.Slice
-import android.app.slice.SliceSpec
-import android.graphics.drawable.Icon
-import android.net.Uri
-import android.util.Log
-import java.util.Collections
-
-/**
- * UI representation for a credential entry used during the get credential flow.
- *
- * TODO: move to jetpack.
- */
-open class CredentialEntry constructor(
-        // TODO("Add credential type display name for both CredentialEntry & CreateEntry")
-        val type: String,
-        val typeDisplayName: CharSequence,
-        val username: CharSequence,
-        val displayName: CharSequence?,
-        val pendingIntent: PendingIntent?,
-        // TODO("Consider using Instant or other strongly typed time data type")
-        val lastUsedTimeMillis: Long,
-        val icon: Icon?,
-        var autoSelectAllowed: Boolean
-) {
-  init {
-    require(type.isNotEmpty()) { "type must not be empty" }
-    require(username.isNotEmpty()) { "type must not be empty" }
-  }
-
-  companion object {
-    private const val TAG = "CredentialEntry"
-    internal const val SLICE_HINT_TYPE_DISPLAY_NAME =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_TYPE_DISPLAY_NAME"
-    internal const val SLICE_HINT_USERNAME =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_USER_NAME"
-    internal const val SLICE_HINT_DISPLAYNAME =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_CREDENTIAL_TYPE_DISPLAY_NAME"
-    internal const val SLICE_HINT_LAST_USED_TIME_MILLIS =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_LAST_USED_TIME_MILLIS"
-    internal const val SLICE_HINT_ICON =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PROFILE_ICON"
-    internal const val SLICE_HINT_PENDING_INTENT =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_PENDING_INTENT"
-    internal const val SLICE_HINT_AUTO_ALLOWED =
-            "androidx.credentials.provider.credentialEntry.SLICE_HINT_AUTO_ALLOWED"
-    internal const val AUTO_SELECT_TRUE_STRING = "true"
-    internal const val AUTO_SELECT_FALSE_STRING = "false"
-
-    @JvmStatic
-    internal fun toSlice(credentialEntry: CredentialEntry): Slice {
-      // TODO("Put the right revision value")
-      val autoSelectAllowed = if (credentialEntry.autoSelectAllowed) {
-        AUTO_SELECT_TRUE_STRING
-      } else {
-        AUTO_SELECT_FALSE_STRING
-      }
-      val sliceBuilder = Slice.Builder(Uri.EMPTY, SliceSpec(
-              credentialEntry.type, 1))
-              .addText(credentialEntry.typeDisplayName, /*subType=*/null,
-                      listOf(SLICE_HINT_TYPE_DISPLAY_NAME))
-              .addText(credentialEntry.username, /*subType=*/null,
-                      listOf(SLICE_HINT_USERNAME))
-              .addText(credentialEntry.displayName, /*subType=*/null,
-                      listOf(SLICE_HINT_DISPLAYNAME))
-              .addLong(credentialEntry.lastUsedTimeMillis, /*subType=*/null,
-                      listOf(SLICE_HINT_LAST_USED_TIME_MILLIS))
-              .addText(autoSelectAllowed, /*subType=*/null,
-                      listOf(SLICE_HINT_AUTO_ALLOWED))
-      if (credentialEntry.icon != null) {
-        sliceBuilder.addIcon(credentialEntry.icon, /*subType=*/null,
-                listOf(SLICE_HINT_ICON))
-      }
-      if (credentialEntry.pendingIntent != null) {
-        sliceBuilder.addAction(credentialEntry.pendingIntent,
-                Slice.Builder(sliceBuilder)
-                        .addHints(Collections.singletonList(SLICE_HINT_PENDING_INTENT))
-                        .build(),
-                /*subType=*/null)
-      }
-      return sliceBuilder.build()
-    }
-
-    /**
-     * Returns an instance of [CredentialEntry] derived from a [Slice] object.
-     *
-     * @param slice the [Slice] object constructed through [toSlice]
-     */
-    @SuppressLint("WrongConstant") // custom conversion between jetpack and framework
-    @JvmStatic
-    fun fromSlice(slice: Slice): CredentialEntry? {
-      var typeDisplayName: CharSequence? = null
-      var username: CharSequence? = null
-      var displayName: CharSequence? = null
-      var icon: Icon? = null
-      var pendingIntent: PendingIntent? = null
-      var lastUsedTimeMillis: Long = 0
-      var autoSelectAllowed = false
-
-      slice.items.forEach {
-        if (it.hasHint(SLICE_HINT_TYPE_DISPLAY_NAME)) {
-          typeDisplayName = it.text
-        } else if (it.hasHint(SLICE_HINT_USERNAME)) {
-          username = it.text
-        } else if (it.hasHint(SLICE_HINT_DISPLAYNAME)) {
-          displayName = it.text
-        } else if (it.hasHint(SLICE_HINT_ICON)) {
-          icon = it.icon
-        } else if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
-          pendingIntent = it.action
-        } else if (it.hasHint(SLICE_HINT_LAST_USED_TIME_MILLIS)) {
-          lastUsedTimeMillis = it.long
-        } else if (it.hasHint(SLICE_HINT_AUTO_ALLOWED)) {
-          val autoSelectValue = it.text
-          if (autoSelectValue == AUTO_SELECT_TRUE_STRING) {
-            autoSelectAllowed = true
-          }
-        }
-      }
-
-      return try {
-        CredentialEntry(slice.spec!!.type, typeDisplayName!!, username!!,
-                displayName, pendingIntent,
-                lastUsedTimeMillis, icon, autoSelectAllowed)
-      } catch (e: Exception) {
-        Log.i(TAG, "fromSlice failed with: " + e.message)
-        null
-      }
-    }
-  }
-}
diff --git a/packages/SettingsLib/ActivityEmbedding/OWNERS b/packages/SettingsLib/ActivityEmbedding/OWNERS
new file mode 100644
index 0000000..7022402
--- /dev/null
+++ b/packages/SettingsLib/ActivityEmbedding/OWNERS
@@ -0,0 +1,5 @@
+# Default reviewers for this and subdirectories.
+arcwang@google.com
+chiujason@google.com
+
+# Emergency approvers in case the above are not available
diff --git a/packages/SettingsLib/OWNERS b/packages/SettingsLib/OWNERS
index 36315b5..1b3020a 100644
--- a/packages/SettingsLib/OWNERS
+++ b/packages/SettingsLib/OWNERS
@@ -1,12 +1,11 @@
 # People who can approve changes for submission
 dsandler@android.com
 edgarwang@google.com
-emilychuang@google.com
 evanlaird@google.com
-hanxu@google.com
 juliacr@google.com
+lijun@google.com
+songchenxi@google.com
 yantingyang@google.com
-ykhung@google.com
 
 # Exempt resource files (because they are in a flat directory and too hard to manage via OWNERS)
 per-file *.xml=*
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ChartPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ChartPage.kt
index eeab085..69c4705 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ChartPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/ChartPage.kt
@@ -46,7 +46,7 @@
 
 object ChartPageProvider : SettingsPageProvider {
     override val name = "Chart"
-    val owner = createSettingsPage()
+    private val owner = createSettingsPage()
 
     override fun getTitle(arguments: Bundle?): String {
         return TITLE
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
index 2328fcb..9c7e0ce 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/FooterPage.kt
@@ -37,7 +37,7 @@
 
 object FooterPageProvider : SettingsPageProvider {
     override val name = "Footer"
-    val owner = createSettingsPage()
+    private val owner = createSettingsPage()
 
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         val entryList = mutableListOf<SettingsEntry>()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt
index 45b7989..ee22b96 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/IllustrationPage.kt
@@ -36,7 +36,7 @@
 
 object IllustrationPageProvider : SettingsPageProvider {
     override val name = "Illustration"
-    val owner = createSettingsPage()
+    private val owner = createSettingsPage()
 
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         val entryList = mutableListOf<SettingsEntry>()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
index 74e49a6..1051549 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/page/SliderPage.kt
@@ -42,7 +42,7 @@
 
 object SliderPageProvider : SettingsPageProvider {
     override val name = "Slider"
-    val owner = createSettingsPage()
+    private val owner = createSettingsPage()
 
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         val entryList = mutableListOf<SettingsEntry>()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt
index d100d9d..442ace8 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/MainSwitchPreferencePage.kt
@@ -38,7 +38,7 @@
 
 object MainSwitchPreferencePageProvider : SettingsPageProvider {
     override val name = "MainSwitchPreference"
-    val owner = createSettingsPage()
+    private val owner = createSettingsPage()
 
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         val entryList = mutableListOf<SettingsEntry>()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt
index 6ad4bd8..b67e066 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/SwitchPreferencePage.kt
@@ -43,7 +43,7 @@
 
 object SwitchPreferencePageProvider : SettingsPageProvider {
     override val name = "SwitchPreference"
-    val owner = createSettingsPage()
+    private val owner = createSettingsPage()
 
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         val entryList = mutableListOf<SettingsEntry>()
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt
index 770f9a0..a2cd283 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/preference/TwoTargetSwitchPreferencePage.kt
@@ -40,7 +40,7 @@
 
 object TwoTargetSwitchPreferencePageProvider : SettingsPageProvider {
     override val name = "TwoTargetSwitchPreference"
-    val owner = createSettingsPage()
+    private val owner = createSettingsPage()
 
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         val entryList = mutableListOf<SettingsEntry>()
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
index 838c0cf..494e3cc 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/DebugProvider.kt
@@ -161,7 +161,7 @@
                 .add(ColumnEnum.PAGE_ROUTE.id, page.buildRoute())
                 .add(ColumnEnum.PAGE_INTENT_URI.id, intent.toUri(URI_INTENT_SCHEME))
                 .add(ColumnEnum.PAGE_ENTRY_COUNT.id, pageWithEntry.entries.size)
-                .add(ColumnEnum.HAS_RUNTIME_PARAM.id, if (page.hasRuntimeParam()) 1 else 0)
+                .add(ColumnEnum.PAGE_BROWSABLE.id, if (page.isBrowsable()) 1 else 0)
         }
         return cursor
     }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
index bb9a134..fc6160e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/debug/ProviderColumn.kt
@@ -28,7 +28,7 @@
     PAGE_ROUTE("pageRoute"),
     PAGE_INTENT_URI("pageIntent"),
     PAGE_ENTRY_COUNT("entryCount"),
-    HAS_RUNTIME_PARAM("hasRuntimeParam"),
+    PAGE_BROWSABLE("pageBrowsable"),
     PAGE_START_ADB("pageStartAdb"),
 
     // Columns related to entry
@@ -67,7 +67,7 @@
             ColumnEnum.PAGE_ROUTE,
             ColumnEnum.PAGE_INTENT_URI,
             ColumnEnum.PAGE_ENTRY_COUNT,
-            ColumnEnum.HAS_RUNTIME_PARAM,
+            ColumnEnum.PAGE_BROWSABLE,
         )
     ),
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
index bccd8aa..f1b1abd 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt
@@ -39,7 +39,7 @@
 import androidx.navigation.NavGraph.Companion.findStartDestination
 import com.android.settingslib.spa.R
 import com.android.settingslib.spa.framework.common.LogCategory
-import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.common.NullPageProvider
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
 import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
@@ -106,14 +106,13 @@
 
 @Composable
 private fun NavControllerWrapperImpl.NavContent(allProvider: Collection<SettingsPageProvider>) {
-    val nullPage = SettingsPage.createNull()
     AnimatedNavHost(
         navController = navController,
-        startDestination = nullPage.sppName,
+        startDestination = NullPageProvider.name,
     ) {
         val slideEffect = tween<IntOffset>(durationMillis = 300)
         val fadeEffect = tween<Float>(durationMillis = 300)
-        composable(nullPage.sppName) {}
+        composable(NullPageProvider.name) {}
         for (spp in allProvider) {
             composable(
                 route = spp.name + spp.parameter.navRoute(),
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
index 44714ab..4b73e94 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntry.kt
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.remember
 import com.android.settingslib.spa.framework.compose.LocalNavController
+import com.android.settingslib.spa.framework.util.genEntryId
 
 private const val INJECT_ENTRY_NAME = "INJECT"
 private const val ROOT_ENTRY_NAME = "ROOT"
@@ -188,7 +189,7 @@
         val page = fromPage ?: owner
         val isEnabled = page.isEnabled()
         return SettingsEntry(
-            id = id(),
+            id = genEntryId(name, owner, fromPage, toPage),
             name = name,
             owner = owner,
             displayName = displayName,
@@ -274,11 +275,6 @@
         return this
     }
 
-    // The unique id of this entry, which is computed by name + owner + fromPage + toPage.
-    private fun id(): String {
-        return "$name:${owner.id}(${fromPage?.id}-${toPage?.id})".toHashId()
-    }
-
     companion object {
         fun create(entryName: String, owner: SettingsPage): SettingsEntryBuilder {
             return SettingsEntryBuilder(entryName, owner)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
index 14b1629..14dc785 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsEntryRepository.kt
@@ -49,7 +49,7 @@
         entryMap = mutableMapOf()
         pageWithEntryMap = mutableMapOf()
 
-        val nullPage = SettingsPage.createNull()
+        val nullPage = NullPageProvider.createSettingsPage()
         val entryQueue = LinkedList<SettingsEntry>()
         for (page in sppRepository.getAllRootPages()) {
             val rootEntry =
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
index 94cfcc2..c810648 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPage.kt
@@ -19,12 +19,9 @@
 import android.os.Bundle
 import androidx.compose.runtime.Composable
 import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.util.genPageId
 import com.android.settingslib.spa.framework.util.isRuntimeParam
 import com.android.settingslib.spa.framework.util.navLink
-import com.android.settingslib.spa.framework.util.normalize
-import com.android.settingslib.spa.framework.util.normalizeArgList
-
-private const val NULL_PAGE_NAME = "NULL"
 
 /**
  * Defines data to identify a Settings page.
@@ -46,10 +43,7 @@
     val arguments: Bundle? = null,
 ) {
     companion object {
-        fun createNull(): SettingsPage {
-            return create(NULL_PAGE_NAME)
-        }
-
+        // TODO: cleanup it once all its usage in Settings are switched to Spp.createSettingsPage
         fun create(
             name: String,
             displayName: String? = null,
@@ -57,23 +51,13 @@
             arguments: Bundle? = null
         ): SettingsPage {
             return SettingsPage(
-                id = id(name, parameter, arguments),
+                id = genPageId(name, parameter, arguments),
                 sppName = name,
                 displayName = displayName ?: name,
                 parameter = parameter,
                 arguments = arguments
             )
         }
-
-        // The unique id of this page, which is computed by name + normalized(arguments)
-        private fun id(
-            name: String,
-            parameter: List<NamedNavArgument> = emptyList(),
-            arguments: Bundle? = null
-        ): String {
-            val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
-            return "$name:${normArguments?.toString()}".toHashId()
-        }
     }
 
     // Returns if this Settings Page is created by the given Spp.
@@ -85,43 +69,20 @@
         return sppName + parameter.navLink(arguments)
     }
 
-    fun hasRuntimeParam(): Boolean {
-        for (navArg in parameter) {
-            if (navArg.isRuntimeParam()) return true
-        }
-        return false
-    }
-
     fun isBrowsable(): Boolean {
-        return !isCreateBy(NULL_PAGE_NAME) && !hasRuntimeParam()
-    }
-
-    private fun getProvider(): SettingsPageProvider? {
-        if (!SpaEnvironmentFactory.isReady()) return null
-        val pageProviderRepository by SpaEnvironmentFactory.instance.pageProviderRepository
-        return pageProviderRepository.getProviderOrNull(sppName)
+        if (sppName == NullPageProvider.name) return false
+        for (navArg in parameter) {
+            if (navArg.isRuntimeParam()) return false
+        }
+        return true
     }
 
     fun isEnabled(): Boolean {
-        return getProvider()?.isEnabled(arguments) ?: false
+        return getPageProvider(sppName)?.isEnabled(arguments) ?: false
     }
 
     @Composable
     fun UiLayout() {
-        getProvider()?.Page(arguments)
+        getPageProvider(sppName)?.Page(arguments)
     }
 }
-
-fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
-    return SettingsPage.create(
-        name = name,
-        displayName = displayName + parameter.normalizeArgList(arguments, eraseRuntimeValues = true)
-            .joinToString("") { arg -> "/$arg" },
-        parameter = parameter,
-        arguments = arguments,
-    )
-}
-
-fun String.toHashId(): String {
-    return this.hashCode().toUInt().toString(36)
-}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
index c564130..18f964e 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SettingsPageProvider.kt
@@ -20,8 +20,12 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.util.genPageId
+import com.android.settingslib.spa.framework.util.normalizeArgList
 import com.android.settingslib.spa.widget.scaffold.RegularScaffold
 
+private const val NULL_PAGE_NAME = "NULL"
+
 /**
  * An SettingsPageProvider which is used to create Settings page instances.
  */
@@ -62,3 +66,24 @@
         }
     }
 }
+
+fun SettingsPageProvider.createSettingsPage(arguments: Bundle? = null): SettingsPage {
+    return SettingsPage(
+        id = genPageId(name, parameter, arguments),
+        sppName = name,
+        displayName = displayName + parameter.normalizeArgList(arguments, eraseRuntimeValues = true)
+            .joinToString("") { arg -> "/$arg" },
+        parameter = parameter,
+        arguments = arguments,
+    )
+}
+
+object NullPageProvider : SettingsPageProvider {
+    override val name = NULL_PAGE_NAME
+}
+
+fun getPageProvider(sppName: String): SettingsPageProvider? {
+    if (!SpaEnvironmentFactory.isReady()) return null
+    val pageProviderRepository by SpaEnvironmentFactory.instance.pageProviderRepository
+    return pageProviderRepository.getProviderOrNull(sppName)
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/UniqueId.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/UniqueId.kt
new file mode 100644
index 0000000..3b0ff7d
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/UniqueId.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.util
+
+import android.os.Bundle
+import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.common.SettingsPage
+
+fun genPageId(
+    sppName: String,
+    parameter: List<NamedNavArgument> = emptyList(),
+    arguments: Bundle? = null
+): String {
+    val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
+    return "$sppName:${normArguments?.toString()}".toHashId()
+}
+
+fun genEntryId(
+    name: String,
+    owner: SettingsPage,
+    fromPage: SettingsPage? = null,
+    toPage: SettingsPage? = null
+): String {
+    return "$name:${owner.id}(${fromPage?.id}-${toPage?.id})".toHashId()
+}
+
+// TODO: implement a better hash function
+private fun String.toHashId(): String {
+    return this.hashCode().toUInt().toString(36)
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt
index c0b7464..730aa8f 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryRepositoryTest.kt
@@ -19,12 +19,12 @@
 import android.content.Context
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.util.genEntryId
+import com.android.settingslib.spa.framework.util.genPageId
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
 import com.android.settingslib.spa.tests.testutils.SppHome
 import com.android.settingslib.spa.tests.testutils.SppLayer1
 import com.android.settingslib.spa.tests.testutils.SppLayer2
-import com.android.settingslib.spa.tests.testutils.getUniqueEntryId
-import com.android.settingslib.spa.tests.testutils.getUniquePageId
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -41,18 +41,18 @@
         val pageWithEntry = entryRepository.getAllPageWithEntry()
         assertThat(pageWithEntry.size).isEqualTo(3)
         assertThat(
-            entryRepository.getPageWithEntry(getUniquePageId("SppHome"))
+            entryRepository.getPageWithEntry(genPageId("SppHome"))
                 ?.entries?.size
         ).isEqualTo(1)
         assertThat(
-            entryRepository.getPageWithEntry(getUniquePageId("SppLayer1"))
+            entryRepository.getPageWithEntry(genPageId("SppLayer1"))
                 ?.entries?.size
         ).isEqualTo(3)
         assertThat(
-            entryRepository.getPageWithEntry(getUniquePageId("SppLayer2"))
+            entryRepository.getPageWithEntry(genPageId("SppLayer2"))
                 ?.entries?.size
         ).isEqualTo(2)
-        assertThat(entryRepository.getPageWithEntry(getUniquePageId("SppWithParam"))).isNull()
+        assertThat(entryRepository.getPageWithEntry(genPageId("SppWithParam"))).isNull()
     }
 
     @Test
@@ -61,17 +61,17 @@
         assertThat(entry.size).isEqualTo(7)
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId(
+                genEntryId(
                     "ROOT",
                     SppHome.createSettingsPage(),
-                    SettingsPage.createNull(),
+                    NullPageProvider.createSettingsPage(),
                     SppHome.createSettingsPage(),
                 )
             )
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId(
+                genEntryId(
                     "INJECT",
                     SppLayer1.createSettingsPage(),
                     SppHome.createSettingsPage(),
@@ -81,7 +81,7 @@
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId(
+                genEntryId(
                     "INJECT",
                     SppLayer2.createSettingsPage(),
                     SppLayer1.createSettingsPage(),
@@ -91,22 +91,22 @@
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId("Layer1Entry1", SppLayer1.createSettingsPage())
+                genEntryId("Layer1Entry1", SppLayer1.createSettingsPage())
             )
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId("Layer1Entry2", SppLayer1.createSettingsPage())
+                genEntryId("Layer1Entry2", SppLayer1.createSettingsPage())
             )
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
+                genEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
             )
         ).isNotNull()
         assertThat(
             entryRepository.getEntry(
-                getUniqueEntryId("Layer2Entry2", SppLayer2.createSettingsPage())
+                genEntryId("Layer2Entry2", SppLayer2.createSettingsPage())
             )
         ).isNotNull()
     }
@@ -115,21 +115,21 @@
     fun testGetEntryPath() {
         assertThat(
             entryRepository.getEntryPathWithDisplayName(
-                getUniqueEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
+                genEntryId("Layer2Entry1", SppLayer2.createSettingsPage())
             )
         ).containsExactly("Layer2Entry1", "INJECT_SppLayer2", "INJECT_SppLayer1", "ROOT_SppHome")
             .inOrder()
 
         assertThat(
             entryRepository.getEntryPathWithTitle(
-                getUniqueEntryId("Layer2Entry2", SppLayer2.createSettingsPage()),
+                genEntryId("Layer2Entry2", SppLayer2.createSettingsPage()),
                 "entryTitle"
             )
         ).containsExactly("entryTitle", "SppLayer2", "TitleLayer1", "TitleHome").inOrder()
 
         assertThat(
             entryRepository.getEntryPathWithDisplayName(
-                getUniqueEntryId(
+                genEntryId(
                     "INJECT",
                     SppLayer1.createSettingsPage(),
                     SppHome.createSettingsPage(),
@@ -140,7 +140,7 @@
 
         assertThat(
             entryRepository.getEntryPathWithTitle(
-                getUniqueEntryId(
+                genEntryId(
                     "INJECT",
                     SppLayer2.createSettingsPage(),
                     SppLayer1.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
index 6de1ae5..5754c9b 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsEntryTest.kt
@@ -23,11 +23,12 @@
 import androidx.core.os.bundleOf
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.util.genEntryId
+import com.android.settingslib.spa.framework.util.genPageId
 import com.android.settingslib.spa.slice.appendSpaParams
 import com.android.settingslib.spa.slice.getEntryId
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
-import com.android.settingslib.spa.tests.testutils.getUniqueEntryId
-import com.android.settingslib.spa.tests.testutils.getUniquePageId
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
@@ -64,9 +65,9 @@
 
     @Test
     fun testBuildBasic() {
-        val owner = SettingsPage.create("mySpp")
+        val owner = createSettingsPage("mySpp")
         val entry = SettingsEntryBuilder.create(owner, "myEntry").build()
-        assertThat(entry.id).isEqualTo(getUniqueEntryId("myEntry", owner))
+        assertThat(entry.id).isEqualTo(genEntryId("myEntry", owner))
         assertThat(entry.displayName).isEqualTo("myEntry")
         assertThat(entry.owner.sppName).isEqualTo("mySpp")
         assertThat(entry.owner.displayName).isEqualTo("mySpp")
@@ -80,19 +81,19 @@
 
     @Test
     fun testBuildWithLink() {
-        val owner = SettingsPage.create("mySpp")
-        val fromPage = SettingsPage.create("fromSpp")
-        val toPage = SettingsPage.create("toSpp")
+        val owner = createSettingsPage("mySpp")
+        val fromPage = createSettingsPage("fromSpp")
+        val toPage = createSettingsPage("toSpp")
         val entryFrom =
             SettingsEntryBuilder.createLinkFrom("myEntry", owner).setLink(toPage = toPage).build()
-        assertThat(entryFrom.id).isEqualTo(getUniqueEntryId("myEntry", owner, owner, toPage))
+        assertThat(entryFrom.id).isEqualTo(genEntryId("myEntry", owner, owner, toPage))
         assertThat(entryFrom.displayName).isEqualTo("myEntry")
         assertThat(entryFrom.fromPage!!.sppName).isEqualTo("mySpp")
         assertThat(entryFrom.toPage!!.sppName).isEqualTo("toSpp")
 
         val entryTo =
             SettingsEntryBuilder.createLinkTo("myEntry", owner).setLink(fromPage = fromPage).build()
-        assertThat(entryTo.id).isEqualTo(getUniqueEntryId("myEntry", owner, fromPage, owner))
+        assertThat(entryTo.id).isEqualTo(genEntryId("myEntry", owner, fromPage, owner))
         assertThat(entryTo.displayName).isEqualTo("myEntry")
         assertThat(entryTo.fromPage!!.sppName).isEqualTo("fromSpp")
         assertThat(entryTo.toPage!!.sppName).isEqualTo("mySpp")
@@ -100,10 +101,10 @@
 
     @Test
     fun testBuildInject() {
-        val owner = SettingsPage.create("mySpp")
+        val owner = createSettingsPage("mySpp")
         val entryInject = SettingsEntryBuilder.createInject(owner).build()
         assertThat(entryInject.id).isEqualTo(
-            getUniqueEntryId(
+            genEntryId(
                 INJECT_ENTRY_NAME_TEST, owner, toPage = owner
             )
         )
@@ -114,10 +115,10 @@
 
     @Test
     fun testBuildRoot() {
-        val owner = SettingsPage.create("mySpp")
+        val owner = createSettingsPage("mySpp")
         val entryInject = SettingsEntryBuilder.createRoot(owner, "myRootEntry").build()
         assertThat(entryInject.id).isEqualTo(
-            getUniqueEntryId(
+            genEntryId(
                 ROOT_ENTRY_NAME_TEST, owner, toPage = owner
             )
         )
@@ -129,7 +130,7 @@
     @Test
     fun testSetAttributes() {
         SpaEnvironmentFactory.reset(spaEnvironment)
-        val owner = SettingsPage.create("SppHome")
+        val owner = createSettingsPage("SppHome")
         val entryBuilder =
             SettingsEntryBuilder.create(owner, "myEntry")
                 .setDisplayName("myEntryDisplay")
@@ -138,7 +139,7 @@
                 .setSearchDataFn { null }
                 .setSliceDataFn { _, _ -> null }
         val entry = entryBuilder.build()
-        assertThat(entry.id).isEqualTo(getUniqueEntryId("myEntry", owner))
+        assertThat(entry.id).isEqualTo(genEntryId("myEntry", owner))
         assertThat(entry.displayName).isEqualTo("myEntryDisplay")
         assertThat(entry.fromPage).isNull()
         assertThat(entry.toPage).isNull()
@@ -148,7 +149,7 @@
         assertThat(entry.hasSliceSupport).isTrue()
 
         // Test disabled Spp
-        val ownerDisabled = SettingsPage.create("SppDisabled")
+        val ownerDisabled = createSettingsPage("SppDisabled")
         val entryBuilderDisabled =
             SettingsEntryBuilder.create(ownerDisabled, "myEntry")
                 .setDisplayName("myEntryDisplay")
@@ -157,7 +158,7 @@
                 .setSearchDataFn { null }
                 .setSliceDataFn { _, _ -> null }
         val entryDisabled = entryBuilderDisabled.build()
-        assertThat(entryDisabled.id).isEqualTo(getUniqueEntryId("myEntry", ownerDisabled))
+        assertThat(entryDisabled.id).isEqualTo(genEntryId("myEntry", ownerDisabled))
         assertThat(entryDisabled.displayName).isEqualTo("myEntryDisplay")
         assertThat(entryDisabled.fromPage).isNull()
         assertThat(entryDisabled.toPage).isNull()
@@ -173,7 +174,7 @@
         // Clear SppHome in spa environment
         SpaEnvironmentFactory.reset()
         val entry3 = entryBuilder.build()
-        assertThat(entry3.id).isEqualTo(getUniqueEntryId("myEntry", owner))
+        assertThat(entry3.id).isEqualTo(genEntryId("myEntry", owner))
         assertThat(entry3.displayName).isEqualTo("myEntryDisplay")
         assertThat(entry3.fromPage).isNull()
         assertThat(entry3.toPage).isNull()
@@ -186,12 +187,12 @@
     @Test
     fun testSetMarco() {
         SpaEnvironmentFactory.reset(spaEnvironment)
-        val owner = SettingsPage.create("SppHome", arguments = bundleOf("param" to "v1"))
+        val owner = createSettingsPage("SppHome", arguments = bundleOf("param" to "v1"))
         val entry = SettingsEntryBuilder.create(owner, "myEntry").setMacro {
             assertThat(it?.getString("param")).isEqualTo("v1")
             assertThat(it?.getString("rtParam")).isEqualTo("v2")
             assertThat(it?.getString("unknown")).isNull()
-            MacroForTest(getUniquePageId("SppHome"), getUniqueEntryId("myEntry", owner))
+            MacroForTest(genPageId("SppHome"), genEntryId("myEntry", owner))
         }.build()
 
         val rtArguments = bundleOf("rtParam" to "v2")
@@ -211,8 +212,8 @@
     @Test
     fun testSetSliceDataFn() {
         SpaEnvironmentFactory.reset(spaEnvironment)
-        val owner = SettingsPage.create("SppHome")
-        val entryId = getUniqueEntryId("myEntry", owner)
+        val owner = createSettingsPage("SppHome")
+        val entryId = genEntryId("myEntry", owner)
         val emptySliceData = EntrySliceData()
 
         val entryBuilder = SettingsEntryBuilder.create(owner, "myEntry").setSliceDataFn { uri, _ ->
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt
index 6c0c652..8576573 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageProviderRepositoryTest.kt
@@ -17,6 +17,7 @@
 package com.android.settingslib.spa.framework.common
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -29,13 +30,14 @@
         assertThat(sppRepoEmpty.getDefaultStartPage()).isEqualTo("")
         assertThat(sppRepoEmpty.getAllRootPages()).isEmpty()
 
+        val nullPage = NullPageProvider.createSettingsPage()
         val sppRepoNull =
-            SettingsPageProviderRepository(emptyList(), listOf(SettingsPage.createNull()))
+            SettingsPageProviderRepository(emptyList(), listOf(nullPage))
         assertThat(sppRepoNull.getDefaultStartPage()).isEqualTo("NULL")
-        assertThat(sppRepoNull.getAllRootPages()).contains(SettingsPage.createNull())
+        assertThat(sppRepoNull.getAllRootPages()).contains(nullPage)
 
-        val rootPage1 = SettingsPage.create(name = "Spp1", displayName = "Spp1")
-        val rootPage2 = SettingsPage.create(name = "Spp2", displayName = "Spp2")
+        val rootPage1 = createSettingsPage(sppName = "Spp1", displayName = "Spp1")
+        val rootPage2 = createSettingsPage(sppName = "Spp2", displayName = "Spp2")
         val sppRepo = SettingsPageProviderRepository(emptyList(), listOf(rootPage1, rootPage2))
         val allRoots = sppRepo.getAllRootPages()
         assertThat(sppRepo.getDefaultStartPage()).isEqualTo("Spp1")
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
index 7fa1e26..dc74a10 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SettingsPageTest.kt
@@ -22,8 +22,9 @@
 import androidx.navigation.navArgument
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.util.genPageId
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
-import com.android.settingslib.spa.tests.testutils.getUniquePageId
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -35,27 +36,25 @@
 
     @Test
     fun testNullPage() {
-        val page = SettingsPage.createNull()
-        assertThat(page.id).isEqualTo(getUniquePageId("NULL"))
+        val page = NullPageProvider.createSettingsPage()
+        assertThat(page.id).isEqualTo(genPageId("NULL"))
         assertThat(page.sppName).isEqualTo("NULL")
         assertThat(page.displayName).isEqualTo("NULL")
         assertThat(page.buildRoute()).isEqualTo("NULL")
         assertThat(page.isCreateBy("NULL")).isTrue()
         assertThat(page.isCreateBy("Spp")).isFalse()
-        assertThat(page.hasRuntimeParam()).isFalse()
         assertThat(page.isBrowsable()).isFalse()
     }
 
     @Test
     fun testRegularPage() {
-        val page = SettingsPage.create("mySpp", "SppDisplayName")
-        assertThat(page.id).isEqualTo(getUniquePageId("mySpp"))
+        val page = createSettingsPage("mySpp", "SppDisplayName")
+        assertThat(page.id).isEqualTo(genPageId("mySpp"))
         assertThat(page.sppName).isEqualTo("mySpp")
         assertThat(page.displayName).isEqualTo("SppDisplayName")
         assertThat(page.buildRoute()).isEqualTo("mySpp")
         assertThat(page.isCreateBy("NULL")).isFalse()
         assertThat(page.isCreateBy("mySpp")).isTrue()
-        assertThat(page.hasRuntimeParam()).isFalse()
         assertThat(page.isBrowsable()).isTrue()
     }
 
@@ -67,7 +66,7 @@
         )
         val page = spaEnvironment.createPage("SppWithParam", arguments)
         assertThat(page.id).isEqualTo(
-            getUniquePageId(
+            genPageId(
                 "SppWithParam", listOf(
                     navArgument("string_param") { type = NavType.StringType },
                     navArgument("int_param") { type = NavType.IntType },
@@ -78,7 +77,6 @@
         assertThat(page.displayName).isEqualTo("SppWithParam/myStr/10")
         assertThat(page.buildRoute()).isEqualTo("SppWithParam/myStr/10")
         assertThat(page.isCreateBy("SppWithParam")).isTrue()
-        assertThat(page.hasRuntimeParam()).isFalse()
         assertThat(page.isBrowsable()).isTrue()
     }
 
@@ -91,7 +89,7 @@
         )
         val page = spaEnvironment.createPage("SppWithRtParam", arguments)
         assertThat(page.id).isEqualTo(
-            getUniquePageId(
+            genPageId(
                 "SppWithRtParam", listOf(
                     navArgument("string_param") { type = NavType.StringType },
                     navArgument("int_param") { type = NavType.IntType },
@@ -103,7 +101,6 @@
         assertThat(page.displayName).isEqualTo("SppWithRtParam/myStr/10")
         assertThat(page.buildRoute()).isEqualTo("SppWithRtParam/myStr/10/rtStr")
         assertThat(page.isCreateBy("SppWithRtParam")).isTrue()
-        assertThat(page.hasRuntimeParam()).isTrue()
         assertThat(page.isBrowsable()).isFalse()
     }
 }
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt
index 1854728..f974cca 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/SpaIntentTest.kt
@@ -19,9 +19,10 @@
 import android.content.Context
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.common.NullPageProvider
 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
-import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
 import com.google.common.truth.Truth
 import org.junit.Before
@@ -40,7 +41,7 @@
 
     @Test
     fun testCreateIntent() {
-        val nullPage = SettingsPage.createNull()
+        val nullPage = NullPageProvider.createSettingsPage()
         Truth.assertThat(nullPage.createIntent()).isNull()
         Truth.assertThat(SettingsEntryBuilder.createInject(nullPage).build().createIntent())
             .isNull()
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/UniqueIdTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/UniqueIdTest.kt
new file mode 100644
index 0000000..c17f83b
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/util/UniqueIdTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.util
+
+import androidx.core.os.bundleOf
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.tests.testutils.createSettingsPage
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class UniqueIdTest {
+    @Test
+    fun testUniquePageId() {
+        Truth.assertThat(genPageId("mySpp")).isEqualTo("1byojwa")
+
+        val parameter = listOf(
+            navArgument("string_param") { type = NavType.StringType },
+            navArgument("int_param") { type = NavType.IntType },
+        )
+        val arguments = bundleOf(
+            "string_param" to "myStr",
+            "int_param" to 10,
+        )
+        Truth.assertThat(genPageId("mySppWithParam", parameter, arguments)).isEqualTo("1sz4pbq")
+
+        val parameter2 = listOf(
+            navArgument("string_param") { type = NavType.StringType },
+            navArgument("int_param") { type = NavType.IntType },
+            navArgument("rt_param") { type = NavType.StringType },
+        )
+        val arguments2 = bundleOf(
+            "string_param" to "myStr",
+            "int_param" to 10,
+            "rt_param" to "myRtStr",
+        )
+        Truth.assertThat(genPageId("mySppWithRtParam", parameter2, arguments2)).isEqualTo("ts6d8k")
+    }
+
+    @Test
+    fun testUniqueEntryId() {
+        val owner = createSettingsPage("mySpp")
+        val fromPage = createSettingsPage("fromSpp")
+        val toPage = createSettingsPage("toSpp")
+
+        Truth.assertThat(genEntryId("myEntry", owner)).isEqualTo("145pppn")
+        Truth.assertThat(genEntryId("myEntry", owner, fromPage = fromPage, toPage = toPage))
+            .isEqualTo("1m7jzew")
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
index 530d2ed..341a4a5 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
@@ -25,10 +25,10 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.util.genEntryId
 import com.android.settingslib.spa.tests.testutils.SpaEnvironmentForTest
 import com.android.settingslib.spa.tests.testutils.SppHome
 import com.android.settingslib.spa.tests.testutils.SppLayer2
-import com.android.settingslib.spa.tests.testutils.getUniqueEntryId
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
@@ -52,7 +52,7 @@
 
         // Slice supported
         val page = SppLayer2.createSettingsPage()
-        val entryId = getUniqueEntryId("Layer2Entry1", page)
+        val entryId = genEntryId("Layer2Entry1", page)
         val sliceUri = Uri.Builder().appendSpaParams(page.buildRoute(), entryId).build()
         assertThat(sliceUri.getDestination()).isEqualTo("SppLayer2")
         assertThat(sliceUri.getSliceId()).isEqualTo("${entryId}_Bundle[{}]")
@@ -61,7 +61,7 @@
         assertThat(sliceDataRepository.getOrBuildSliceData(sliceUri)).isSameInstanceAs(sliceData)
 
         // Slice unsupported
-        val entryId2 = getUniqueEntryId("Layer2Entry2", page)
+        val entryId2 = genEntryId("Layer2Entry2", page)
         val sliceUri2 = Uri.Builder().appendSpaParams(page.buildRoute(), entryId2).build()
         assertThat(sliceUri2.getDestination()).isEqualTo("SppLayer2")
         assertThat(sliceUri2.getSliceId()).isEqualTo("${entryId2}_Bundle[{}]")
@@ -73,7 +73,7 @@
         SpaEnvironmentFactory.reset(spaEnvironment)
 
         val page = SppLayer2.createSettingsPage()
-        val entryId = getUniqueEntryId("Layer2Entry1", page)
+        val entryId = genEntryId("Layer2Entry1", page)
         val sliceUri = Uri.Builder().appendSpaParams(page.buildRoute(), entryId).build()
 
         // build slice data first
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SettingsPageHelper.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SettingsPageHelper.kt
new file mode 100644
index 0000000..caf4136
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/SettingsPageHelper.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.tests.testutils
+
+import android.os.Bundle
+import androidx.navigation.NamedNavArgument
+import com.android.settingslib.spa.framework.common.SettingsPage
+import com.android.settingslib.spa.framework.util.genPageId
+
+fun createSettingsPage(
+    sppName: String,
+    displayName: String? = null,
+    parameter: List<NamedNavArgument> = emptyList(),
+    arguments: Bundle? = null
+): SettingsPage {
+    return SettingsPage(
+        id = genPageId(sppName, parameter, arguments),
+        sppName = sppName,
+        displayName = displayName ?: sppName,
+        parameter = parameter,
+        arguments = arguments
+    )
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt
deleted file mode 100644
index ce9b791..0000000
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/tests/testutils/UniqueIdHelper.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.spa.tests.testutils
-
-import android.os.Bundle
-import androidx.navigation.NamedNavArgument
-import com.android.settingslib.spa.framework.common.SettingsPage
-import com.android.settingslib.spa.framework.common.toHashId
-import com.android.settingslib.spa.framework.util.normalize
-
-fun getUniquePageId(
-    name: String,
-    parameter: List<NamedNavArgument> = emptyList(),
-    arguments: Bundle? = null
-): String {
-    val normArguments = parameter.normalize(arguments, eraseRuntimeValues = true)
-    return "$name:${normArguments?.toString()}".toHashId()
-}
-
-fun getUniquePageId(page: SettingsPage): String {
-    return getUniquePageId(page.sppName, page.parameter, page.arguments)
-}
-
-fun getUniqueEntryId(
-    name: String,
-    owner: SettingsPage,
-    fromPage: SettingsPage? = null,
-    toPage: SettingsPage? = null
-): String {
-    val ownerId = getUniquePageId(owner)
-    val fromId = if (fromPage == null) "null" else getUniquePageId(fromPage)
-    val toId = if (toPage == null) "null" else getUniquePageId(toPage)
-    return "$name:$ownerId($fromId-$toId)".toHashId()
-}
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index c155433..adc6a02 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1633,4 +1633,12 @@
     <string name="back_navigation_animation_summary">Enable system animations for predictive back.</string>
     <!-- Developer setting: enable animations when a back gesture is executed, full explanation[CHAR LIMIT=NONE] -->
     <string name="back_navigation_animation_dialog">This setting enables system animations for predictive gesture animation. It requires setting per-app "enableOnBackInvokedCallback" to true in the manifest file.</string>
+
+    <!-- [CHAR LIMIT=NONE] Messages shown when users press outside of udfps region during -->
+    <string-array name="udfps_accessibility_touch_hints">
+        <item>Move left</item>
+        <item>Move down</item>
+        <item>Move right</item>
+        <item>Move up</item>
+    </string-array>
 </resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/OWNERS b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/OWNERS
new file mode 100644
index 0000000..09e7991
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/OWNERS
@@ -0,0 +1,5 @@
+# Default reviewers for this and subdirectories.
+emilychuang@google.com
+ykhung@google.com
+
+# Emergency approvers in case the above are not available
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt
similarity index 97%
rename from packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
rename to packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt
index 7f3846c..d55a027 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayParams.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsOverlayParams.kt
@@ -1,4 +1,4 @@
-package com.android.systemui.biometrics
+package com.android.settingslib.udfps
 
 import android.graphics.Rect
 import android.view.Surface
diff --git a/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsUtils.java b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsUtils.java
new file mode 100644
index 0000000..dc8a862
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/udfps/UdfpsUtils.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.udfps;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.util.DisplayUtils;
+import android.util.Log;
+import android.util.RotationUtils;
+import android.view.Display;
+import android.view.DisplayInfo;
+import android.view.MotionEvent;
+import android.view.Surface;
+
+import com.android.settingslib.R;
+
+/** Utility class for working with udfps. */
+public class UdfpsUtils {
+    private static final String TAG = "UdfpsUtils";
+
+    /**
+     * Gets the scale factor representing the user's current resolution / the stable (default)
+     * resolution.
+     *
+     * @param displayInfo The display information.
+     */
+    public float getScaleFactor(DisplayInfo displayInfo) {
+        Display.Mode maxDisplayMode =
+                DisplayUtils.getMaximumResolutionDisplayMode(displayInfo.supportedModes);
+        float scaleFactor =
+                DisplayUtils.getPhysicalPixelDisplaySizeRatio(
+                        maxDisplayMode.getPhysicalWidth(),
+                        maxDisplayMode.getPhysicalHeight(),
+                        displayInfo.getNaturalWidth(),
+                        displayInfo.getNaturalHeight()
+                );
+        return (scaleFactor == Float.POSITIVE_INFINITY) ? 1f : scaleFactor;
+    }
+
+    /**
+     * Gets the touch in native coordinates. Map the touch to portrait mode if the device is in
+     * landscape mode.
+     *
+     * @param idx                The pointer identifier.
+     * @param event              The MotionEvent object containing full information about the event.
+     * @param udfpsOverlayParams The [UdfpsOverlayParams] used.
+     * @return The mapped touch event.
+     */
+    public Point getTouchInNativeCoordinates(int idx, MotionEvent event,
+            UdfpsOverlayParams udfpsOverlayParams) {
+        Point portraitTouch = new Point((int) event.getRawX(idx), (int) event.getRawY(idx));
+        int rot = udfpsOverlayParams.getRotation();
+        if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
+            RotationUtils.rotatePoint(
+                    portraitTouch,
+                    RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
+                    udfpsOverlayParams.getLogicalDisplayWidth(),
+                    udfpsOverlayParams.getLogicalDisplayHeight()
+            );
+        }
+
+        // Scale the coordinates to native resolution.
+        float scale = udfpsOverlayParams.getScaleFactor();
+        portraitTouch.x = (int) (portraitTouch.x / scale);
+        portraitTouch.y = (int) (portraitTouch.y / scale);
+        return portraitTouch;
+    }
+
+    /**
+     * This function computes the angle of touch relative to the sensor and maps the angle to a list
+     * of help messages which are announced if accessibility is enabled.
+     *
+     * @return Whether the announcing string is null
+     */
+    public String onTouchOutsideOfSensorArea(boolean touchExplorationEnabled,
+            Context context, int touchX, int touchY, UdfpsOverlayParams udfpsOverlayParams) {
+        if (!touchExplorationEnabled) {
+            return null;
+        }
+
+        String[] touchHints = context.getResources().getStringArray(
+                R.array.udfps_accessibility_touch_hints);
+        if (touchHints.length != 4) {
+            Log.e(TAG, "expected exactly 4 touch hints, got " + touchHints.length + "?");
+            return null;
+        }
+
+        // Scale the coordinates to native resolution.
+        float scale = udfpsOverlayParams.getScaleFactor();
+        float scaledSensorX = udfpsOverlayParams.getSensorBounds().centerX() / scale;
+        float scaledSensorY = udfpsOverlayParams.getSensorBounds().centerY() / scale;
+        String theStr =
+                onTouchOutsideOfSensorAreaImpl(
+                        touchHints,
+                        touchX,
+                        touchY,
+                        scaledSensorX,
+                        scaledSensorY,
+                        udfpsOverlayParams.getRotation()
+                );
+        Log.v(TAG, "Announcing touch outside : $theStr");
+        return theStr;
+    }
+
+    /**
+     * This function computes the angle of touch relative to the sensor and maps the angle to a list
+     * of help messages which are announced if accessibility is enabled.
+     *
+     * There are 4 quadrants of the circle (90 degree arcs)
+     *
+     * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left" [45, 135) ->
+     * touchHints[1] = "Move Fingerprint down" And so on.
+     */
+    private String onTouchOutsideOfSensorAreaImpl(String[] touchHints, float touchX,
+            float touchY, float sensorX, float sensorY, int rotation) {
+        float xRelativeToSensor = touchX - sensorX;
+        // Touch coordinates are with respect to the upper left corner, so reverse
+        // this calculation
+        float yRelativeToSensor = sensorY - touchY;
+        double angleInRad = Math.atan2(yRelativeToSensor, xRelativeToSensor);
+        // If the radians are negative, that means we are counting clockwise.
+        // So we need to add 360 degrees
+        if (angleInRad < 0.0) {
+            angleInRad += 2.0 * Math.PI;
+        }
+        // rad to deg conversion
+        double degrees = Math.toDegrees(angleInRad);
+        double degreesPerBucket = 360.0 / touchHints.length;
+        double halfBucketDegrees = degreesPerBucket / 2.0;
+        // The mapping should be as follows
+        // [315, 360] && [0, 45] -> 0
+        // [45, 135]             -> 1
+        int index = (int) ((degrees + halfBucketDegrees) % 360 / degreesPerBucket);
+        index %= touchHints.length;
+
+        // A rotation of 90 degrees corresponds to increasing the index by 1.
+        if (rotation == Surface.ROTATION_90) {
+            index = (index + 1) % touchHints.length;
+        }
+        if (rotation == Surface.ROTATION_270) {
+            index = (index + 3) % touchHints.length;
+        }
+        return touchHints[index];
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/udfps/UdfpsUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/udfps/UdfpsUtilsTest.java
new file mode 100644
index 0000000..f4f0ef9
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/udfps/UdfpsUtilsTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.udfps;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.Surface;
+
+import com.android.settingslib.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+
+@RunWith(RobolectricTestRunner.class)
+public class UdfpsUtilsTest {
+    @Rule
+    public final MockitoRule rule = MockitoJUnit.rule();
+
+    private Context mContext;
+    private String[] mTouchHints;
+    private UdfpsUtils mUdfpsUtils;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+        mTouchHints = mContext.getResources().getStringArray(
+                R.array.udfps_accessibility_touch_hints);
+        mUdfpsUtils = new UdfpsUtils();
+    }
+
+    @Test
+    public void testTouchOutsideAreaNoRotation() {
+        int rotation = Surface.ROTATION_0;
+        // touch at 0 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        0 /* touchX */, 0/* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[0]);
+        // touch at 90 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        0 /* touchX */, -1/* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[1]);
+        // touch at 180 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        -1 /* touchX */, 0/* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[2]);
+        // touch at 270 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        0 /* touchX */, 1/* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[3]);
+    }
+
+
+    @Test
+    public void testTouchOutsideAreaNoRotation90Degrees() {
+        int rotation = Surface.ROTATION_90;
+        // touch at 0 degrees -> 90 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        0 /* touchX */, 0 /* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[1]);
+        // touch at 90 degrees -> 180 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        0 /* touchX */, -1 /* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[2]);
+        // touch at 180 degrees -> 270 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        -1 /* touchX */, 0 /* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[3]);
+        // touch at 270 degrees -> 0 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        0 /* touchX */, 1/* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[0]);
+    }
+
+
+    @Test
+    public void testTouchOutsideAreaNoRotation270Degrees() {
+        int rotation = Surface.ROTATION_270;
+        // touch at 0 degrees -> 270 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        0 /* touchX */, 0/* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[3]);
+        // touch at 90 degrees -> 0 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        0 /* touchX */, -1/* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[0]);
+        // touch at 180 degrees -> 90 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        -1 /* touchX */, 0/* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[1]);
+        // touch at 270 degrees -> 180 degrees
+        assertThat(
+                mUdfpsUtils.onTouchOutsideOfSensorArea(true, mContext,
+                        0 /* touchX */, 1/* touchY */,
+                        new UdfpsOverlayParams(new Rect(), new Rect(), 0, 0, 1f, rotation)
+                )
+        ).isEqualTo(mTouchHints[2]);
+    }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
index c764d53..74bc910 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -87,7 +87,7 @@
                     * (1.-sparkleRing) * in_fadeSparkle;
 
                 float rippleInsideAlpha = (1.-inside) * in_fadeFill;
-                float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing * in_sparkle_strength;
+                float rippleRingAlpha = (1.-sparkleRing) * in_fadeRing;
                 float rippleAlpha = max(rippleInsideAlpha, rippleRingAlpha) * in_color.a;
                 vec4 ripple = vec4(in_color.rgb, 1.0) * rippleAlpha;
                 return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength);
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index bc4796a..3c9328c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -117,15 +117,6 @@
         rippleShader.color = ColorUtils.setAlphaComponent(color, alpha)
     }
 
-    /**
-     * Set whether the ripple should remain filled as the ripple expands.
-     *
-     * See [RippleShader.rippleFill].
-     */
-    fun setRippleFill(rippleFill: Boolean) {
-        rippleShader.rippleFill = rippleFill
-    }
-
     /** Set the intensity of the sparkles. */
     fun setSparkleStrength(strength: Float) {
         rippleShader.sparkleStrength = strength
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index bbfb6fb..4ef525a 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -20,6 +20,7 @@
 import com.android.internal.annotations.Keep
 import com.android.systemui.plugins.annotations.ProvidesInterface
 import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.statusbar.Weather
 import java.io.PrintWriter
 import java.util.Locale
 import java.util.TimeZone
@@ -111,6 +112,9 @@
 
     /** Call whenever the color palette should update */
     fun onColorPaletteChanged(resources: Resources) {}
+
+    /** Call whenever the weather data should update */
+    fun onWeatherDataChanged(data: Weather) {}
 }
 
 /** Methods which trigger various clock animations */
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
index 64ece47..ca4028a 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_pin_view.xml
@@ -105,6 +105,7 @@
             android:id="@+id/key1"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key2"
             androidprv:digit="1"
             androidprv:textView="@+id/pinEntry" />
 
@@ -112,6 +113,7 @@
             android:id="@+id/key2"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key3"
             androidprv:digit="2"
             androidprv:textView="@+id/pinEntry" />
 
@@ -119,6 +121,7 @@
             android:id="@+id/key3"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key4"
             androidprv:digit="3"
             androidprv:textView="@+id/pinEntry" />
 
@@ -126,6 +129,7 @@
             android:id="@+id/key4"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key5"
             androidprv:digit="4"
             androidprv:textView="@+id/pinEntry" />
 
@@ -133,6 +137,7 @@
             android:id="@+id/key5"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key6"
             androidprv:digit="5"
             androidprv:textView="@+id/pinEntry" />
 
@@ -140,6 +145,7 @@
             android:id="@+id/key6"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key7"
             androidprv:digit="6"
             androidprv:textView="@+id/pinEntry" />
 
@@ -147,13 +153,16 @@
             android:id="@+id/key7"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key8"
             androidprv:digit="7"
             androidprv:textView="@+id/pinEntry" />
 
+
         <com.android.keyguard.NumPadKey
             android:id="@+id/key8"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key9"
             androidprv:digit="8"
             androidprv:textView="@+id/pinEntry" />
 
@@ -161,34 +170,33 @@
             android:id="@+id/key9"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/delete_button"
             androidprv:digit="9"
             androidprv:textView="@+id/pinEntry" />
 
-
         <com.android.keyguard.NumPadButton
             android:id="@+id/delete_button"
+            style="@style/NumPadKey.Delete"
             android:layout_width="0dp"
             android:layout_height="0dp"
-            style="@style/NumPadKey.Delete"
-            android:contentDescription="@string/keyboardview_keycode_delete"
-            />
+            android:accessibilityTraversalBefore="@id/key0"
+            android:contentDescription="@string/keyboardview_keycode_delete" />
 
         <com.android.keyguard.NumPadKey
             android:id="@+id/key0"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:accessibilityTraversalBefore="@id/key_enter"
             androidprv:digit="0"
             androidprv:textView="@+id/pinEntry" />
 
         <com.android.keyguard.NumPadButton
             android:id="@+id/key_enter"
+            style="@style/NumPadKey.Enter"
             android:layout_width="0dp"
             android:layout_height="0dp"
-            style="@style/NumPadKey.Enter"
-            android:contentDescription="@string/keyboardview_keycode_enter"
-            />
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
+            android:contentDescription="@string/keyboardview_keycode_enter" />
+</androidx.constraintlayout.widget.ConstraintLayout>
 
     <include layout="@layout/keyguard_eca"
              android:id="@+id/keyguard_selector_fade_container"
diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml
index ded6f93..aa211bf 100644
--- a/packages/SystemUI/res/layout/controls_with_favorites.xml
+++ b/packages/SystemUI/res/layout/controls_with_favorites.xml
@@ -20,7 +20,6 @@
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:orientation="horizontal"
-      android:layout_marginTop="@dimen/controls_top_margin"
       android:layout_marginBottom="@dimen/controls_header_bottom_margin">
 
     <!-- make sure the header stays centered in the layout by adding a spacer -->
@@ -78,6 +77,7 @@
         android:layout_weight="1"
         android:orientation="vertical"
         android:clipChildren="true"
+        android:paddingHorizontal="16dp"
         android:scrollbars="none">
     <include layout="@layout/global_actions_controls_list_view" />
 
@@ -88,8 +88,6 @@
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:layout_weight="1"
-      android:layout_marginLeft="@dimen/global_actions_side_margin"
-      android:layout_marginRight="@dimen/global_actions_side_margin"
       android:background="@drawable/controls_panel_background"
       android:visibility="gone"
       />
diff --git a/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml b/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml
index de43a5e..4799f8c 100644
--- a/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml
+++ b/packages/SystemUI/res/layout/udfps_fpm_empty_view.xml
@@ -19,4 +19,12 @@
     android:id="@+id/udfps_animation_view"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
+    <!-- The layout height/width are placeholders, which will be overwritten by
+     FingerprintSensorPropertiesInternal. -->
+    <View
+        android:id="@+id/udfps_enroll_accessibility_view"
+        android:layout_gravity="center"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:contentDescription="@string/accessibility_fingerprint_label"/>
 </com.android.systemui.biometrics.UdfpsFpmEmptyView>
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index fff2544..ac81dcc 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -64,4 +64,6 @@
 
     <dimen name="qs_panel_padding_top">@dimen/qqs_layout_margin_top</dimen>
     <dimen name="qs_panel_padding_top_combined_headers">@dimen/qs_panel_padding_top</dimen>
+
+    <dimen name="controls_padding_horizontal">16dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index db7fb48..4f24d83 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -51,9 +51,6 @@
     <!-- Text size for user name in user switcher -->
     <dimen name="kg_user_switcher_text_size">18sp</dimen>
 
-    <dimen name="controls_header_bottom_margin">12dp</dimen>
-    <dimen name="controls_top_margin">24dp</dimen>
-
     <dimen name="global_actions_grid_item_layout_height">80dp</dimen>
 
     <dimen name="qs_brightness_margin_bottom">16dp</dimen>
diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
index 122806a..2b88e55 100644
--- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
@@ -17,7 +17,6 @@
 */
 -->
 <resources>
-    <dimen name="controls_padding_horizontal">205dp</dimen>
     <dimen name="split_shade_notifications_scrim_margin_bottom">24dp</dimen>
     <dimen name="notification_panel_margin_bottom">64dp</dimen>
 
diff --git a/packages/SystemUI/res/values-sw720dp/dimens.xml b/packages/SystemUI/res/values-sw720dp/dimens.xml
index 927059a..8f59df6 100644
--- a/packages/SystemUI/res/values-sw720dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp/dimens.xml
@@ -19,7 +19,7 @@
     <!-- gap on either side of status bar notification icons -->
     <dimen name="status_bar_icon_padding">1dp</dimen>
 
-    <dimen name="controls_padding_horizontal">75dp</dimen>
+    <dimen name="controls_padding_horizontal">40dp</dimen>
 
     <dimen name="large_screen_shade_header_height">56dp</dimen>
 
diff --git a/packages/SystemUI/res/values-television/strings.xml b/packages/SystemUI/res/values-television/strings.xml
deleted file mode 100644
index 86106e6..0000000
--- a/packages/SystemUI/res/values-television/strings.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/**
- * Copyright (c) 2022, The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
--->
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- Learn more URL for the log access confirmation dialog. [DO NOT TRANSLATE]-->
-    <string name="log_access_confirmation_learn_more_url" translatable="false"></string>
-</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index ebf232f..2df2513 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -43,12 +43,18 @@
     <dimen name="navigation_edge_panel_height">268dp</dimen>
     <!-- The threshold to drag to trigger the edge action -->
     <dimen name="navigation_edge_action_drag_threshold">16dp</dimen>
+    <!-- The drag distance to consider evaluating gesture -->
+    <dimen name="navigation_edge_action_min_distance_to_start_animation">24dp</dimen>
     <!-- The threshold to progress back animation for edge swipe -->
     <dimen name="navigation_edge_action_progress_threshold">412dp</dimen>
     <!-- The minimum display position of the arrow on the screen -->
     <dimen name="navigation_edge_arrow_min_y">64dp</dimen>
     <!-- The amount by which the arrow is shifted to avoid the finger-->
     <dimen name="navigation_edge_finger_offset">64dp</dimen>
+    <!-- The threshold to dynamically activate the edge action -->
+    <dimen name="navigation_edge_action_reactivation_drag_threshold">32dp</dimen>
+    <!-- The threshold to dynamically deactivate the edge action -->
+    <dimen name="navigation_edge_action_deactivation_drag_threshold">32dp</dimen>
 
     <!-- The thickness of the arrow -->
     <dimen name="navigation_edge_arrow_thickness">4dp</dimen>
@@ -56,37 +62,61 @@
     <dimen name="navigation_edge_minimum_x_delta_for_switch">32dp</dimen>
 
     <!-- entry state -->
+    <item name="navigation_edge_entry_scale" format="float" type="dimen">0.98</item>
     <dimen name="navigation_edge_entry_margin">4dp</dimen>
-    <dimen name="navigation_edge_entry_background_width">8dp</dimen>
-    <dimen name="navigation_edge_entry_background_height">60dp</dimen>
-    <dimen name="navigation_edge_entry_edge_corners">30dp</dimen>
-    <dimen name="navigation_edge_entry_far_corners">30dp</dimen>
-    <dimen name="navigation_edge_entry_arrow_length">10dp</dimen>
-    <dimen name="navigation_edge_entry_arrow_height">7dp</dimen>
+    <item name="navigation_edge_entry_background_alpha" format="float" type="dimen">1.0</item>
+    <dimen name="navigation_edge_entry_background_width">0dp</dimen>
+    <dimen name="navigation_edge_entry_background_height">48dp</dimen>
+    <dimen name="navigation_edge_entry_edge_corners">6dp</dimen>
+    <dimen name="navigation_edge_entry_far_corners">6dp</dimen>
+    <item name="navigation_edge_entry_arrow_alpha" format="float" type="dimen">0.0</item>
+    <dimen name="navigation_edge_entry_arrow_length">8.6dp</dimen>
+    <dimen name="navigation_edge_entry_arrow_height">5dp</dimen>
 
     <!-- pre-threshold -->
     <dimen name="navigation_edge_pre_threshold_margin">4dp</dimen>
-    <dimen name="navigation_edge_pre_threshold_background_width">64dp</dimen>
-    <dimen name="navigation_edge_pre_threshold_background_height">60dp</dimen>
-    <dimen name="navigation_edge_pre_threshold_edge_corners">22dp</dimen>
-    <dimen name="navigation_edge_pre_threshold_far_corners">26dp</dimen>
+    <item name="navigation_edge_pre_threshold_background_alpha" format="float" type="dimen">1.0
+    </item>
+    <item name="navigation_edge_pre_threshold_scale" format="float" type="dimen">0.98</item>
+    <dimen name="navigation_edge_pre_threshold_background_width">51dp</dimen>
+    <dimen name="navigation_edge_pre_threshold_background_height">46dp</dimen>
+    <dimen name="navigation_edge_pre_threshold_edge_corners">16dp</dimen>
+    <dimen name="navigation_edge_pre_threshold_far_corners">20dp</dimen>
+    <item name="navigation_edge_pre_threshold_arrow_alpha" format="float" type="dimen">1.0</item>
+    <dimen name="navigation_edge_pre_threshold_arrow_length">8dp</dimen>
+    <dimen name="navigation_edge_pre_threshold_arrow_height">5.6dp</dimen>
 
-    <!-- post-threshold / active -->
+    <!-- active (post-threshold) -->
+    <item name="navigation_edge_active_scale" format="float" type="dimen">1.0</item>
     <dimen name="navigation_edge_active_margin">14dp</dimen>
-    <dimen name="navigation_edge_active_background_width">60dp</dimen>
-    <dimen name="navigation_edge_active_background_height">60dp</dimen>
-    <dimen name="navigation_edge_active_edge_corners">30dp</dimen>
-    <dimen name="navigation_edge_active_far_corners">30dp</dimen>
-    <dimen name="navigation_edge_active_arrow_length">8dp</dimen>
-    <dimen name="navigation_edge_active_arrow_height">9dp</dimen>
+    <item name="navigation_edge_active_background_alpha" format="float" type="dimen">1.0</item>
+    <dimen name="navigation_edge_active_background_width">48dp</dimen>
+    <dimen name="navigation_edge_active_background_height">48dp</dimen>
+    <dimen name="navigation_edge_active_edge_corners">24dp</dimen>
+    <dimen name="navigation_edge_active_far_corners">24dp</dimen>
+    <item name="navigation_edge_active_arrow_alpha" format="float" type="dimen">1.0</item>
+    <dimen name="navigation_edge_active_arrow_length">6.4dp</dimen>
+    <dimen name="navigation_edge_active_arrow_height">7.2dp</dimen>
 
+    <!-- committed -->
+    <item name="navigation_edge_committed_scale" format="float" type="dimen">0.85</item>
+    <item name="navigation_edge_committed_alpha" format="float" type="dimen">0</item>
+
+    <!-- cancelled -->
+    <dimen name="navigation_edge_cancelled_background_width">0dp</dimen>
+
+    <item name="navigation_edge_stretch_scale" format="float" type="dimen">1.0</item>
     <dimen name="navigation_edge_stretch_margin">18dp</dimen>
-    <dimen name="navigation_edge_stretch_background_width">74dp</dimen>
-    <dimen name="navigation_edge_stretch_background_height">60dp</dimen>
-    <dimen name="navigation_edge_stretch_edge_corners">30dp</dimen>
-    <dimen name="navigation_edge_stretch_far_corners">30dp</dimen>
-    <dimen name="navigation_edge_stretched_arrow_length">7dp</dimen>
-    <dimen name="navigation_edge_stretched_arrow_height">10dp</dimen>
+    <dimen name="navigation_edge_stretch_background_width">60dp</dimen>
+    <item name="navigation_edge_stretch_background_alpha" format="float" type="dimen">
+        @dimen/navigation_edge_entry_background_alpha
+    </item>
+    <dimen name="navigation_edge_stretch_background_height">48dp</dimen>
+    <dimen name="navigation_edge_stretch_edge_corners">24dp</dimen>
+    <dimen name="navigation_edge_stretch_far_corners">24dp</dimen>
+    <item name="navigation_edge_strech_arrow_alpha" format="float" type="dimen">1.0</item>
+    <dimen name="navigation_edge_stretched_arrow_length">5.6dp</dimen>
+    <dimen name="navigation_edge_stretched_arrow_height">8dp</dimen>
 
     <dimen name="navigation_edge_cancelled_arrow_length">12dp</dimen>
     <dimen name="navigation_edge_cancelled_arrow_height">0dp</dimen>
@@ -1156,7 +1186,7 @@
 
     <!-- Home Controls -->
     <dimen name="controls_header_menu_size">48dp</dimen>
-    <dimen name="controls_header_bottom_margin">24dp</dimen>
+    <dimen name="controls_header_bottom_margin">16dp</dimen>
     <dimen name="controls_header_app_icon_size">24dp</dimen>
     <dimen name="controls_top_margin">48dp</dimen>
     <dimen name="controls_padding_horizontal">0dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index b535e60..8d95164 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -910,14 +910,6 @@
     <!-- Message shown when non-bypass face authentication succeeds. [CHAR LIMIT=60] -->
     <string name="keyguard_face_successful_unlock_alt1">Face recognized</string>
 
-    <!-- Messages shown when users press outside of udfps region during -->
-    <string-array name="udfps_accessibility_touch_hints">
-        <item>Move left</item>
-        <item>Move down</item>
-        <item>Move right</item>
-        <item>Move up</item>
-    </string-array>
-
     <!-- Message shown when face authentication fails and the pin pad is visible. [CHAR LIMIT=60] -->
     <string name="keyguard_retry">Swipe up to try again</string>
 
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index e56e5d5..00a0444 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -48,8 +48,7 @@
             app:layout_constrainedWidth="true"
             app:layout_constraintStart_toEndOf="@id/clock"
             app:layout_constraintEnd_toStartOf="@id/barrier"
-            app:layout_constraintTop_toTopOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintBaseline_toBaselineOf="@id/clock"
             app:layout_constraintHorizontal_bias="0"
         />
     </Constraint>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
index b7e2494..44c0e16 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
@@ -88,6 +88,7 @@
                 final ArrayList<WindowContainerToken> pausingTasks = new ArrayList<>();
                 WindowContainerToken pipTask = null;
                 WindowContainerToken recentsTask = null;
+                int recentsTaskId = -1;
                 for (int i = apps.length - 1; i >= 0; --i) {
                     final ActivityManager.RunningTaskInfo taskInfo = apps[i].taskInfo;
                     if (apps[i].mode == MODE_CLOSING) {
@@ -106,8 +107,10 @@
                         // This task is for recents, keep it on top.
                         t.setLayer(apps[i].leash, info.getChanges().size() * 3 - i);
                         recentsTask = taskInfo.token;
+                        recentsTaskId = taskInfo.taskId;
                     } else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
                         recentsTask = taskInfo.token;
+                        recentsTaskId = taskInfo.taskId;
                     }
                 }
                 // Also make all the wallpapers opaque since we want the visible from the start
@@ -116,7 +119,7 @@
                 }
                 t.apply();
                 mRecentsSession.setup(controller, info, finishedCallback, pausingTasks, pipTask,
-                        recentsTask, leashMap, mToken,
+                        recentsTask, recentsTaskId, leashMap, mToken,
                         (info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0);
                 recents.onAnimationStart(mRecentsSession, apps, wallpapers, new Rect(0, 0, 0, 0),
                         new Rect());
@@ -154,6 +157,7 @@
         private ArrayList<WindowContainerToken> mPausingTasks = null;
         private WindowContainerToken mPipTask = null;
         private WindowContainerToken mRecentsTask = null;
+        private int mRecentsTaskId = 0;
         private TransitionInfo mInfo = null;
         private ArrayList<SurfaceControl> mOpeningLeashes = null;
         private boolean mOpeningHome = false;
@@ -167,8 +171,8 @@
         void setup(RecentsAnimationControllerCompat wrapped, TransitionInfo info,
                 IRemoteTransitionFinishedCallback finishCB,
                 ArrayList<WindowContainerToken> pausingTasks, WindowContainerToken pipTask,
-                WindowContainerToken recentsTask, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
-                IBinder transition, boolean keyguardLocked) {
+                WindowContainerToken recentsTask, int recentsTaskId, ArrayMap<SurfaceControl,
+                SurfaceControl> leashMap, IBinder transition, boolean keyguardLocked) {
             if (mInfo != null) {
                 throw new IllegalStateException("Trying to run a new recents animation while"
                         + " recents is already active.");
@@ -179,6 +183,7 @@
             mPausingTasks = pausingTasks;
             mPipTask = pipTask;
             mRecentsTask = recentsTask;
+            mRecentsTaskId = recentsTaskId;
             mLeashMap = leashMap;
             mTransition = transition;
             mKeyguardLocked = keyguardLocked;
@@ -296,6 +301,15 @@
         }
 
         @Override public void setInputConsumerEnabled(boolean enabled) {
+            if (enabled) {
+                // transient launches don't receive focus automatically. Since we are taking over
+                // the gesture now, take focus explicitly.
+                try {
+                    ActivityTaskManager.getService().setFocusedTask(mRecentsTaskId);
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Failed to set focused task", e);
+                }
+            }
             if (mWrapped != null) mWrapped.setInputConsumerEnabled(enabled);
         }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 7f95ca6..ea079a9 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -48,6 +48,7 @@
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogLevel.DEBUG
 import com.android.systemui.shared.regionsampling.RegionSampler
+import com.android.systemui.statusbar.Weather
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -273,6 +274,12 @@
         override fun onUserSwitchComplete(userId: Int) {
             clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context))
         }
+
+        override fun onWeatherDataChanged(data: Weather?) {
+            if (data != null) {
+                clock?.events?.onWeatherDataChanged(data)
+            }
+        }
     }
 
     fun registerListeners(parent: View) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 8011efd..92e3641 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -131,6 +131,7 @@
 
     @Override
     void resetState() {
+        mMessageAreaController.setMessage(getInitialMessageResId());
         mView.setPasswordEntryEnabled(true);
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 57bfe54..c785ee9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -247,6 +247,7 @@
                 @Override
                 public void onThemeChanged() {
                     reloadColors();
+                    reset();
                 }
 
                 @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index afa9ef6..d23ea9e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -109,7 +109,6 @@
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
-import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -154,6 +153,7 @@
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.Weather;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.telephony.TelephonyListenerManager;
@@ -3219,6 +3219,24 @@
     }
 
     /**
+     * @param data the weather data (temp, conditions, unit) for weather clock to use
+     */
+    public void sendWeatherData(Weather data) {
+        mHandler.post(()-> {
+            handleWeatherDataUpdate(data); });
+    }
+
+    private void handleWeatherDataUpdate(Weather data) {
+        Assert.isMainThread();
+        for (int i = 0; i < mCallbacks.size(); i++) {
+            KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+            if (cb != null) {
+                cb.onWeatherDataChanged(data);
+            }
+        }
+    }
+
+    /**
      * Handle {@link #MSG_BATTERY_UPDATE}
      */
     private void handleBatteryUpdate(BatteryStatus status) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index e6b9ac8..4a7dd24 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -24,6 +24,7 @@
 
 import com.android.settingslib.fuelgauge.BatteryStatus;
 import com.android.systemui.statusbar.KeyguardIndicationController;
+import com.android.systemui.statusbar.Weather;
 
 import java.util.TimeZone;
 
@@ -58,6 +59,11 @@
     public void onTimeFormatChanged(String timeFormat) { }
 
     /**
+     * Called when receive new weather data.
+     */
+    public void onWeatherDataChanged(Weather data) { }
+
+    /**
      * Called when the carrier PLMN or SPN changes.
      */
     public void onRefreshCarrierInfo() { }
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index c1c7f2d..fb65588 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -124,16 +124,7 @@
     };
     private final ScreenDecorationsLogger mLogger;
 
-    private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
-        @Override
-        public void onFaceSensorLocationChanged() {
-            mLogger.onSensorLocationChanged();
-            if (mExecutor != null) {
-                mExecutor.execute(
-                        () -> updateOverlayProviderViews(new Integer[]{mFaceScanningViewId}));
-            }
-        }
-    };
+    private final AuthController mAuthController;
 
     private DisplayTracker mDisplayTracker;
     @VisibleForTesting
@@ -340,9 +331,22 @@
         mFaceScanningFactory = faceScanningFactory;
         mFaceScanningViewId = com.android.systemui.R.id.face_scanning_anim;
         mLogger = logger;
-        authController.addCallback(mAuthControllerCallback);
+        mAuthController = authController;
     }
 
+
+    private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
+        @Override
+        public void onFaceSensorLocationChanged() {
+            mLogger.onSensorLocationChanged();
+            if (mExecutor != null) {
+                mExecutor.execute(
+                        () -> updateOverlayProviderViews(
+                                new Integer[]{mFaceScanningViewId}));
+            }
+        }
+    };
+
     @Override
     public void start() {
         if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
@@ -353,6 +357,7 @@
         mExecutor = mThreadFactory.buildDelayableExecutorOnHandler(mHandler);
         mExecutor.execute(this::startOnScreenDecorationsThread);
         mDotViewController.setUiExecutor(mExecutor);
+        mAuthController.addCallback(mAuthControllerCallback);
     }
 
     private boolean isPrivacyDotEnabled() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index 4c1a9fa..b342a29 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -50,6 +50,7 @@
 import android.widget.SeekBar;
 import android.widget.Switch;
 
+import com.android.internal.accessibility.common.MagnificationConstants;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
 import com.android.systemui.R;
@@ -139,8 +140,10 @@
         @Override
         public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
             float scale = progress * A11Y_CHANGE_SCALE_DIFFERENCE + A11Y_SCALE_MIN_VALUE;
-            // update persisted scale only when scale >= 2.0
-            if (scale >= 2.0f) {
+            // Update persisted scale only when scale >= PERSISTED_SCALE_MIN_VALUE const.
+            // We assume if the scale is lower than the PERSISTED_SCALE_MIN_VALUE, there will be
+            // no obvious magnification effect.
+            if (scale >= MagnificationConstants.PERSISTED_SCALE_MIN_VALUE) {
                 Settings.Secure.putFloatForUser(mContext.getContentResolver(),
                         Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, scale,
                         UserHandle.USER_CURRENT);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index dad6ebe..c8cf5d7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -55,7 +55,6 @@
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserManager;
-import android.util.DisplayUtils;
 import android.util.Log;
 import android.util.RotationUtils;
 import android.util.SparseBooleanArray;
@@ -69,6 +68,8 @@
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.settingslib.udfps.UdfpsOverlayParams;
+import com.android.settingslib.udfps.UdfpsUtils;
 import com.android.systemui.CoreStartable;
 import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
 import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
@@ -169,6 +170,7 @@
     @NonNull private final UserManager mUserManager;
     @NonNull private final LockPatternUtils mLockPatternUtils;
     @NonNull private final InteractionJankMonitor mInteractionJankMonitor;
+    @NonNull private final UdfpsUtils mUdfpsUtils;
     private final @Background DelayableExecutor mBackgroundExecutor;
     private final DisplayInfo mCachedDisplayInfo = new DisplayInfo();
 
@@ -578,17 +580,7 @@
      */
     private void updateSensorLocations() {
         mDisplay.getDisplayInfo(mCachedDisplayInfo);
-        final Display.Mode maxDisplayMode =
-                DisplayUtils.getMaximumResolutionDisplayMode(mCachedDisplayInfo.supportedModes);
-        final float scaleFactor = android.util.DisplayUtils.getPhysicalPixelDisplaySizeRatio(
-                maxDisplayMode.getPhysicalWidth(), maxDisplayMode.getPhysicalHeight(),
-                mCachedDisplayInfo.getNaturalWidth(), mCachedDisplayInfo.getNaturalHeight());
-        if (scaleFactor == Float.POSITIVE_INFINITY) {
-            mScaleFactor = 1f;
-        } else {
-            mScaleFactor = scaleFactor;
-        }
-
+        mScaleFactor = mUdfpsUtils.getScaleFactor(mCachedDisplayInfo);
         updateUdfpsLocation();
         updateFingerprintLocation();
         updateFaceLocation();
@@ -732,7 +724,8 @@
             @NonNull InteractionJankMonitor jankMonitor,
             @Main Handler handler,
             @Background DelayableExecutor bgExecutor,
-            @NonNull VibratorHelper vibrator) {
+            @NonNull VibratorHelper vibrator,
+            @NonNull UdfpsUtils udfpsUtils) {
         mContext = context;
         mExecution = execution;
         mUserManager = userManager;
@@ -753,6 +746,7 @@
         mSfpsEnrolledForUser = new SparseBooleanArray();
         mFaceEnrolledForUser = new SparseBooleanArray();
         mVibratorHelper = vibrator;
+        mUdfpsUtils = udfpsUtils;
 
         mLogContextInteractor = logContextInteractor;
         mBiometricPromptInteractor = biometricPromptInteractor;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 64d5518..074928a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -50,10 +50,8 @@
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.util.Log;
-import android.util.RotationUtils;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
-import android.view.Surface;
 import android.view.VelocityTracker;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
@@ -66,6 +64,8 @@
 import com.android.internal.util.LatencyTracker;
 import com.android.keyguard.FaceAuthApiRequestReason;
 import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.settingslib.udfps.UdfpsOverlayParams;
+import com.android.settingslib.udfps.UdfpsUtils;
 import com.android.systemui.Dumpable;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.biometrics.dagger.BiometricsBackground;
@@ -168,6 +168,7 @@
     @NonNull private final SessionTracker mSessionTracker;
     @NonNull private final AlternateBouncerInteractor mAlternateBouncerInteractor;
     @NonNull private final SecureSettings mSecureSettings;
+    @NonNull private final UdfpsUtils mUdfpsUtils;
 
     // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
     // sensors, this, in addition to a lot of the code here, will be updated.
@@ -266,7 +267,7 @@
                             mUdfpsDisplayMode, mSecureSettings, requestId, reason, callback,
                             (view, event, fromUdfpsView) -> onTouch(requestId, event,
                                     fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags,
-                            mPrimaryBouncerInteractor, mAlternateBouncerInteractor)));
+                            mPrimaryBouncerInteractor, mAlternateBouncerInteractor, mUdfpsUtils)));
         }
 
         @Override
@@ -475,27 +476,6 @@
                 && mOverlayParams.getSensorBounds().contains((int) x, (int) y);
     }
 
-    private Point getTouchInNativeCoordinates(@NonNull MotionEvent event, int idx) {
-        Point portraitTouch = new Point(
-                (int) event.getRawX(idx),
-                (int) event.getRawY(idx)
-        );
-        final int rot = mOverlayParams.getRotation();
-        if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
-            RotationUtils.rotatePoint(portraitTouch,
-                    RotationUtils.deltaRotation(rot, Surface.ROTATION_0),
-                    mOverlayParams.getLogicalDisplayWidth(),
-                    mOverlayParams.getLogicalDisplayHeight()
-            );
-        }
-
-        // Scale the coordinates to native resolution.
-        final float scale = mOverlayParams.getScaleFactor();
-        portraitTouch.x = (int) (portraitTouch.x / scale);
-        portraitTouch.y = (int) (portraitTouch.y / scale);
-        return portraitTouch;
-    }
-
     private void tryDismissingKeyguard() {
         if (!mOnFingerDown) {
             playStartHaptic();
@@ -713,7 +693,8 @@
                         break;
                     }
                     // Map the touch to portrait mode if the device is in landscape mode.
-                    final Point scaledTouch = getTouchInNativeCoordinates(event, idx);
+                    final Point scaledTouch = mUdfpsUtils.getTouchInNativeCoordinates(
+                            idx, event, mOverlayParams);
                     if (actionMoveWithinSensorArea) {
                         if (mVelocityTracker == null) {
                             // touches could be injected, so the velocity tracker may not have
@@ -757,16 +738,7 @@
                                         + "but serverRequest is null");
                                 return;
                             }
-                            // Scale the coordinates to native resolution.
-                            final float scale = mOverlayParams.getScaleFactor();
-                            final float scaledSensorX =
-                                    mOverlayParams.getSensorBounds().centerX() / scale;
-                            final float scaledSensorY =
-                                    mOverlayParams.getSensorBounds().centerY() / scale;
-
-                            mOverlay.onTouchOutsideOfSensorArea(
-                                    scaledTouch.x, scaledTouch.y, scaledSensorX, scaledSensorY,
-                                    mOverlayParams.getRotation());
+                            mOverlay.onTouchOutsideOfSensorArea(scaledTouch);
                         });
                     }
                 }
@@ -838,7 +810,8 @@
             @NonNull SinglePointerTouchProcessor singlePointerTouchProcessor,
             @NonNull SessionTracker sessionTracker,
             @NonNull AlternateBouncerInteractor alternateBouncerInteractor,
-            @NonNull SecureSettings secureSettings) {
+            @NonNull SecureSettings secureSettings,
+            @NonNull UdfpsUtils udfpsUtils) {
         mContext = context;
         mExecution = execution;
         mVibrator = vibrator;
@@ -880,6 +853,7 @@
         mPrimaryBouncerInteractor = primaryBouncerInteractor;
         mAlternateBouncerInteractor = alternateBouncerInteractor;
         mSecureSettings = secureSettings;
+        mUdfpsUtils = udfpsUtils;
 
         mTouchProcessor = mFeatureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
                 ? singlePointerTouchProcessor : null;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 45ca24d..55bacef 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -20,6 +20,7 @@
 import android.annotation.UiThread
 import android.content.Context
 import android.graphics.PixelFormat
+import android.graphics.Point
 import android.graphics.Rect
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
@@ -46,6 +47,8 @@
 import androidx.annotation.LayoutRes
 import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.settingslib.udfps.UdfpsUtils
+import com.android.settingslib.udfps.UdfpsOverlayParams
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.dump.DumpManager
@@ -101,6 +104,7 @@
         private val primaryBouncerInteractor: PrimaryBouncerInteractor,
         private val alternateBouncerInteractor: AlternateBouncerInteractor,
         private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
+        private val udfpsUtils: UdfpsUtils
 ) {
     /** The view, when [isShowing], or null. */
     var overlayView: UdfpsView? = null
@@ -239,7 +243,9 @@
                                 FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) {
                     // Enroll udfps UI is handled by settings, so use empty view here
                     UdfpsFpmEmptyViewController(
-                            view.addUdfpsView(R.layout.udfps_fpm_empty_view),
+                            view.addUdfpsView(R.layout.udfps_fpm_empty_view){
+                                updateAccessibilityViewLocation(sensorBounds)
+                            },
                             statusBarStateController,
                             shadeExpansionStateManager,
                             dialogManager,
@@ -348,81 +354,18 @@
      * the angle to a list of help messages which are announced if accessibility is enabled.
      *
      */
-    fun onTouchOutsideOfSensorArea(
-        touchX: Float,
-        touchY: Float,
-        sensorX: Float,
-        sensorY: Float,
-        rotation: Int
-    ) {
-
-        if (!touchExplorationEnabled) {
-            return
+    fun onTouchOutsideOfSensorArea(scaledTouch: Point) {
+        val theStr =
+            udfpsUtils.onTouchOutsideOfSensorArea(
+                touchExplorationEnabled,
+                context,
+                scaledTouch.x,
+                scaledTouch.y,
+                overlayParams
+            )
+        if (theStr != null) {
+            animationViewController?.doAnnounceForAccessibility(theStr)
         }
-        val touchHints =
-            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
-        if (touchHints.size != 4) {
-            Log.e(TAG, "expected exactly 4 touch hints, got $touchHints.size?")
-            return
-        }
-        val theStr = onTouchOutsideOfSensorAreaImpl(touchX, touchY, sensorX, sensorY, rotation)
-        Log.v(TAG, "Announcing touch outside : " + theStr)
-        animationViewController?.doAnnounceForAccessibility(theStr)
-    }
-
-    /**
-     * This function computes the angle of touch relative to the sensor and maps
-     * the angle to a list of help messages which are announced if accessibility is enabled.
-     *
-     * There are 4 quadrants of the circle (90 degree arcs)
-     *
-     * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left"
-     * [45,  135)            -> touchHints[1] = "Move Fingerprint down"
-     * And so on.
-     */
-    fun onTouchOutsideOfSensorAreaImpl(
-        touchX: Float,
-        touchY: Float,
-        sensorX: Float,
-        sensorY: Float,
-        rotation: Int
-    ): String {
-        val touchHints =
-            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
-
-        val xRelativeToSensor = touchX - sensorX
-        // Touch coordinates are with respect to the upper left corner, so reverse
-        // this calculation
-        val yRelativeToSensor = sensorY - touchY
-
-        var angleInRad =
-            Math.atan2(yRelativeToSensor.toDouble(), xRelativeToSensor.toDouble())
-        // If the radians are negative, that means we are counting clockwise.
-        // So we need to add 360 degrees
-        if (angleInRad < 0.0) {
-            angleInRad += 2.0 * Math.PI
-        }
-        // rad to deg conversion
-        val degrees = Math.toDegrees(angleInRad)
-
-        val degreesPerBucket = 360.0 / touchHints.size
-        val halfBucketDegrees = degreesPerBucket / 2.0
-        // The mapping should be as follows
-        // [315, 360] && [0, 45] -> 0
-        // [45, 135]             -> 1
-        var index = (((degrees + halfBucketDegrees) % 360) / degreesPerBucket).toInt()
-        index %= touchHints.size
-
-        // A rotation of 90 degrees corresponds to increasing the index by 1.
-        if (rotation == Surface.ROTATION_90) {
-            index = (index + 1) % touchHints.size
-        }
-
-        if (rotation == Surface.ROTATION_270) {
-            index = (index + 3) % touchHints.size
-        }
-
-        return touchHints[index]
     }
 
     /** Cancel this request. */
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt
index e8f041e..8352d0a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyView.kt
@@ -16,7 +16,11 @@
 package com.android.systemui.biometrics
 
 import android.content.Context
+import android.graphics.Rect
 import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import com.android.systemui.R
 
 /**
  * View corresponding with udfps_fpm_empty_view.xml
@@ -32,4 +36,13 @@
     private val fingerprintDrawable: UdfpsFpDrawable = UdfpsFpDrawable(context)
 
     override fun getDrawable(): UdfpsDrawable = fingerprintDrawable
+
+    fun updateAccessibilityViewLocation(sensorBounds: Rect) {
+        val fingerprintAccessibilityView: View = findViewById(R.id.udfps_enroll_accessibility_view)
+        val params: ViewGroup.LayoutParams = fingerprintAccessibilityView.layoutParams
+        params.width = sensorBounds.width()
+        params.height = sensorBounds.height()
+        fingerprintAccessibilityView.layoutParams = params
+        fingerprintAccessibilityView.requestLayout()
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
index 802b9b6..079c0b3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlay.kt
@@ -32,6 +32,7 @@
 import android.view.WindowManager
 import android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.settingslib.udfps.UdfpsOverlayParams
 import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
index 4e6a06b..28ca41d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsOverlayView.kt
@@ -25,6 +25,7 @@
 import android.util.AttributeSet
 import android.view.MotionEvent
 import android.widget.FrameLayout
+import com.android.settingslib.udfps.UdfpsOverlayParams
 
 private const val TAG = "UdfpsOverlayView"
 private const val POINT_SIZE = 10f
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
index e61c614..06dee7a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt
@@ -26,6 +26,7 @@
 import android.util.Log
 import android.view.MotionEvent
 import android.widget.FrameLayout
+import com.android.settingslib.udfps.UdfpsOverlayParams
 import com.android.systemui.R
 import com.android.systemui.doze.DozeReceiver
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index 6f8efba..67d2f30 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.biometrics.dagger
 
+import com.android.settingslib.udfps.UdfpsUtils
 import com.android.systemui.biometrics.data.repository.PromptRepository
 import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl
 import com.android.systemui.biometrics.domain.interactor.CredentialInteractor
@@ -54,6 +55,9 @@
         @BiometricsBackground
         fun providesPluginExecutor(threadFactory: ThreadFactory): Executor =
             threadFactory.buildExecutorOnNewThread("biometrics")
+
+        @Provides
+        fun providesUdfpsUtils(): UdfpsUtils = UdfpsUtils()
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
index 39ea936..234b383 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt
@@ -21,7 +21,7 @@
 import android.view.MotionEvent
 import android.view.MotionEvent.INVALID_POINTER_ID
 import android.view.Surface
-import com.android.systemui.biometrics.UdfpsOverlayParams
+import com.android.settingslib.udfps.UdfpsOverlayParams
 import com.android.systemui.biometrics.udfps.TouchProcessorResult.Failure
 import com.android.systemui.biometrics.udfps.TouchProcessorResult.ProcessedTouch
 import com.android.systemui.dagger.SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt
index ffcebf9..4bf0ef6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/TouchProcessor.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.biometrics.udfps
 
 import android.view.MotionEvent
-import com.android.systemui.biometrics.UdfpsOverlayParams
+import com.android.settingslib.udfps.UdfpsOverlayParams
 
 /**
  * Determines whether a finger entered or left the area of the under-display fingerprint sensor
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index e1aed65..43fb39f 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -492,7 +492,7 @@
     // TODO(b/238475428): Tracking Bug
     @JvmField
     val WM_SHADE_ALLOW_BACK_GESTURE =
-        unreleasedFlag(1207, "persist.wm.debug.shade_allow_back_gesture", teamfood = false)
+        sysPropBooleanFlag(1207, "persist.wm.debug.shade_allow_back_gesture", default = false)
 
     // TODO(b/238475428): Tracking Bug
     @JvmField
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index a692ad7..52d4171 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -28,12 +28,15 @@
 import android.os.UserHandle
 import android.view.ViewGroup
 import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyStateProvider
 import com.android.internal.app.AbstractMultiProfilePagerAdapter.MyUserIdProvider
 import com.android.internal.app.ChooserActivity
 import com.android.internal.app.ResolverListController
 import com.android.internal.app.chooser.NotSelectableTargetInfo
 import com.android.internal.app.chooser.TargetInfo
 import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorComponent
 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController
 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
@@ -47,6 +50,7 @@
 class MediaProjectionAppSelectorActivity(
     private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
     private val activityLauncher: AsyncActivityLauncher,
+    private val featureFlags: FeatureFlags,
     /** This is used to override the dependency in a screenshot test */
     @VisibleForTesting
     private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
@@ -56,7 +60,8 @@
     constructor(
         componentFactory: MediaProjectionAppSelectorComponent.Factory,
         activityLauncher: AsyncActivityLauncher,
-    ) : this(componentFactory, activityLauncher, null)
+        featureFlags: FeatureFlags
+    ) : this(componentFactory, activityLauncher, featureFlags, listControllerFactory = null)
 
     private lateinit var configurationController: ConfigurationController
     private lateinit var controller: MediaProjectionAppSelectorController
@@ -91,6 +96,13 @@
 
     override fun appliedThemeResId(): Int = R.style.Theme_SystemUI_MediaProjectionAppSelector
 
+    override fun createBlockerEmptyStateProvider(): EmptyStateProvider =
+        if (featureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
+            component.emptyStateProvider
+        } else {
+            super.createBlockerEmptyStateProvider()
+        }
+
     override fun createListController(userHandle: UserHandle): ResolverListController =
         listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle)
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index d830fc4..c4e76b2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -45,33 +45,46 @@
 import android.util.Log;
 import android.view.Window;
 
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
 import com.android.systemui.screenrecord.MediaProjectionPermissionDialog;
 import com.android.systemui.screenrecord.ScreenShareOption;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.util.Utils;
 
+import javax.inject.Inject;
+
+import dagger.Lazy;
+
 public class MediaProjectionPermissionActivity extends Activity
         implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
     private static final String TAG = "MediaProjectionPermissionActivity";
     private static final float MAX_APP_NAME_SIZE_PX = 500f;
     private static final String ELLIPSIS = "\u2026";
 
+    private final FeatureFlags mFeatureFlags;
+    private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver;
+
     private String mPackageName;
     private int mUid;
     private IMediaProjectionManager mService;
-    private FeatureFlags mFeatureFlags;
 
     private AlertDialog mDialog;
 
+    @Inject
+    public MediaProjectionPermissionActivity(FeatureFlags featureFlags,
+            Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver) {
+        mFeatureFlags = featureFlags;
+        mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver;
+    }
+
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
 
-        mFeatureFlags = Dependency.get(FeatureFlags.class);
         mPackageName = getCallingPackage();
         IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
         mService = IMediaProjectionManager.Stub.asInterface(b);
@@ -104,6 +117,12 @@
             return;
         }
 
+        if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
+            if (showScreenCaptureDisabledDialogIfNeeded()) {
+                return;
+            }
+        }
+
         TextPaint paint = new TextPaint();
         paint.setTextSize(42);
 
@@ -171,16 +190,7 @@
             mDialog = dialogBuilder.create();
         }
 
-        SystemUIDialog.registerDismissListener(mDialog);
-        SystemUIDialog.applyFlags(mDialog);
-        SystemUIDialog.setDialogSize(mDialog);
-
-        mDialog.setOnCancelListener(this);
-        mDialog.create();
-        mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
-
-        final Window w = mDialog.getWindow();
-        w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+        setUpDialog(mDialog);
 
         mDialog.show();
     }
@@ -200,6 +210,32 @@
         }
     }
 
+    private void setUpDialog(AlertDialog dialog) {
+        SystemUIDialog.registerDismissListener(dialog);
+        SystemUIDialog.applyFlags(dialog);
+        SystemUIDialog.setDialogSize(dialog);
+
+        dialog.setOnCancelListener(this);
+        dialog.create();
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true);
+
+        final Window w = dialog.getWindow();
+        w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+    }
+
+    private boolean showScreenCaptureDisabledDialogIfNeeded() {
+        final UserHandle hostUserHandle = getHostUserHandle();
+        if (mScreenCaptureDevicePolicyResolver.get()
+                .isScreenCaptureCompletelyDisabled(hostUserHandle)) {
+            AlertDialog dialog = new ScreenCaptureDisabledDialog(this);
+            setUpDialog(dialog);
+            dialog.show();
+            return true;
+        }
+
+        return false;
+    }
+
     private void grantMediaProjectionPermission(int screenShareMode) {
         try {
             if (screenShareMode == ENTIRE_SCREEN) {
@@ -211,7 +247,7 @@
                 intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
                         projection.asBinder());
                 intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
-                        UserHandle.getUserHandleForUid(getLaunchedFromUid()));
+                        getHostUserHandle());
                 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
 
                 // Start activity from the current foreground user to avoid creating a separate
@@ -230,6 +266,10 @@
         }
     }
 
+    private UserHandle getHostUserHandle() {
+        return UserHandle.getUserHandleForUid(getLaunchedFromUid());
+    }
+
     private IMediaProjection createProjection(int uid, String packageName) throws RemoteException {
         return mService.createProjection(uid, packageName,
                 MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index f1acae8..997370b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -34,7 +34,7 @@
 
     init {
         setupShader(RippleShader.RippleShape.CIRCLE)
-        setRippleFill(true)
+        setupRippleFadeParams()
         setSparkleStrength(0f)
         isStarted = false
     }
@@ -72,7 +72,7 @@
         animator.removeAllUpdateListeners()
 
         // Only show the outline as ripple expands and disappears when animation ends.
-        setRippleFill(false)
+        removeRippleFill()
 
         val startingPercentage = calculateStartingPercentage(newHeight)
         animator.duration = EXPAND_TO_FULL_DURATION
@@ -103,6 +103,32 @@
         return 1 - remainingPercentage
     }
 
+    private fun setupRippleFadeParams() {
+        with(rippleShader) {
+            // No fade out for the base ring.
+            baseRingFadeParams.fadeOutStart = 1f
+            baseRingFadeParams.fadeOutEnd = 1f
+
+            // No fade in and outs for the center fill, as we always draw it.
+            centerFillFadeParams.fadeInStart = 0f
+            centerFillFadeParams.fadeInEnd = 0f
+            centerFillFadeParams.fadeOutStart = 1f
+            centerFillFadeParams.fadeOutEnd = 1f
+        }
+    }
+
+    private fun removeRippleFill() {
+        with(rippleShader) {
+            baseRingFadeParams.fadeOutStart = RippleShader.DEFAULT_BASE_RING_FADE_OUT_START
+            baseRingFadeParams.fadeOutEnd = RippleShader.DEFAULT_FADE_OUT_END
+
+            centerFillFadeParams.fadeInStart = RippleShader.DEFAULT_FADE_IN_START
+            centerFillFadeParams.fadeInEnd = RippleShader.DEFAULT_CENTER_FILL_FADE_IN_END
+            centerFillFadeParams.fadeOutStart = RippleShader.DEFAULT_CENTER_FILL_FADE_OUT_START
+            centerFillFadeParams.fadeOutEnd = RippleShader.DEFAULT_CENTER_FILL_FADE_OUT_END
+        }
+    }
+
     companion object {
         const val DEFAULT_DURATION = 333L
         const val EXPAND_TO_FULL_DURATION = 1000L
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
index e665d83..1678c6e 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.media.MediaProjectionAppSelectorActivity
 import com.android.systemui.media.MediaProjectionAppSelectorActivity.Companion.EXTRA_HOST_APP_USER_HANDLE
+import com.android.systemui.media.MediaProjectionPermissionActivity
 import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerLabelLoader
 import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader
 import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
@@ -45,10 +46,10 @@
 import dagger.Subcomponent
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
-import javax.inject.Qualifier
-import javax.inject.Scope
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.SupervisorJob
+import javax.inject.Qualifier
+import javax.inject.Scope
 
 @Qualifier @Retention(AnnotationRetention.BINARY) annotation class MediaProjectionAppSelector
 
@@ -67,6 +68,11 @@
     fun provideMediaProjectionAppSelectorActivity(
         activity: MediaProjectionAppSelectorActivity
     ): Activity
+
+    @Binds
+    @IntoMap
+    @ClassKey(MediaProjectionPermissionActivity::class)
+    fun bindsMediaProjectionPermissionActivity(impl: MediaProjectionPermissionActivity): Activity
 }
 
 /**
@@ -149,6 +155,7 @@
 
     val controller: MediaProjectionAppSelectorController
     val recentsViewController: MediaProjectionRecentsViewController
+    val emptyStateProvider: MediaProjectionBlockerEmptyStateProvider
     @get:HostUserHandle val hostUserHandle: UserHandle
     @get:PersonalProfile val personalProfileUserHandle: UserHandle
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
index 2822435..f335733 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanel.kt
@@ -1,14 +1,20 @@
 package com.android.systemui.navigationbar.gestural
 
 import android.content.Context
+import android.content.res.Configuration
+import android.content.res.TypedArray
 import android.graphics.Canvas
 import android.graphics.Paint
 import android.graphics.Path
 import android.graphics.RectF
+import android.util.MathUtils.min
+import android.util.TypedValue
 import android.view.View
+import androidx.appcompat.view.ContextThemeWrapper
 import androidx.dynamicanimation.animation.FloatPropertyCompat
 import androidx.dynamicanimation.animation.SpringAnimation
 import androidx.dynamicanimation.animation.SpringForce
+import com.android.internal.R.style.Theme_DeviceDefault
 import com.android.internal.util.LatencyTracker
 import com.android.settingslib.Utils
 import com.android.systemui.navigationbar.gestural.BackPanelController.DelayedOnAnimationEndListener
@@ -16,7 +22,10 @@
 private const val TAG = "BackPanel"
 private const val DEBUG = false
 
-class BackPanel(context: Context, private val latencyTracker: LatencyTracker) : View(context) {
+class BackPanel(
+        context: Context,
+        private val latencyTracker: LatencyTracker
+) : View(context) {
 
     var arrowsPointLeft = false
         set(value) {
@@ -45,52 +54,54 @@
     /**
      * The length of the arrow measured horizontally. Used for animating [arrowPath]
      */
-    private var arrowLength = AnimatedFloat("arrowLength", SpringForce())
+    private var arrowLength = AnimatedFloat(
+            name = "arrowLength",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS
+    )
 
     /**
      * The height of the arrow measured vertically from its center to its top (i.e. half the total
      * height). Used for animating [arrowPath]
      */
-    private var arrowHeight = AnimatedFloat("arrowHeight", SpringForce())
-
-    private val backgroundWidth = AnimatedFloat(
-        name = "backgroundWidth",
-        SpringForce().apply {
-            stiffness = 600f
-            dampingRatio = 0.65f
-        }
+    var arrowHeight = AnimatedFloat(
+            name = "arrowHeight",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ROTATION_DEGREES
     )
 
-    private val backgroundHeight = AnimatedFloat(
-        name = "backgroundHeight",
-        SpringForce().apply {
-            stiffness = 600f
-            dampingRatio = 0.65f
-        }
+    val backgroundWidth = AnimatedFloat(
+            name = "backgroundWidth",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+            minimumValue = 0f,
+    )
+
+    val backgroundHeight = AnimatedFloat(
+            name = "backgroundHeight",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+            minimumValue = 0f,
     )
 
     /**
      * Corners of the background closer to the edge of the screen (where the arrow appeared from).
      * Used for animating [arrowBackgroundRect]
      */
-    private val backgroundEdgeCornerRadius = AnimatedFloat(
-        name = "backgroundEdgeCornerRadius",
-        SpringForce().apply {
-            stiffness = 400f
-            dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
-        }
-    )
+    val backgroundEdgeCornerRadius = AnimatedFloat("backgroundEdgeCornerRadius")
 
     /**
      * Corners of the background further from the edge of the screens (toward the direction the
      * arrow is being dragged). Used for animating [arrowBackgroundRect]
      */
-    private val backgroundFarCornerRadius = AnimatedFloat(
-        name = "backgroundDragCornerRadius",
-        SpringForce().apply {
-            stiffness = 2200f
-            dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
-        }
+    val backgroundFarCornerRadius = AnimatedFloat("backgroundFarCornerRadius")
+
+    var scale = AnimatedFloat(
+            name = "scale",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_SCALE,
+            minimumValue = 0f
+    )
+
+    val scalePivotX = AnimatedFloat(
+            name = "scalePivotX",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_PIXELS,
+            minimumValue = backgroundWidth.pos / 2,
     )
 
     /**
@@ -98,34 +109,40 @@
      * background's margin relative to the screen edge. The arrow will be centered within the
      * background.
      */
-    private var horizontalTranslation = AnimatedFloat("horizontalTranslation", SpringForce())
+    var horizontalTranslation = AnimatedFloat(name = "horizontalTranslation")
 
-    private val currentAlpha: FloatPropertyCompat<BackPanel> =
-        object : FloatPropertyCompat<BackPanel>("currentAlpha") {
-            override fun setValue(panel: BackPanel, value: Float) {
-                panel.alpha = value
-            }
+    var arrowAlpha = AnimatedFloat(
+            name = "arrowAlpha",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
+            minimumValue = 0f,
+            maximumValue = 1f
+    )
 
-            override fun getValue(panel: BackPanel): Float = panel.alpha
-        }
+    val backgroundAlpha = AnimatedFloat(
+            name = "backgroundAlpha",
+            minimumVisibleChange = SpringAnimation.MIN_VISIBLE_CHANGE_ALPHA,
+            minimumValue = 0f,
+            maximumValue = 1f
+    )
 
-    private val alphaAnimation = SpringAnimation(this, currentAlpha)
-        .setSpring(
-            SpringForce()
-                .setStiffness(60f)
-                .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
-        )
+    private val allAnimatedFloat = setOf(
+            arrowLength,
+            arrowHeight,
+            backgroundWidth,
+            backgroundEdgeCornerRadius,
+            backgroundFarCornerRadius,
+            scalePivotX,
+            scale,
+            horizontalTranslation,
+            arrowAlpha,
+            backgroundAlpha
+    )
 
     /**
      * Canvas vertical translation. How far up/down the arrow and background appear relative to the
      * canvas.
      */
-    private var verticalTranslation: AnimatedFloat = AnimatedFloat(
-        name = "verticalTranslation",
-        SpringForce().apply {
-            stiffness = SpringForce.STIFFNESS_MEDIUM
-        }
-    )
+    var verticalTranslation = AnimatedFloat("verticalTranslation")
 
     /**
      * Use for drawing debug info. Can only be set if [DEBUG]=true
@@ -136,28 +153,67 @@
         }
 
     internal fun updateArrowPaint(arrowThickness: Float) {
-        // Arrow constants
+
         arrowPaint.strokeWidth = arrowThickness
 
-        arrowPaint.color =
-            Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
-        arrowBackgroundPaint.color = Utils.getColorAccentDefaultColor(context)
+        val isDeviceInNightTheme = resources.configuration.uiMode and
+                Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
+
+        val colorControlActivated = ContextThemeWrapper(context, Theme_DeviceDefault)
+                .run {
+                    val typedValue = TypedValue()
+                    val a: TypedArray = obtainStyledAttributes(typedValue.data,
+                            intArrayOf(android.R.attr.colorControlActivated))
+                    val color = a.getColor(0, 0)
+                    a.recycle()
+                    color
+                }
+
+        val colorPrimary =
+                Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
+
+        arrowPaint.color = Utils.getColorAccentDefaultColor(context)
+
+        arrowBackgroundPaint.color = if (isDeviceInNightTheme) {
+            colorPrimary
+        } else {
+            colorControlActivated
+        }
     }
 
-    private inner class AnimatedFloat(name: String, springForce: SpringForce) {
+    inner class AnimatedFloat(
+            name: String,
+            private val minimumVisibleChange: Float? = null,
+            private val minimumValue: Float? = null,
+            private val maximumValue: Float? = null,
+    ) {
+
         // The resting position when not stretched by a touch drag
         private var restingPosition = 0f
 
         // The current position as updated by the SpringAnimation
         var pos = 0f
-            set(v) {
+            private set(v) {
                 if (field != v) {
                     field = v
                     invalidate()
                 }
             }
 
-        val animation: SpringAnimation
+        private val animation: SpringAnimation
+        var spring: SpringForce
+            get() = animation.spring
+            set(value) {
+                animation.cancel()
+                animation.spring = value
+            }
+
+        val isRunning: Boolean
+            get() = animation.isRunning
+
+        fun addEndListener(listener: DelayedOnAnimationEndListener) {
+            animation.addEndListener(listener)
+        }
 
         init {
             val floatProp = object : FloatPropertyCompat<AnimatedFloat>(name) {
@@ -167,8 +223,12 @@
 
                 override fun getValue(animatedFloat: AnimatedFloat): Float = animatedFloat.pos
             }
-            animation = SpringAnimation(this, floatProp)
-            animation.spring = springForce
+            animation = SpringAnimation(this, floatProp).apply {
+                spring = SpringForce()
+                this@AnimatedFloat.minimumValue?.let { setMinValue(it) }
+                this@AnimatedFloat.maximumValue?.let { setMaxValue(it) }
+                this@AnimatedFloat.minimumVisibleChange?.let { minimumVisibleChange = it }
+            }
         }
 
         fun snapTo(newPosition: Float) {
@@ -178,8 +238,24 @@
             pos = newPosition
         }
 
-        fun stretchTo(stretchAmount: Float) {
-            animation.animateToFinalPosition(restingPosition + stretchAmount)
+        fun snapToRestingPosition() {
+            snapTo(restingPosition)
+        }
+
+
+        fun stretchTo(
+                stretchAmount: Float,
+                startingVelocity: Float? = null,
+                springForce: SpringForce? = null
+        ) {
+            animation.apply {
+                startingVelocity?.let {
+                    cancel()
+                    setStartVelocity(it)
+                }
+                springForce?.let { spring = springForce }
+                animateToFinalPosition(restingPosition + stretchAmount)
+            }
         }
 
         /**
@@ -188,18 +264,23 @@
          *
          * The [restingPosition] will remain unchanged. Only the animation is updated.
          */
-        fun stretchBy(finalPosition: Float, amount: Float) {
-            val stretchedAmount = amount * (finalPosition - restingPosition)
+        fun stretchBy(finalPosition: Float?, amount: Float) {
+            val stretchedAmount = amount * ((finalPosition ?: 0f) - restingPosition)
             animation.animateToFinalPosition(restingPosition + stretchedAmount)
         }
 
-        fun updateRestingPosition(pos: Float, animated: Boolean) {
+        fun updateRestingPosition(pos: Float?, animated: Boolean = true) {
+            if (pos == null) return
+
             restingPosition = pos
-            if (animated)
+            if (animated) {
                 animation.animateToFinalPosition(restingPosition)
-            else
+            } else {
                 snapTo(restingPosition)
+            }
         }
+
+        fun cancel() = animation.cancel()
     }
 
     init {
@@ -224,126 +305,203 @@
         return arrowPath
     }
 
-    fun addEndListener(endListener: DelayedOnAnimationEndListener): Boolean {
-        return if (alphaAnimation.isRunning) {
-            alphaAnimation.addEndListener(endListener)
-            true
-        } else if (horizontalTranslation.animation.isRunning) {
-            horizontalTranslation.animation.addEndListener(endListener)
+    fun addAnimationEndListener(
+            animatedFloat: AnimatedFloat,
+            endListener: DelayedOnAnimationEndListener
+    ): Boolean {
+        return if (animatedFloat.isRunning) {
+            animatedFloat.addEndListener(endListener)
             true
         } else {
-            endListener.runNow()
+            endListener.run()
             false
         }
     }
 
+    fun cancelAnimations() {
+        allAnimatedFloat.forEach { it.cancel() }
+    }
+
     fun setStretch(
-        horizontalTranslationStretchAmount: Float,
-        arrowStretchAmount: Float,
-        backgroundWidthStretchAmount: Float,
-        fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens
+            horizontalTranslationStretchAmount: Float,
+            arrowStretchAmount: Float,
+            arrowAlphaStretchAmount: Float,
+            backgroundAlphaStretchAmount: Float,
+            backgroundWidthStretchAmount: Float,
+            backgroundHeightStretchAmount: Float,
+            edgeCornerStretchAmount: Float,
+            farCornerStretchAmount: Float,
+            fullyStretchedDimens: EdgePanelParams.BackIndicatorDimens
     ) {
         horizontalTranslation.stretchBy(
-            finalPosition = fullyStretchedDimens.horizontalTranslation,
-            amount = horizontalTranslationStretchAmount
+                finalPosition = fullyStretchedDimens.horizontalTranslation,
+                amount = horizontalTranslationStretchAmount
         )
         arrowLength.stretchBy(
-            finalPosition = fullyStretchedDimens.arrowDimens.length,
-            amount = arrowStretchAmount
+                finalPosition = fullyStretchedDimens.arrowDimens.length,
+                amount = arrowStretchAmount
         )
         arrowHeight.stretchBy(
-            finalPosition = fullyStretchedDimens.arrowDimens.height,
-            amount = arrowStretchAmount
+                finalPosition = fullyStretchedDimens.arrowDimens.height,
+                amount = arrowStretchAmount
+        )
+        arrowAlpha.stretchBy(
+                finalPosition = fullyStretchedDimens.arrowDimens.alpha,
+                amount = arrowAlphaStretchAmount
+        )
+        backgroundAlpha.stretchBy(
+                finalPosition = fullyStretchedDimens.backgroundDimens.alpha,
+                amount = backgroundAlphaStretchAmount
         )
         backgroundWidth.stretchBy(
-            finalPosition = fullyStretchedDimens.backgroundDimens.width,
-            amount = backgroundWidthStretchAmount
+                finalPosition = fullyStretchedDimens.backgroundDimens.width,
+                amount = backgroundWidthStretchAmount
+        )
+        backgroundHeight.stretchBy(
+                finalPosition = fullyStretchedDimens.backgroundDimens.height,
+                amount = backgroundHeightStretchAmount
+        )
+        backgroundEdgeCornerRadius.stretchBy(
+                finalPosition = fullyStretchedDimens.backgroundDimens.edgeCornerRadius,
+                amount = edgeCornerStretchAmount
+        )
+        backgroundFarCornerRadius.stretchBy(
+                finalPosition = fullyStretchedDimens.backgroundDimens.farCornerRadius,
+                amount = farCornerStretchAmount
         )
     }
 
+    fun popOffEdge(startingVelocity: Float) {
+        val heightStretchAmount = startingVelocity * 50
+        val widthStretchAmount = startingVelocity * 150
+        val scaleStretchAmount = startingVelocity * 0.8f
+        backgroundHeight.stretchTo(stretchAmount = 0f, startingVelocity = -heightStretchAmount)
+        backgroundWidth.stretchTo(stretchAmount = 0f, startingVelocity = widthStretchAmount)
+        scale.stretchTo(stretchAmount = 0f, startingVelocity = -scaleStretchAmount)
+    }
+
+    fun popScale(startingVelocity: Float) {
+        scalePivotX.snapTo(backgroundWidth.pos / 2)
+        scale.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity)
+    }
+
+    fun popArrowAlpha(startingVelocity: Float, springForce: SpringForce? = null) {
+        arrowAlpha.stretchTo(stretchAmount = 0f, startingVelocity = startingVelocity,
+                springForce = springForce)
+    }
+
     fun resetStretch() {
-        horizontalTranslation.stretchTo(0f)
-        arrowLength.stretchTo(0f)
-        arrowHeight.stretchTo(0f)
-        backgroundWidth.stretchTo(0f)
-        backgroundHeight.stretchTo(0f)
-        backgroundEdgeCornerRadius.stretchTo(0f)
-        backgroundFarCornerRadius.stretchTo(0f)
+        backgroundAlpha.snapTo(1f)
+        verticalTranslation.snapTo(0f)
+        scale.snapTo(1f)
+
+        horizontalTranslation.snapToRestingPosition()
+        arrowLength.snapToRestingPosition()
+        arrowHeight.snapToRestingPosition()
+        arrowAlpha.snapToRestingPosition()
+        backgroundWidth.snapToRestingPosition()
+        backgroundHeight.snapToRestingPosition()
+        backgroundEdgeCornerRadius.snapToRestingPosition()
+        backgroundFarCornerRadius.snapToRestingPosition()
     }
 
     /**
      * Updates resting arrow and background size not accounting for stretch
      */
     internal fun setRestingDimens(
-        restingParams: EdgePanelParams.BackIndicatorDimens,
-        animate: Boolean
+            restingParams: EdgePanelParams.BackIndicatorDimens,
+            animate: Boolean = true
     ) {
-        horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation, animate)
+        horizontalTranslation.updateRestingPosition(restingParams.horizontalTranslation)
+        scale.updateRestingPosition(restingParams.scale)
+        arrowAlpha.updateRestingPosition(restingParams.arrowDimens.alpha)
+        backgroundAlpha.updateRestingPosition(restingParams.backgroundDimens.alpha)
+
         arrowLength.updateRestingPosition(restingParams.arrowDimens.length, animate)
         arrowHeight.updateRestingPosition(restingParams.arrowDimens.height, animate)
+        scalePivotX.updateRestingPosition(restingParams.backgroundDimens.width, animate)
         backgroundWidth.updateRestingPosition(restingParams.backgroundDimens.width, animate)
         backgroundHeight.updateRestingPosition(restingParams.backgroundDimens.height, animate)
         backgroundEdgeCornerRadius.updateRestingPosition(
-            restingParams.backgroundDimens.edgeCornerRadius,
-            animate
+                restingParams.backgroundDimens.edgeCornerRadius, animate
         )
         backgroundFarCornerRadius.updateRestingPosition(
-            restingParams.backgroundDimens.farCornerRadius,
-            animate
+                restingParams.backgroundDimens.farCornerRadius, animate
         )
     }
 
     fun animateVertically(yPos: Float) = verticalTranslation.stretchTo(yPos)
 
-    fun setArrowStiffness(arrowStiffness: Float, arrowDampingRatio: Float) {
-        arrowLength.animation.spring.apply {
-            stiffness = arrowStiffness
-            dampingRatio = arrowDampingRatio
-        }
-        arrowHeight.animation.spring.apply {
-            stiffness = arrowStiffness
-            dampingRatio = arrowDampingRatio
-        }
+    fun setSpring(
+            horizontalTranslation: SpringForce? = null,
+            verticalTranslation: SpringForce? = null,
+            scale: SpringForce? = null,
+            arrowLength: SpringForce? = null,
+            arrowHeight: SpringForce? = null,
+            arrowAlpha: SpringForce? = null,
+            backgroundAlpha: SpringForce? = null,
+            backgroundFarCornerRadius: SpringForce? = null,
+            backgroundEdgeCornerRadius: SpringForce? = null,
+            backgroundWidth: SpringForce? = null,
+            backgroundHeight: SpringForce? = null,
+    ) {
+        arrowLength?.let { this.arrowLength.spring = it }
+        arrowHeight?.let { this.arrowHeight.spring = it }
+        arrowAlpha?.let { this.arrowAlpha.spring = it }
+        backgroundAlpha?.let { this.backgroundAlpha.spring = it }
+        backgroundFarCornerRadius?.let { this.backgroundFarCornerRadius.spring = it }
+        backgroundEdgeCornerRadius?.let { this.backgroundEdgeCornerRadius.spring = it }
+        scale?.let { this.scale.spring = it }
+        backgroundWidth?.let { this.backgroundWidth.spring = it }
+        backgroundHeight?.let { this.backgroundHeight.spring = it }
+        horizontalTranslation?.let { this.horizontalTranslation.spring = it }
+        verticalTranslation?.let { this.verticalTranslation.spring = it }
     }
 
     override fun hasOverlappingRendering() = false
 
     override fun onDraw(canvas: Canvas) {
-        var edgeCorner = backgroundEdgeCornerRadius.pos
+        val edgeCorner = backgroundEdgeCornerRadius.pos
         val farCorner = backgroundFarCornerRadius.pos
         val halfHeight = backgroundHeight.pos / 2
+        val canvasWidth = width
+        val backgroundWidth = backgroundWidth.pos
+        val scalePivotX = scalePivotX.pos
 
         canvas.save()
 
-        if (!isLeftPanel) canvas.scale(-1f, 1f, width / 2.0f, 0f)
+        if (!isLeftPanel) canvas.scale(-1f, 1f, canvasWidth / 2.0f, 0f)
 
         canvas.translate(
-            horizontalTranslation.pos,
-            height * 0.5f + verticalTranslation.pos
+                horizontalTranslation.pos,
+                height * 0.5f + verticalTranslation.pos
         )
 
+        canvas.scale(scale.pos, scale.pos, scalePivotX, 0f)
+
         val arrowBackground = arrowBackgroundRect.apply {
             left = 0f
             top = -halfHeight
-            right = backgroundWidth.pos
+            right = backgroundWidth
             bottom = halfHeight
         }.toPathWithRoundCorners(
-            topLeft = edgeCorner,
-            bottomLeft = edgeCorner,
-            topRight = farCorner,
-            bottomRight = farCorner
+                topLeft = edgeCorner,
+                bottomLeft = edgeCorner,
+                topRight = farCorner,
+                bottomRight = farCorner
         )
-        canvas.drawPath(arrowBackground, arrowBackgroundPaint)
+        canvas.drawPath(arrowBackground,
+                arrowBackgroundPaint.apply { alpha = (255 * backgroundAlpha.pos).toInt() })
 
         val dx = arrowLength.pos
         val dy = arrowHeight.pos
 
         // How far the arrow bounding box should be from the edge of the screen. Measured from
         // either the tip or the back of the arrow, whichever is closer
-        var arrowOffset = (backgroundWidth.pos - dx) / 2
+        val arrowOffset = (backgroundWidth - dx) / 2
         canvas.translate(
-            /* dx= */ arrowOffset,
-            /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
+                /* dx= */ arrowOffset,
+                /* dy= */ 0f /* pass 0 for the y position since the canvas was already translated */
         )
 
         val arrowPointsAwayFromEdge = !arrowsPointLeft.xor(isLeftPanel)
@@ -355,6 +513,8 @@
         }
 
         val arrowPath = calculateArrowPath(dx = dx, dy = dy)
+        val arrowPaint = arrowPaint
+                .apply { alpha = (255 * min(arrowAlpha.pos, backgroundAlpha.pos)).toInt() }
         canvas.drawPath(arrowPath, arrowPaint)
         canvas.restore()
 
@@ -372,26 +532,17 @@
     }
 
     private fun RectF.toPathWithRoundCorners(
-        topLeft: Float = 0f,
-        topRight: Float = 0f,
-        bottomRight: Float = 0f,
-        bottomLeft: Float = 0f
+            topLeft: Float = 0f,
+            topRight: Float = 0f,
+            bottomRight: Float = 0f,
+            bottomLeft: Float = 0f
     ): Path = Path().apply {
         val corners = floatArrayOf(
-            topLeft, topLeft,
-            topRight, topRight,
-            bottomRight, bottomRight,
-            bottomLeft, bottomLeft
+                topLeft, topLeft,
+                topRight, topRight,
+                bottomRight, bottomRight,
+                bottomLeft, bottomLeft
         )
         addRoundRect(this@toPathWithRoundCorners, corners, Path.Direction.CW)
     }
-
-    fun cancelAlphaAnimations() {
-        alphaAnimation.cancel()
-        alpha = 1f
-    }
-
-    fun fadeOut() {
-        alphaAnimation.animateToFinalPosition(0f)
-    }
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index 1950c69..ae37fb6 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -24,18 +24,15 @@
 import android.os.SystemClock
 import android.os.VibrationEffect
 import android.util.Log
-import android.util.MathUtils.constrain
-import android.util.MathUtils.saturate
+import android.util.MathUtils
 import android.view.Gravity
 import android.view.MotionEvent
 import android.view.VelocityTracker
-import android.view.View
 import android.view.ViewConfiguration
 import android.view.WindowManager
-import android.view.animation.DecelerateInterpolator
-import android.view.animation.PathInterpolator
+import androidx.core.os.postDelayed
+import androidx.core.view.isVisible
 import androidx.dynamicanimation.animation.DynamicAnimation
-import androidx.dynamicanimation.animation.SpringForce
 import com.android.internal.util.LatencyTracker
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.MotionEventsHandlerBase
@@ -51,58 +48,42 @@
 import kotlin.math.sign
 
 private const val TAG = "BackPanelController"
-private const val DEBUG = false
-
 private const val ENABLE_FAILSAFE = true
 
-private const val FAILSAFE_DELAY_MS: Long = 350
+private const val PX_PER_SEC = 1000
+private const val PX_PER_MS = 1
 
-/**
- * The time required between the arrow-appears vibration effect and the back-committed vibration
- * effect. If the arrow is flung quickly, the phone only vibrates once. However, if the arrow is
- * held on the screen for a long time, it will vibrate a second time when the back gesture is
- * committed.
- */
-private const val GESTURE_DURATION_FOR_CLICK_MS = 400
+private const val MIN_DURATION_CANCELLED_ANIMATION = 200L
+private const val MIN_DURATION_COMMITTED_ANIMATION = 200L
+private const val MIN_DURATION_ACTIVE_ANIMATION = 300L
+private const val MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION = 50L
+private const val MIN_DURATION_CONSIDERED_AS_FLING = 100L
 
-/**
- * The min duration arrow remains on screen during a fling event.
- */
-private const val FLING_MIN_APPEARANCE_DURATION = 235L
+private const val FAILSAFE_DELAY_MS = 350L
+private const val POP_ON_FLING_DELAY = 160L
 
-/**
- * The min duration arrow remains on screen during a fling event.
- */
-private const val MIN_FLING_VELOCITY = 3000
+private val VIBRATE_ACTIVATED_EFFECT =
+        VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
 
-/**
- * The amount of rubber banding we do for the vertical translation
- */
-private const val RUBBER_BAND_AMOUNT = 15
+private val VIBRATE_DEACTIVATED_EFFECT =
+        VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)
 
-private const val ARROW_APPEAR_STIFFNESS = 600f
-private const val ARROW_APPEAR_DAMPING_RATIO = 0.4f
-private const val ARROW_DISAPPEAR_STIFFNESS = 1200f
-private const val ARROW_DISAPPEAR_DAMPING_RATIO = SpringForce.DAMPING_RATIO_NO_BOUNCY
-
-/**
- * The interpolator used to rubber band
- */
-private val RUBBER_BAND_INTERPOLATOR = PathInterpolator(1.0f / 5.0f, 1.0f, 1.0f, 1.0f)
-
-private val DECELERATE_INTERPOLATOR = DecelerateInterpolator()
-
-private val DECELERATE_INTERPOLATOR_SLOW = DecelerateInterpolator(0.7f)
+private const val DEBUG = false
 
 class BackPanelController private constructor(
-    context: Context,
-    private val windowManager: WindowManager,
-    private val viewConfiguration: ViewConfiguration,
-    @Main private val mainHandler: Handler,
-    private val vibratorHelper: VibratorHelper,
-    private val configurationController: ConfigurationController,
-    latencyTracker: LatencyTracker
-) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
+        context: Context,
+        private val windowManager: WindowManager,
+        private val viewConfiguration: ViewConfiguration,
+        @Main private val mainHandler: Handler,
+        private val vibratorHelper: VibratorHelper,
+        private val configurationController: ConfigurationController,
+        private val latencyTracker: LatencyTracker
+) : ViewController<BackPanel>(
+        BackPanel(
+                context,
+                latencyTracker
+        )
+), NavigationEdgeBackPlugin {
 
     /**
      * Injectable instance to create a new BackPanelController.
@@ -111,23 +92,23 @@
      * BackPanelController, and we need to match EdgeBackGestureHandler's context.
      */
     class Factory @Inject constructor(
-        private val windowManager: WindowManager,
-        private val viewConfiguration: ViewConfiguration,
-        @Main private val mainHandler: Handler,
-        private val vibratorHelper: VibratorHelper,
-        private val configurationController: ConfigurationController,
-        private val latencyTracker: LatencyTracker
+            private val windowManager: WindowManager,
+            private val viewConfiguration: ViewConfiguration,
+            @Main private val mainHandler: Handler,
+            private val vibratorHelper: VibratorHelper,
+            private val configurationController: ConfigurationController,
+            private val latencyTracker: LatencyTracker
     ) {
         /** Construct a [BackPanelController].  */
         fun create(context: Context): BackPanelController {
             val backPanelController = BackPanelController(
-                context,
-                windowManager,
-                viewConfiguration,
-                mainHandler,
-                vibratorHelper,
-                configurationController,
-                latencyTracker
+                    context,
+                    windowManager,
+                    viewConfiguration,
+                    mainHandler,
+                    vibratorHelper,
+                    configurationController,
+                    latencyTracker
             )
             backPanelController.init()
             return backPanelController
@@ -138,17 +119,15 @@
     private var currentState: GestureState = GestureState.GONE
     private var previousState: GestureState = GestureState.GONE
 
-    // Phone should only vibrate the first time the arrow is activated
-    private var hasHapticPlayed = false
-
     // Screen attributes
     private lateinit var layoutParams: WindowManager.LayoutParams
     private val displaySize = Point()
 
     private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
-
+    private var previousXTranslationOnActiveOffset = 0f
     private var previousXTranslation = 0f
     private var totalTouchDelta = 0f
+    private var touchDeltaStartX = 0f
     private var velocityTracker: VelocityTracker? = null
         set(value) {
             if (field != value) field?.recycle()
@@ -162,8 +141,18 @@
     // The x,y position of the first touch event
     private var startX = 0f
     private var startY = 0f
+    private var startIsLeft: Boolean? = null
 
-    private var gestureStartTime = 0L
+    private var gestureSinceActionDown = 0L
+    private var gestureEntryTime = 0L
+    private var gestureActiveTime = 0L
+    private var gestureInactiveOrEntryTime = 0L
+    private var gestureArrowStrokeVisibleTime = 0L
+
+    private val elapsedTimeSinceActionDown
+        get() = SystemClock.uptimeMillis() - gestureSinceActionDown
+    private val elapsedTimeSinceEntry
+        get() = SystemClock.uptimeMillis() - gestureEntryTime
 
     // Whether the current gesture has moved a sufficiently large amount,
     // so that we can unambiguously start showing the ENTRY animation
@@ -192,17 +181,6 @@
 
         /* back action currently cancelling, arrow soon to be GONE */
         CANCELLED;
-
-        /**
-         * @return true if the current state responds to touch move events in some way (e.g. by
-         * stretching the back indicator)
-         */
-        fun isInteractive(): Boolean {
-            return when (this) {
-                ENTRY, ACTIVE, INACTIVE -> true
-                GONE, FLUNG, COMMITTED, CANCELLED -> false
-            }
-        }
     }
 
     /**
@@ -210,50 +188,43 @@
      * runnable is not called if the animation is cancelled
      */
     inner class DelayedOnAnimationEndListener internal constructor(
-        private val handler: Handler,
-        private val runnable: Runnable,
-        private val minDuration: Long
+            private val handler: Handler,
+            private val runnableDelay: Long,
+            val runnable: Runnable,
     ) : DynamicAnimation.OnAnimationEndListener {
+
         override fun onAnimationEnd(
-            animation: DynamicAnimation<*>,
-            canceled: Boolean,
-            value: Float,
-            velocity: Float
+                animation: DynamicAnimation<*>,
+                canceled: Boolean,
+                value: Float,
+                velocity: Float
         ) {
             animation.removeEndListener(this)
+
             if (!canceled) {
-                // Total elapsed time of the gesture and the animation
-                val totalElapsedTime = SystemClock.uptimeMillis() - gestureStartTime
+
                 // The delay between finishing this animation and starting the runnable
-                val delay = max(0, minDuration - totalElapsedTime)
+                val delay = max(0, runnableDelay - elapsedTimeSinceEntry)
+
                 handler.postDelayed(runnable, delay)
             }
         }
 
-        internal fun runNow() {
-            runnable.run()
-        }
+        internal fun run() = runnable.run()
     }
 
-    private val setCommittedEndListener =
-        DelayedOnAnimationEndListener(
-            mainHandler,
-            { updateArrowState(GestureState.COMMITTED) },
-            minDuration = FLING_MIN_APPEARANCE_DURATION
-        )
+    private val onEndSetCommittedStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) {
+        updateArrowState(GestureState.COMMITTED)
+    }
 
-    private val setGoneEndListener =
-        DelayedOnAnimationEndListener(
-            mainHandler,
-            {
+
+    private val onEndSetGoneStateListener =
+            DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
                 cancelFailsafe()
                 updateArrowState(GestureState.GONE)
-            },
-            minDuration = 0
-        )
+            }
 
-    // Vibration
-    private var vibrationTime: Long = 0
+    private val playAnimationThenSetGoneOnAlphaEnd = Runnable { playAnimationThenSetGoneEnd() }
 
     // Minimum of the screen's width or the predefined threshold
     private var fullyStretchedThreshold = 0f
@@ -280,7 +251,7 @@
         updateConfiguration()
         updateArrowDirection(configurationController.isLayoutRtl)
         updateArrowState(GestureState.GONE, force = true)
-        updateRestingArrowDimens(animated = false, currentState)
+        updateRestingArrowDimens()
         configurationController.addCallback(configurationListener)
     }
 
@@ -297,22 +268,57 @@
         velocityTracker!!.addMovement(event)
         when (event.actionMasked) {
             MotionEvent.ACTION_DOWN -> {
-                resetOnDown()
+                gestureSinceActionDown = SystemClock.uptimeMillis()
+                cancelAllPendingAnimations()
                 startX = event.x
                 startY = event.y
-                gestureStartTime = SystemClock.uptimeMillis()
+
+                updateArrowState(GestureState.GONE)
+                updateYStartPosition(startY)
+
+                // reset animation properties
+                startIsLeft = mView.isLeftPanel
+                hasPassedDragSlop = false
+                mView.resetStretch()
             }
             MotionEvent.ACTION_MOVE -> {
-                // only go to the ENTRY state after some minimum motion has occurred
                 if (dragSlopExceeded(event.x, startX)) {
                     handleMoveEvent(event)
                 }
             }
             MotionEvent.ACTION_UP -> {
-                if (currentState == GestureState.ACTIVE) {
-                    updateArrowState(if (isFlung()) GestureState.FLUNG else GestureState.COMMITTED)
-                } else if (currentState != GestureState.GONE) { // if invisible, skip animation
-                    updateArrowState(GestureState.CANCELLED)
+                when (currentState) {
+                    GestureState.ENTRY -> {
+                        if (isFlungAwayFromEdge(endX = event.x)) {
+                            updateArrowState(GestureState.ACTIVE)
+                            updateArrowState(GestureState.FLUNG)
+                        } else {
+                            updateArrowState(GestureState.CANCELLED)
+                        }
+                    }
+                    GestureState.INACTIVE -> {
+                        if (isFlungAwayFromEdge(endX = event.x)) {
+                            mainHandler.postDelayed(MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION) {
+                                updateArrowState(GestureState.ACTIVE)
+                                updateArrowState(GestureState.FLUNG)
+                            }
+                        } else {
+                            updateArrowState(GestureState.CANCELLED)
+                        }
+                    }
+                    GestureState.ACTIVE -> {
+                        if (elapsedTimeSinceEntry < MIN_DURATION_CONSIDERED_AS_FLING) {
+                            updateArrowState(GestureState.FLUNG)
+                        } else {
+                            updateArrowState(GestureState.COMMITTED)
+                        }
+                    }
+                    GestureState.GONE,
+                    GestureState.FLUNG,
+                    GestureState.COMMITTED,
+                    GestureState.CANCELLED -> {
+                        updateArrowState(GestureState.CANCELLED)
+                    }
                 }
                 velocityTracker = null
             }
@@ -326,6 +332,14 @@
         }
     }
 
+    private fun cancelAllPendingAnimations() {
+        cancelFailsafe()
+        mView.cancelAnimations()
+        mainHandler.removeCallbacks(onEndSetCommittedStateListener.runnable)
+        mainHandler.removeCallbacks(onEndSetGoneStateListener.runnable)
+        mainHandler.removeCallbacks(playAnimationThenSetGoneOnAlphaEnd)
+    }
+
     /**
      * Returns false until the current gesture exceeds the touch slop threshold,
      * and returns true thereafter (we reset on the subsequent back gesture).
@@ -336,7 +350,7 @@
     private fun dragSlopExceeded(curX: Float, startX: Float): Boolean {
         if (hasPassedDragSlop) return true
 
-        if (abs(curX - startX) > viewConfiguration.scaledTouchSlop) {
+        if (abs(curX - startX) > viewConfiguration.scaledEdgeSlop) {
             // Reset the arrow to the side
             updateArrowState(GestureState.ENTRY)
 
@@ -349,39 +363,46 @@
     }
 
     private fun updateArrowStateOnMove(yTranslation: Float, xTranslation: Float) {
-        if (!currentState.isInteractive())
-            return
+
+        val isWithinYActivationThreshold = xTranslation * 2 >= yTranslation
 
         when (currentState) {
-            // Check if we should transition from ENTRY to ACTIVE
-            GestureState.ENTRY ->
-                if (xTranslation > params.swipeTriggerThreshold) {
+            GestureState.ENTRY -> {
+                if (xTranslation > params.staticTriggerThreshold) {
                     updateArrowState(GestureState.ACTIVE)
                 }
+            }
+            GestureState.ACTIVE -> {
+                val isPastDynamicDeactivationThreshold =
+                        totalTouchDelta <= params.deactivationSwipeTriggerThreshold
+                val isMinDurationElapsed =
+                        elapsedTimeSinceActionDown > MIN_DURATION_ACTIVE_ANIMATION
 
-            // Abort if we had continuous motion toward the edge for a while, OR the direction
-            // in Y is bigger than X * 2
-            GestureState.ACTIVE ->
-                if ((totalTouchDelta < 0 && -totalTouchDelta > params.minDeltaForSwitch) ||
-                    (yTranslation > xTranslation * 2)
+                if (isMinDurationElapsed && (!isWithinYActivationThreshold ||
+                                isPastDynamicDeactivationThreshold)
                 ) {
                     updateArrowState(GestureState.INACTIVE)
                 }
+            }
+            GestureState.INACTIVE -> {
+                val isPastStaticThreshold =
+                        xTranslation > params.staticTriggerThreshold
+                val isPastDynamicReactivationThreshold = totalTouchDelta > 0 &&
+                        abs(totalTouchDelta) >=
+                        params.reactivationTriggerThreshold
 
-            //  Re-activate if we had continuous motion away from the edge for a while
-            GestureState.INACTIVE ->
-                if (totalTouchDelta > 0 && totalTouchDelta > params.minDeltaForSwitch) {
+                if (isPastStaticThreshold &&
+                        isPastDynamicReactivationThreshold &&
+                        isWithinYActivationThreshold
+                ) {
                     updateArrowState(GestureState.ACTIVE)
                 }
-
-            // By default assume the current direction is kept
+            }
             else -> {}
         }
     }
 
     private fun handleMoveEvent(event: MotionEvent) {
-        if (!currentState.isInteractive())
-            return
 
         val x = event.x
         val y = event.y
@@ -401,23 +422,44 @@
         previousXTranslation = xTranslation
 
         if (abs(xDelta) > 0) {
-            if (sign(xDelta) == sign(totalTouchDelta)) {
+            val range =
+                params.run { deactivationSwipeTriggerThreshold..reactivationTriggerThreshold }
+            val isTouchInContinuousDirection =
+                    sign(xDelta) == sign(totalTouchDelta) || totalTouchDelta in range
+
+            if (isTouchInContinuousDirection) {
                 // Direction has NOT changed, so keep counting the delta
                 totalTouchDelta += xDelta
             } else {
                 // Direction has changed, so reset the delta
                 totalTouchDelta = xDelta
+                touchDeltaStartX = x
             }
         }
 
         updateArrowStateOnMove(yTranslation, xTranslation)
+
         when (currentState) {
-            GestureState.ACTIVE ->
-                stretchActiveBackIndicator(fullScreenStretchProgress(xTranslation))
-            GestureState.ENTRY ->
-                stretchEntryBackIndicator(preThresholdStretchProgress(xTranslation))
-            GestureState.INACTIVE ->
-                mView.resetStretch()
+            GestureState.ACTIVE -> {
+                stretchActiveBackIndicator(fullScreenProgress(xTranslation))
+            }
+            GestureState.ENTRY -> {
+                val progress = staticThresholdProgress(xTranslation)
+                stretchEntryBackIndicator(progress)
+
+                params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let {
+                    mView.popArrowAlpha(0f, it.value)
+                }
+            }
+            GestureState.INACTIVE -> {
+                val progress = reactivationThresholdProgress(totalTouchDelta)
+                stretchInactiveBackIndicator(progress)
+
+                params.arrowStrokeAlphaSpring.get(progress).takeIf { it.isNewState }?.let {
+                    gestureArrowStrokeVisibleTime = SystemClock.uptimeMillis()
+                    mView.popArrowAlpha(0f, it.value)
+                }
+            }
             else -> {}
         }
 
@@ -428,21 +470,22 @@
     private fun setVerticalTranslation(yOffset: Float) {
         val yTranslation = abs(yOffset)
         val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f
-        val yProgress = saturate(yTranslation / (maxYOffset * RUBBER_BAND_AMOUNT))
-        mView.animateVertically(
-            RUBBER_BAND_INTERPOLATOR.getInterpolation(yProgress) * maxYOffset *
+        val rubberbandAmount = 15f
+        val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount))
+        val yPosition = params.translationInterpolator.getInterpolation(yProgress) *
+                maxYOffset *
                 sign(yOffset)
-        )
+        mView.animateVertically(yPosition)
     }
 
     /**
-     * @return the relative position of the drag from the time after the arrow is activated until
+     * Tracks the relative position of the drag from the time after the arrow is activated until
      * the arrow is fully stretched (between 0.0 - 1.0f)
      */
-    private fun fullScreenStretchProgress(xTranslation: Float): Float {
-        return saturate(
-            (xTranslation - params.swipeTriggerThreshold) /
-                (fullyStretchedThreshold - params.swipeTriggerThreshold)
+    private fun fullScreenProgress(xTranslation: Float): Float {
+        return MathUtils.saturate(
+                (xTranslation - previousXTranslationOnActiveOffset) /
+                        (fullyStretchedThreshold - previousXTranslationOnActiveOffset)
         )
     }
 
@@ -450,26 +493,74 @@
      * Tracks the relative position of the drag from the entry until the threshold where the arrow
      * activates (between 0.0 - 1.0f)
      */
-    private fun preThresholdStretchProgress(xTranslation: Float): Float {
-        return saturate(xTranslation / params.swipeTriggerThreshold)
+    private fun staticThresholdProgress(xTranslation: Float): Float {
+        return MathUtils.saturate(xTranslation / params.staticTriggerThreshold)
+    }
+
+    private fun reactivationThresholdProgress(totalTouchDelta: Float): Float {
+        return MathUtils.saturate(totalTouchDelta / params.reactivationTriggerThreshold)
     }
 
     private fun stretchActiveBackIndicator(progress: Float) {
-        val rubberBandIterpolation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
         mView.setStretch(
-            horizontalTranslationStretchAmount = rubberBandIterpolation,
-            arrowStretchAmount = rubberBandIterpolation,
-            backgroundWidthStretchAmount = DECELERATE_INTERPOLATOR_SLOW.getInterpolation(progress),
-            params.fullyStretchedIndicator
+                horizontalTranslationStretchAmount = params.translationInterpolator
+                        .getInterpolation(progress),
+                arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+                backgroundWidthStretchAmount = params.activeWidthInterpolator
+                        .getInterpolation(progress),
+                backgroundAlphaStretchAmount = 1f,
+                backgroundHeightStretchAmount = 1f,
+                arrowAlphaStretchAmount = 1f,
+                edgeCornerStretchAmount = 1f,
+                farCornerStretchAmount = 1f,
+                fullyStretchedDimens = params.fullyStretchedIndicator
         )
     }
 
     private fun stretchEntryBackIndicator(progress: Float) {
         mView.setStretch(
-            horizontalTranslationStretchAmount = 0f,
-            arrowStretchAmount = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress),
-            backgroundWidthStretchAmount = DECELERATE_INTERPOLATOR.getInterpolation(progress),
-            params.preThresholdIndicator
+                horizontalTranslationStretchAmount = 0f,
+                arrowStretchAmount = params.arrowAngleInterpolator
+                        .getInterpolation(progress),
+                backgroundWidthStretchAmount = params.entryWidthInterpolator
+                        .getInterpolation(progress),
+                backgroundHeightStretchAmount = params.heightInterpolator
+                        .getInterpolation(progress),
+                backgroundAlphaStretchAmount = 1f,
+                arrowAlphaStretchAmount = params.arrowStrokeAlphaInterpolator.get(progress).value,
+                edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+                farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+                fullyStretchedDimens = params.preThresholdIndicator
+        )
+    }
+
+    private var previousPreThresholdWidthInterpolator = params.entryWidthTowardsEdgeInterpolator
+    fun preThresholdWidthStretchAmount(progress: Float): Float {
+        val interpolator = run {
+            val isPastSlop = abs(totalTouchDelta) > ViewConfiguration.get(context).scaledTouchSlop
+            if (isPastSlop) {
+                if (totalTouchDelta > 0) {
+                    params.entryWidthInterpolator
+                } else params.entryWidthTowardsEdgeInterpolator
+            } else {
+                previousPreThresholdWidthInterpolator
+            }.also { previousPreThresholdWidthInterpolator = it }
+        }
+        return interpolator.getInterpolation(progress).coerceAtLeast(0f)
+    }
+
+    private fun stretchInactiveBackIndicator(progress: Float) {
+        mView.setStretch(
+                horizontalTranslationStretchAmount = 0f,
+                arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
+                backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
+                backgroundHeightStretchAmount = params.heightInterpolator
+                        .getInterpolation(progress),
+                backgroundAlphaStretchAmount = 1f,
+                arrowAlphaStretchAmount = params.arrowStrokeAlphaInterpolator.get(progress).value,
+                edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
+                farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
+                fullyStretchedDimens = params.preThresholdIndicator
         )
     }
 
@@ -487,8 +578,7 @@
         }
     }
 
-    override fun setInsets(insetLeft: Int, insetRight: Int) {
-    }
+    override fun setInsets(insetLeft: Int, insetRight: Int) = Unit
 
     override fun setBackCallback(callback: NavigationEdgeBackPlugin.BackCallback) {
         backCallback = callback
@@ -500,65 +590,57 @@
     }
 
     override fun setMotionEventsHandler(motionEventsHandler: MotionEventsHandlerBase?) {
-        TODO("Not yet implemented")
+        // TODO(255697805): Integrate MotionEventHandler for trackpad.
     }
 
-    private fun isFlung() = velocityTracker!!.run {
-        computeCurrentVelocity(1000)
-        abs(xVelocity) > MIN_FLING_VELOCITY
+    private fun isDragAwayFromEdge(velocityPxPerSecThreshold: Int = 0) = velocityTracker!!.run {
+        computeCurrentVelocity(PX_PER_SEC)
+        val velocity = xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
+        velocity > velocityPxPerSecThreshold
     }
 
-    private fun playFlingBackAnimation() {
-        playAnimation(setCommittedEndListener)
+    private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean {
+        val minDistanceConsideredForFling = ViewConfiguration.get(context).scaledTouchSlop
+        val flingDistance = abs(endX - startX)
+        val isPastFlingVelocity = isDragAwayFromEdge(
+                velocityPxPerSecThreshold =
+                ViewConfiguration.get(context).scaledMinimumFlingVelocity)
+        return flingDistance > minDistanceConsideredForFling && isPastFlingVelocity
     }
 
-    private fun playCommitBackAnimation() {
-        // Check if we should vibrate again
-        if (previousState != GestureState.FLUNG) {
-            velocityTracker!!.computeCurrentVelocity(1000)
-            val isSlow = abs(velocityTracker!!.xVelocity) < 500
-            val hasNotVibratedRecently =
-                SystemClock.uptimeMillis() - vibrationTime >= GESTURE_DURATION_FOR_CLICK_MS
-            if (isSlow || hasNotVibratedRecently) {
-                vibratorHelper.vibrate(VibrationEffect.EFFECT_CLICK)
-            }
-        }
-        // Dispatch the actual back trigger
-        if (DEBUG) Log.d(TAG, "playCommitBackAnimation() invoked triggerBack() on backCallback")
-        backCallback.triggerBack()
-
-        playAnimation(setGoneEndListener)
-    }
-
-    private fun playCancelBackAnimation() {
-        backCallback.cancelBack()
-        playAnimation(setGoneEndListener)
-    }
-
-    /**
-     * @return true if the animation is running, false otherwise. Some transitions don't animate
-     */
-    private fun playAnimation(endListener: DelayedOnAnimationEndListener) {
-        updateRestingArrowDimens(animated = true, currentState)
-
-        if (!mView.addEndListener(endListener)) {
+    private fun playHorizontalAnimationThen(onEnd: DelayedOnAnimationEndListener) {
+        updateRestingArrowDimens()
+        if (!mView.addAnimationEndListener(mView.horizontalTranslation, onEnd)) {
             scheduleFailsafe()
         }
     }
 
-    private fun resetOnDown() {
-        hasPassedDragSlop = false
-        hasHapticPlayed = false
-        totalTouchDelta = 0f
-        vibrationTime = 0
-        cancelFailsafe()
+    private fun playAnimationThenSetGoneEnd() {
+        updateRestingArrowDimens()
+        if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
+            scheduleFailsafe()
+        }
     }
 
-    private fun updateYPosition(touchY: Float) {
+    private fun playWithBackgroundWidthAnimation(
+            onEnd: DelayedOnAnimationEndListener,
+            delay: Long = 0L
+    ) {
+        if (delay == 0L) {
+            updateRestingArrowDimens()
+            if (!mView.addAnimationEndListener(mView.backgroundWidth, onEnd)) {
+                scheduleFailsafe()
+            }
+        } else {
+            mainHandler.postDelayed(delay) { playWithBackgroundWidthAnimation(onEnd, delay = 0L) }
+        }
+    }
+
+    private fun updateYStartPosition(touchY: Float) {
         var yPosition = touchY - params.fingerOffset
         yPosition = max(yPosition, params.minArrowYPosition.toFloat())
         yPosition -= layoutParams.height / 2.0f
-        layoutParams.y = constrain(yPosition.toInt(), 0, displaySize.y)
+        layoutParams.y = MathUtils.constrain(yPosition.toInt(), 0, displaySize.y)
     }
 
     override fun setDisplaySize(displaySize: Point) {
@@ -569,53 +651,135 @@
     /**
      * Updates resting arrow and background size not accounting for stretch
      */
-    private fun updateRestingArrowDimens(animated: Boolean, currentState: GestureState) {
-        if (animated) {
-            when (currentState) {
-                GestureState.ENTRY, GestureState.ACTIVE, GestureState.FLUNG ->
-                    mView.setArrowStiffness(ARROW_APPEAR_STIFFNESS, ARROW_APPEAR_DAMPING_RATIO)
-                GestureState.CANCELLED -> mView.fadeOut()
-                else ->
-                    mView.setArrowStiffness(
-                        ARROW_DISAPPEAR_STIFFNESS,
-                        ARROW_DISAPPEAR_DAMPING_RATIO
-                    )
+    private fun updateRestingArrowDimens() {
+        when (currentState) {
+            GestureState.GONE,
+            GestureState.ENTRY -> {
+                mView.setSpring(
+                        arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
+                        arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
+                        arrowAlpha = params.entryIndicator.arrowDimens.alphaSpring,
+                        scale = params.entryIndicator.scaleSpring,
+                        verticalTranslation = params.entryIndicator.verticalTranslationSpring,
+                        horizontalTranslation = params.entryIndicator.horizontalTranslationSpring,
+                        backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring,
+                        backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring,
+                        backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring,
+                        backgroundEdgeCornerRadius = params.entryIndicator.backgroundDimens
+                                .edgeCornerRadiusSpring,
+                        backgroundFarCornerRadius = params.entryIndicator.backgroundDimens
+                                .farCornerRadiusSpring,
+                )
             }
+            GestureState.INACTIVE -> {
+                mView.setSpring(
+                        arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring,
+                        arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring,
+                        horizontalTranslation = params.preThresholdIndicator
+                                .horizontalTranslationSpring,
+                        scale = params.preThresholdIndicator.scaleSpring,
+                        backgroundWidth = params.preThresholdIndicator.backgroundDimens
+                                .widthSpring,
+                        backgroundHeight = params.preThresholdIndicator.backgroundDimens
+                                .heightSpring,
+                        backgroundEdgeCornerRadius = params.preThresholdIndicator.backgroundDimens
+                                .edgeCornerRadiusSpring,
+                        backgroundFarCornerRadius = params.preThresholdIndicator.backgroundDimens
+                                .farCornerRadiusSpring,
+                )
+            }
+            GestureState.ACTIVE -> {
+                mView.setSpring(
+                        arrowLength = params.activeIndicator.arrowDimens.lengthSpring,
+                        arrowHeight = params.activeIndicator.arrowDimens.heightSpring,
+                        scale = params.activeIndicator.scaleSpring,
+                        horizontalTranslation = params.activeIndicator.horizontalTranslationSpring,
+                        backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring,
+                        backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring,
+                        backgroundEdgeCornerRadius = params.activeIndicator.backgroundDimens
+                                .edgeCornerRadiusSpring,
+                        backgroundFarCornerRadius = params.activeIndicator.backgroundDimens
+                                .farCornerRadiusSpring,
+                )
+            }
+            GestureState.FLUNG -> {
+                mView.setSpring(
+                        arrowLength = params.flungIndicator.arrowDimens.lengthSpring,
+                        arrowHeight = params.flungIndicator.arrowDimens.heightSpring,
+                        backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring,
+                        backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring,
+                        backgroundEdgeCornerRadius = params.flungIndicator.backgroundDimens
+                                .edgeCornerRadiusSpring,
+                        backgroundFarCornerRadius = params.flungIndicator.backgroundDimens
+                                .farCornerRadiusSpring,
+                )
+            }
+            GestureState.COMMITTED -> {
+                mView.setSpring(
+                        arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
+                        arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
+                        scale = params.committedIndicator.scaleSpring,
+                        backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
+                        backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
+                        backgroundEdgeCornerRadius = params.committedIndicator.backgroundDimens
+                                .edgeCornerRadiusSpring,
+                        backgroundFarCornerRadius = params.committedIndicator.backgroundDimens
+                                .farCornerRadiusSpring,
+                )
+            }
+            else -> {}
         }
+
         mView.setRestingDimens(
-            restingParams = EdgePanelParams.BackIndicatorDimens(
-                horizontalTranslation = when (currentState) {
-                    GestureState.GONE -> -params.activeIndicator.backgroundDimens.width
-                    // Position the committed arrow slightly further off the screen so we  do not
-                    // see part of it bouncing
-                    GestureState.COMMITTED ->
-                        -params.activeIndicator.backgroundDimens.width * 1.5f
-                    GestureState.FLUNG -> params.fullyStretchedIndicator.horizontalTranslation
-                    GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation
-                    GestureState.ENTRY, GestureState.INACTIVE, GestureState.CANCELLED ->
-                        params.entryIndicator.horizontalTranslation
-                },
-                arrowDimens = when (currentState) {
-                    GestureState.ACTIVE, GestureState.INACTIVE,
-                    GestureState.COMMITTED, GestureState.FLUNG -> params.activeIndicator.arrowDimens
-                    GestureState.CANCELLED -> params.cancelledArrowDimens
-                    GestureState.GONE, GestureState.ENTRY -> params.entryIndicator.arrowDimens
-                },
-                backgroundDimens = when (currentState) {
-                    GestureState.GONE, GestureState.ENTRY -> params.entryIndicator.backgroundDimens
-                    else ->
-                        params.activeIndicator.backgroundDimens.copy(
-                            edgeCornerRadius =
-                            if (currentState == GestureState.INACTIVE ||
-                                currentState == GestureState.CANCELLED
-                            )
-                                params.cancelledEdgeCornerRadius
-                            else
-                                params.activeIndicator.backgroundDimens.edgeCornerRadius
-                        )
-                }
-            ),
-            animate = animated
+                animate = !(currentState == GestureState.FLUNG ||
+                        currentState == GestureState.COMMITTED),
+                restingParams = EdgePanelParams.BackIndicatorDimens(
+                        scale = when (currentState) {
+                            GestureState.ACTIVE,
+                            GestureState.FLUNG,
+                            -> params.activeIndicator.scale
+                            GestureState.COMMITTED -> params.committedIndicator.scale
+                            else -> params.preThresholdIndicator.scale
+                        },
+                        scalePivotX = when (currentState) {
+                            GestureState.GONE,
+                            GestureState.ENTRY,
+                            GestureState.INACTIVE,
+                            GestureState.CANCELLED -> params.preThresholdIndicator.scalePivotX
+                            else -> params.committedIndicator.scalePivotX
+                        },
+                        horizontalTranslation = when (currentState) {
+                            GestureState.GONE -> {
+                                params.activeIndicator.backgroundDimens.width?.times(-1)
+                            }
+                            GestureState.ENTRY,
+                            GestureState.INACTIVE -> params.entryIndicator.horizontalTranslation
+                            GestureState.FLUNG -> params.activeIndicator.horizontalTranslation
+                            GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation
+                            GestureState.CANCELLED -> {
+                                params.cancelledIndicator.horizontalTranslation
+                            }
+                            else -> null
+                        },
+                        arrowDimens = when (currentState) {
+                            GestureState.GONE,
+                            GestureState.ENTRY,
+                            GestureState.INACTIVE -> params.entryIndicator.arrowDimens
+                            GestureState.ACTIVE -> params.activeIndicator.arrowDimens
+                            GestureState.FLUNG,
+                            GestureState.COMMITTED -> params.committedIndicator.arrowDimens
+                            GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens
+                        },
+                        backgroundDimens = when (currentState) {
+                            GestureState.GONE,
+                            GestureState.ENTRY,
+                            GestureState.INACTIVE -> params.entryIndicator.backgroundDimens
+                            GestureState.ACTIVE -> params.activeIndicator.backgroundDimens
+                            GestureState.FLUNG -> params.activeIndicator.backgroundDimens
+                            GestureState.COMMITTED -> params.committedIndicator.backgroundDimens
+                            GestureState.CANCELLED -> params.cancelledIndicator.backgroundDimens
+                        }
+                )
         )
     }
 
@@ -628,42 +792,123 @@
     private fun updateArrowState(newState: GestureState, force: Boolean = false) {
         if (!force && currentState == newState) return
 
-        if (DEBUG) Log.d(TAG, "updateArrowState $currentState -> $newState")
         previousState = currentState
         currentState = newState
-        if (currentState == GestureState.GONE) {
-            mView.cancelAlphaAnimations()
-            mView.visibility = View.GONE
-        } else {
-            mView.visibility = View.VISIBLE
+
+        when (currentState) {
+            GestureState.CANCELLED -> {
+                backCallback.cancelBack()
+            }
+            GestureState.FLUNG,
+            GestureState.COMMITTED -> {
+                // When flung, trigger back immediately but don't fire again
+                // once state resolves to committed.
+                if (previousState != GestureState.FLUNG) backCallback.triggerBack()
+            }
+            GestureState.ENTRY,
+            GestureState.INACTIVE -> {
+                backCallback.setTriggerBack(false)
+            }
+            GestureState.ACTIVE -> {
+                backCallback.setTriggerBack(true)
+            }
+            GestureState.GONE -> { }
         }
 
         when (currentState) {
             // Transitioning to GONE never animates since the arrow is (presumably) already off the
             // screen
-            GestureState.GONE -> updateRestingArrowDimens(animated = false, currentState)
+            GestureState.GONE -> {
+                updateRestingArrowDimens()
+                mView.isVisible = false
+            }
             GestureState.ENTRY -> {
-                updateYPosition(startY)
-                updateRestingArrowDimens(animated = true, currentState)
+                mView.isVisible = true
+
+                updateRestingArrowDimens()
+                gestureEntryTime = SystemClock.uptimeMillis()
+                gestureInactiveOrEntryTime = SystemClock.uptimeMillis()
             }
             GestureState.ACTIVE -> {
-                updateRestingArrowDimens(animated = true, currentState)
-                // Vibrate the first time we transition to ACTIVE
-                if (!hasHapticPlayed) {
-                    hasHapticPlayed = true
-                    vibrationTime = SystemClock.uptimeMillis()
-                    vibratorHelper.vibrate(VibrationEffect.EFFECT_TICK)
+                previousXTranslationOnActiveOffset = previousXTranslation
+                gestureActiveTime = SystemClock.uptimeMillis()
+
+                updateRestingArrowDimens()
+
+                vibratorHelper.cancel()
+                mainHandler.postDelayed(10L) {
+                    vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT)
+                }
+
+                val startingVelocity = convertVelocityToSpringStartingVelocity(
+                    valueOnFastVelocity = 0f,
+                    valueOnSlowVelocity = if (previousState == GestureState.ENTRY) 2f else 4.5f
+                )
+
+                when (previousState) {
+                    GestureState.ENTRY,
+                    GestureState.INACTIVE -> {
+                        mView.popOffEdge(startingVelocity)
+                    }
+                    GestureState.COMMITTED -> {
+                        // if previous state was committed then this activation
+                        // was due to a quick second swipe. Don't pop the arrow this time
+                    }
+                    else -> { }
                 }
             }
+
             GestureState.INACTIVE -> {
-                updateRestingArrowDimens(animated = true, currentState)
+                gestureInactiveOrEntryTime = SystemClock.uptimeMillis()
+
+                val startingVelocity = convertVelocityToSpringStartingVelocity(
+                        valueOnFastVelocity = -1.05f,
+                        valueOnSlowVelocity = -1.50f
+                )
+                mView.popOffEdge(startingVelocity)
+
+                vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT)
+                updateRestingArrowDimens()
             }
-            GestureState.FLUNG -> playFlingBackAnimation()
-            GestureState.COMMITTED -> playCommitBackAnimation()
-            GestureState.CANCELLED -> playCancelBackAnimation()
+            GestureState.FLUNG -> {
+                mainHandler.postDelayed(POP_ON_FLING_DELAY) { mView.popScale(1.9f) }
+                playHorizontalAnimationThen(onEndSetCommittedStateListener)
+            }
+            GestureState.COMMITTED -> {
+                if (previousState == GestureState.FLUNG) {
+                    playAnimationThenSetGoneEnd()
+                } else {
+                    mView.popScale(3f)
+                    mainHandler.postDelayed(
+                            playAnimationThenSetGoneOnAlphaEnd,
+                            MIN_DURATION_COMMITTED_ANIMATION
+                    )
+                }
+            }
+            GestureState.CANCELLED -> {
+                val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry)
+                playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay)
+
+                params.arrowStrokeAlphaSpring.get(0f).takeIf { it.isNewState }?.let {
+                    mView.popArrowAlpha(0f, it.value)
+                }
+                mainHandler.postDelayed(10L) { vibratorHelper.cancel() }
+            }
         }
     }
 
+    private fun convertVelocityToSpringStartingVelocity(
+            valueOnFastVelocity: Float,
+            valueOnSlowVelocity: Float,
+    ): Float {
+        val factor = velocityTracker?.run {
+            computeCurrentVelocity(PX_PER_MS)
+            MathUtils.smoothStep(0f, 3f, abs(xVelocity))
+        } ?: valueOnFastVelocity
+
+        return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor)
+    }
+
     private fun scheduleFailsafe() {
         if (!ENABLE_FAILSAFE) return
         cancelFailsafe()
@@ -690,24 +935,24 @@
     init {
         if (DEBUG) mView.drawDebugInfo = { canvas ->
             val debugStrings = listOf(
-                "$currentState",
-                "startX=$startX",
-                "startY=$startY",
-                "xDelta=${"%.1f".format(totalTouchDelta)}",
-                "xTranslation=${"%.1f".format(previousXTranslation)}",
-                "pre=${"%.0f".format(preThresholdStretchProgress(previousXTranslation) * 100)}%",
-                "post=${"%.0f".format(fullScreenStretchProgress(previousXTranslation) * 100)}%"
+                    "$currentState",
+                    "startX=$startX",
+                    "startY=$startY",
+                    "xDelta=${"%.1f".format(totalTouchDelta)}",
+                    "xTranslation=${"%.1f".format(previousXTranslation)}",
+                    "pre=${"%.0f".format(staticThresholdProgress(previousXTranslation) * 100)}%",
+                    "post=${"%.0f".format(fullScreenProgress(previousXTranslation) * 100)}%"
             )
             val debugPaint = Paint().apply {
                 color = Color.WHITE
             }
             val debugInfoBottom = debugStrings.size * 32f + 4f
             canvas.drawRect(
-                4f,
-                4f,
-                canvas.width.toFloat(),
-                debugStrings.size * 32f + 4f,
-                debugPaint
+                    4f,
+                    4f,
+                    canvas.width.toFloat(),
+                    debugStrings.size * 32f + 4f,
+                    debugPaint
             )
             debugPaint.apply {
                 color = Color.BLACK
@@ -733,9 +978,71 @@
                 canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
             }
 
-            drawVerticalLine(x = params.swipeTriggerThreshold, color = Color.BLUE)
+            drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
+            drawVerticalLine(x = params.deactivationSwipeTriggerThreshold, color = Color.BLUE)
             drawVerticalLine(x = startX, color = Color.GREEN)
             drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
         }
     }
 }
+
+/**
+ * In addition to a typical step function which returns one or two
+ * values based on a threshold, `Step` also gracefully handles quick
+ * changes in input near the threshold value that would typically
+ * result in the output rapidly changing.
+ *
+ * In the context of Back arrow, the arrow's stroke opacity should
+ * always appear transparent or opaque. Using a typical Step function,
+ * this would resulting in a flickering appearance as the output would
+ * change rapidly. `Step` addresses this by moving the threshold after
+ * it is crossed so it cannot be easily crossed again with small changes
+ * in touch events.
+ */
+class Step<T>(
+        private val threshold: Float,
+        private val factor: Float = 1.1f,
+        private val postThreshold: T,
+        private val preThreshold: T
+) {
+
+    data class Value<T>(val value: T, val isNewState: Boolean)
+
+    private val lowerFactor = 2 - factor
+
+    private lateinit var startValue: Value<T>
+    private lateinit var previousValue: Value<T>
+    private var hasCrossedUpperBoundAtLeastOnce = false
+    private var progress: Float = 0f
+
+    init {
+        reset()
+    }
+
+    fun reset() {
+        hasCrossedUpperBoundAtLeastOnce = false
+        progress = 0f
+        startValue = Value(preThreshold, false)
+        previousValue = startValue
+    }
+
+    fun get(progress: Float): Value<T> {
+        this.progress = progress
+
+        val hasCrossedUpperBound = progress > threshold * factor
+        val hasCrossedLowerBound = progress > threshold * lowerFactor
+
+        return when {
+            hasCrossedUpperBound && !hasCrossedUpperBoundAtLeastOnce -> {
+                hasCrossedUpperBoundAtLeastOnce = true
+                Value(postThreshold, true)
+            }
+            hasCrossedLowerBound -> previousValue.copy(isNewState = false)
+            hasCrossedUpperBoundAtLeastOnce -> {
+                hasCrossedUpperBoundAtLeastOnce = false
+                Value(preThreshold, true)
+            }
+            else -> startValue
+        }.also { previousValue = it }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
index d56537b..0c00022 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgePanelParams.kt
@@ -1,52 +1,82 @@
 package com.android.systemui.navigationbar.gestural
 
 import android.content.res.Resources
+import android.util.TypedValue
+import androidx.core.animation.PathInterpolator
+import androidx.dynamicanimation.animation.SpringForce
 import com.android.systemui.R
 
 data class EdgePanelParams(private var resources: Resources) {
 
     data class ArrowDimens(
-        val length: Float = 0f,
-        val height: Float = 0f
+            val length: Float = 0f,
+            val height: Float = 0f,
+            val alpha: Float = 0f,
+            var alphaSpring: SpringForce? = null,
+            val heightSpring: SpringForce? = null,
+            val lengthSpring: SpringForce? = null,
     )
 
     data class BackgroundDimens(
-        val width: Float = 0f,
-        val height: Float = 0f,
-        val edgeCornerRadius: Float = 0f,
-        val farCornerRadius: Float = 0f
+            val width: Float? = 0f,
+            val height: Float = 0f,
+            val edgeCornerRadius: Float = 0f,
+            val farCornerRadius: Float = 0f,
+            val alpha: Float = 0f,
+            val widthSpring: SpringForce? = null,
+            val heightSpring: SpringForce? = null,
+            val farCornerRadiusSpring: SpringForce? = null,
+            val edgeCornerRadiusSpring: SpringForce? = null,
+            val alphaSpring: SpringForce? = null,
     )
 
     data class BackIndicatorDimens(
-        val horizontalTranslation: Float = 0f,
-        val arrowDimens: ArrowDimens = ArrowDimens(),
-        val backgroundDimens: BackgroundDimens = BackgroundDimens()
+            val horizontalTranslation: Float? = 0f,
+            val scale: Float = 0f,
+            val scalePivotX: Float = 0f,
+            val arrowDimens: ArrowDimens,
+            val backgroundDimens: BackgroundDimens,
+            val verticalTranslationSpring: SpringForce? = null,
+            val horizontalTranslationSpring: SpringForce? = null,
+            val scaleSpring: SpringForce? = null,
     )
 
-    var arrowThickness: Float = 0f
+    lateinit var entryIndicator: BackIndicatorDimens
         private set
-    var entryIndicator = BackIndicatorDimens()
+    lateinit var activeIndicator: BackIndicatorDimens
         private set
-    var activeIndicator = BackIndicatorDimens()
+    lateinit var cancelledIndicator: BackIndicatorDimens
         private set
-    var preThresholdIndicator = BackIndicatorDimens()
+    lateinit var flungIndicator: BackIndicatorDimens
         private set
-    var fullyStretchedIndicator = BackIndicatorDimens()
+    lateinit var committedIndicator: BackIndicatorDimens
         private set
-    var cancelledEdgeCornerRadius: Float = 0f
+    lateinit var preThresholdIndicator: BackIndicatorDimens
         private set
-    var cancelledArrowDimens = ArrowDimens()
+    lateinit var fullyStretchedIndicator: BackIndicatorDimens
+        private set
 
     // navigation bar edge constants
     var arrowPaddingEnd: Int = 0
         private set
+    var arrowThickness: Float = 0f
+        private set
+    lateinit var arrowStrokeAlphaSpring: Step<SpringForce>
+        private set
+    lateinit var arrowStrokeAlphaInterpolator: Step<Float>
+        private set
 
     // The closest to y
     var minArrowYPosition: Int = 0
         private set
     var fingerOffset: Int = 0
         private set
-    var swipeTriggerThreshold: Float = 0f
+    var staticTriggerThreshold: Float = 0f
+        private set
+    var reactivationTriggerThreshold: Float = 0f
+        private set
+    var deactivationSwipeTriggerThreshold: Float = 0f
+        get() = -field
         private set
     var swipeProgressThreshold: Float = 0f
         private set
@@ -55,6 +85,26 @@
     var minDeltaForSwitch: Int = 0
         private set
 
+    var minDragToStartAnimation: Float = 0f
+        private set
+
+    lateinit var entryWidthInterpolator: PathInterpolator
+        private set
+    lateinit var entryWidthTowardsEdgeInterpolator: PathInterpolator
+        private set
+    lateinit var activeWidthInterpolator: PathInterpolator
+        private set
+    lateinit var arrowAngleInterpolator: PathInterpolator
+        private set
+    lateinit var translationInterpolator: PathInterpolator
+        private set
+    lateinit var farCornerInterpolator: PathInterpolator
+        private set
+    lateinit var edgeCornerInterpolator: PathInterpolator
+        private set
+    lateinit var heightInterpolator: PathInterpolator
+        private set
+
     init {
         update(resources)
     }
@@ -63,6 +113,10 @@
         return resources.getDimension(id)
     }
 
+    private fun getDimenFloat(id: Int): Float {
+        return TypedValue().run { resources.getValue(id, this, true); float }
+    }
+
     private fun getPx(id: Int): Int {
         return resources.getDimensionPixelSize(id)
     }
@@ -73,72 +127,200 @@
         arrowPaddingEnd = getPx(R.dimen.navigation_edge_panel_padding)
         minArrowYPosition = getPx(R.dimen.navigation_edge_arrow_min_y)
         fingerOffset = getPx(R.dimen.navigation_edge_finger_offset)
-        swipeTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold)
+        staticTriggerThreshold = getDimen(R.dimen.navigation_edge_action_drag_threshold)
+        reactivationTriggerThreshold =
+                getDimen(R.dimen.navigation_edge_action_reactivation_drag_threshold)
+        deactivationSwipeTriggerThreshold =
+                getDimen(R.dimen.navigation_edge_action_deactivation_drag_threshold)
         swipeProgressThreshold = getDimen(R.dimen.navigation_edge_action_progress_threshold)
         minDeltaForSwitch = getPx(R.dimen.navigation_edge_minimum_x_delta_for_switch)
+        minDragToStartAnimation =
+                getDimen(R.dimen.navigation_edge_action_min_distance_to_start_animation)
+
+        entryWidthInterpolator = PathInterpolator(.19f, 1.27f, .71f, .86f)
+        entryWidthTowardsEdgeInterpolator = PathInterpolator(1f, -3f, 1f, 1.2f)
+        activeWidthInterpolator = PathInterpolator(.15f, .48f, .46f, .89f)
+        arrowAngleInterpolator = entryWidthInterpolator
+        translationInterpolator = PathInterpolator(0.2f, 1.0f, 1.0f, 1.0f)
+        farCornerInterpolator = PathInterpolator(.03f, .19f, .14f, 1.09f)
+        edgeCornerInterpolator = PathInterpolator(0f, 1.11f, .85f, .84f)
+        heightInterpolator = PathInterpolator(1f, .05f, .9f, -0.29f)
+
+        val showArrowOnProgressValue = .2f
+        val showArrowOnProgressValueFactor = 1.05f
+
+        val entryActiveHorizontalTranslationSpring = createSpring(675f, 0.8f)
+        val activeCommittedArrowLengthSpring = createSpring(1500f, 0.29f)
+        val activeCommittedArrowHeightSpring = createSpring(1500f, 0.29f)
+        val flungCommittedEdgeCornerSpring = createSpring(10000f, 1f)
+        val flungCommittedFarCornerSpring = createSpring(10000f, 1f)
+        val flungCommittedWidthSpring = createSpring(10000f, 1f)
+        val flungCommittedHeightSpring = createSpring(10000f, 1f)
 
         entryIndicator = BackIndicatorDimens(
-            horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin),
-            arrowDimens = ArrowDimens(
-                length = getDimen(R.dimen.navigation_edge_entry_arrow_length),
-                height = getDimen(R.dimen.navigation_edge_entry_arrow_height),
-            ),
-            backgroundDimens = BackgroundDimens(
-                width = getDimen(R.dimen.navigation_edge_entry_background_width),
-                height = getDimen(R.dimen.navigation_edge_entry_background_height),
-                edgeCornerRadius = getDimen(R.dimen.navigation_edge_entry_edge_corners),
-                farCornerRadius = getDimen(R.dimen.navigation_edge_entry_far_corners)
-            )
+                horizontalTranslation = getDimen(R.dimen.navigation_edge_entry_margin),
+                scale = getDimenFloat(R.dimen.navigation_edge_entry_scale),
+                scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+                horizontalTranslationSpring = entryActiveHorizontalTranslationSpring,
+                verticalTranslationSpring = createSpring(10000f, 0.9f),
+                scaleSpring = createSpring(120f, 0.8f),
+                arrowDimens = ArrowDimens(
+                        length = getDimen(R.dimen.navigation_edge_entry_arrow_length),
+                        height = getDimen(R.dimen.navigation_edge_entry_arrow_height),
+                        alpha = 0f,
+                        alphaSpring = createSpring(200f, 1f),
+                        lengthSpring = createSpring(600f, 0.4f),
+                        heightSpring = createSpring(600f, 0.4f),
+                ),
+                backgroundDimens = BackgroundDimens(
+                        alpha = 1f,
+                        width = getDimen(R.dimen.navigation_edge_entry_background_width),
+                        height = getDimen(R.dimen.navigation_edge_entry_background_height),
+                        edgeCornerRadius = getDimen(R.dimen.navigation_edge_entry_edge_corners),
+                        farCornerRadius = getDimen(R.dimen.navigation_edge_entry_far_corners),
+                        alphaSpring = createSpring(900f, 1f),
+                        widthSpring = createSpring(450f, 0.65f),
+                        heightSpring = createSpring(1500f, 0.45f),
+                        farCornerRadiusSpring = createSpring(300f, 0.5f),
+                        edgeCornerRadiusSpring = createSpring(150f, 0.5f),
+                )
         )
 
         activeIndicator = BackIndicatorDimens(
-            horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin),
-            arrowDimens = ArrowDimens(
-                length = getDimen(R.dimen.navigation_edge_active_arrow_length),
-                height = getDimen(R.dimen.navigation_edge_active_arrow_height),
-            ),
-            backgroundDimens = BackgroundDimens(
-                width = getDimen(R.dimen.navigation_edge_active_background_width),
-                height = getDimen(R.dimen.navigation_edge_active_background_height),
-                edgeCornerRadius = getDimen(R.dimen.navigation_edge_active_edge_corners),
-                farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners)
-
-            )
+                horizontalTranslation = getDimen(R.dimen.navigation_edge_active_margin),
+                scale = getDimenFloat(R.dimen.navigation_edge_active_scale),
+                horizontalTranslationSpring = entryActiveHorizontalTranslationSpring,
+                scaleSpring = createSpring(450f, 0.415f),
+                arrowDimens = ArrowDimens(
+                        length = getDimen(R.dimen.navigation_edge_active_arrow_length),
+                        height = getDimen(R.dimen.navigation_edge_active_arrow_height),
+                        alpha = 1f,
+                        lengthSpring = activeCommittedArrowLengthSpring,
+                        heightSpring = activeCommittedArrowHeightSpring,
+                ),
+                backgroundDimens = BackgroundDimens(
+                        alpha = 1f,
+                        width = getDimen(R.dimen.navigation_edge_active_background_width),
+                        height = getDimen(R.dimen.navigation_edge_active_background_height),
+                        edgeCornerRadius = getDimen(R.dimen.navigation_edge_active_edge_corners),
+                        farCornerRadius = getDimen(R.dimen.navigation_edge_active_far_corners),
+                        widthSpring = createSpring(375f, 0.675f),
+                        heightSpring = createSpring(10000f, 1f),
+                        edgeCornerRadiusSpring = createSpring(600f, 0.36f),
+                        farCornerRadiusSpring = createSpring(2500f, 0.855f),
+                )
         )
 
         preThresholdIndicator = BackIndicatorDimens(
-            horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin),
-            arrowDimens = ArrowDimens(
-                length = entryIndicator.arrowDimens.length,
-                height = entryIndicator.arrowDimens.height,
-            ),
-            backgroundDimens = BackgroundDimens(
-                width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
-                height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height),
-                edgeCornerRadius = getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
-                farCornerRadius = getDimen(R.dimen.navigation_edge_pre_threshold_far_corners)
-            )
+                horizontalTranslation = getDimen(R.dimen.navigation_edge_pre_threshold_margin),
+                scale = getDimenFloat(R.dimen.navigation_edge_pre_threshold_scale),
+                scalePivotX = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+                scaleSpring = createSpring(120f, 0.8f),
+                horizontalTranslationSpring = createSpring(6000f, 1f),
+                arrowDimens = ArrowDimens(
+                        length = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_length),
+                        height = getDimen(R.dimen.navigation_edge_pre_threshold_arrow_height),
+                        alpha = 1f,
+                        lengthSpring = createSpring(100f, 0.6f),
+                        heightSpring = createSpring(100f, 0.6f),
+                ),
+                backgroundDimens = BackgroundDimens(
+                        alpha = 1f,
+                        width = getDimen(R.dimen.navigation_edge_pre_threshold_background_width),
+                        height = getDimen(R.dimen.navigation_edge_pre_threshold_background_height),
+                        edgeCornerRadius =
+                                getDimen(R.dimen.navigation_edge_pre_threshold_edge_corners),
+                        farCornerRadius =
+                                getDimen(R.dimen.navigation_edge_pre_threshold_far_corners),
+                        widthSpring = createSpring(200f, 0.65f),
+                        heightSpring = createSpring(1500f, 0.45f),
+                        farCornerRadiusSpring = createSpring(200f, 1f),
+                        edgeCornerRadiusSpring = createSpring(150f, 0.5f),
+                )
+        )
+
+        committedIndicator = activeIndicator.copy(
+                horizontalTranslation = null,
+                arrowDimens = activeIndicator.arrowDimens.copy(
+                        lengthSpring = activeCommittedArrowLengthSpring,
+                        heightSpring = activeCommittedArrowHeightSpring,
+                ),
+                backgroundDimens = activeIndicator.backgroundDimens.copy(
+                        alpha = 0f,
+                        // explicitly set to null to preserve previous width upon state change
+                        width = null,
+                        widthSpring = flungCommittedWidthSpring,
+                        heightSpring = flungCommittedHeightSpring,
+                        edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring,
+                        farCornerRadiusSpring = flungCommittedFarCornerSpring,
+                ),
+                scale = 0.85f,
+                scaleSpring = createSpring(650f, 1f),
+        )
+
+        flungIndicator = committedIndicator.copy(
+                arrowDimens = committedIndicator.arrowDimens.copy(
+                        lengthSpring = createSpring(850f, 0.46f),
+                        heightSpring = createSpring(850f, 0.46f),
+                ),
+                backgroundDimens = committedIndicator.backgroundDimens.copy(
+                        widthSpring = flungCommittedWidthSpring,
+                        heightSpring = flungCommittedHeightSpring,
+                        edgeCornerRadiusSpring = flungCommittedEdgeCornerSpring,
+                        farCornerRadiusSpring = flungCommittedFarCornerSpring,
+                )
+        )
+
+        cancelledIndicator = entryIndicator.copy(
+                backgroundDimens = entryIndicator.backgroundDimens.copy(width = 0f)
         )
 
         fullyStretchedIndicator = BackIndicatorDimens(
-            horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin),
-            arrowDimens = ArrowDimens(
-                length = getDimen(R.dimen.navigation_edge_stretched_arrow_length),
-                height = getDimen(R.dimen.navigation_edge_stretched_arrow_height),
-            ),
-            backgroundDimens = BackgroundDimens(
-                width = getDimen(R.dimen.navigation_edge_stretch_background_width),
-                height = getDimen(R.dimen.navigation_edge_stretch_background_height),
-                edgeCornerRadius = getDimen(R.dimen.navigation_edge_stretch_edge_corners),
-                farCornerRadius = getDimen(R.dimen.navigation_edge_stretch_far_corners)
+                horizontalTranslation = getDimen(R.dimen.navigation_edge_stretch_margin),
+                scale = getDimenFloat(R.dimen.navigation_edge_stretch_scale),
+                horizontalTranslationSpring = null,
+                verticalTranslationSpring = null,
+                scaleSpring = null,
+                arrowDimens = ArrowDimens(
+                        length = getDimen(R.dimen.navigation_edge_stretched_arrow_length),
+                        height = getDimen(R.dimen.navigation_edge_stretched_arrow_height),
+                        alpha = 1f,
+                        alphaSpring = null,
+                        heightSpring = null,
+                        lengthSpring = null,
+                ),
+                backgroundDimens = BackgroundDimens(
+                        alpha = 1f,
+                        width = getDimen(R.dimen.navigation_edge_stretch_background_width),
+                        height = getDimen(R.dimen.navigation_edge_stretch_background_height),
+                        edgeCornerRadius = getDimen(R.dimen.navigation_edge_stretch_edge_corners),
+                        farCornerRadius = getDimen(R.dimen.navigation_edge_stretch_far_corners),
+                        alphaSpring = null,
+                        widthSpring = null,
+                        heightSpring = null,
+                        edgeCornerRadiusSpring = null,
+                        farCornerRadiusSpring = null,
+                )
+        )
+
+        arrowStrokeAlphaInterpolator = Step(
+                threshold = showArrowOnProgressValue,
+                factor = showArrowOnProgressValueFactor,
+                postThreshold = 1f,
+                preThreshold = 0f
+        )
+
+        entryIndicator.arrowDimens.alphaSpring?.let { alphaSpring ->
+            arrowStrokeAlphaSpring = Step(
+                    threshold = showArrowOnProgressValue,
+                    factor = showArrowOnProgressValueFactor,
+                    postThreshold = alphaSpring,
+                    preThreshold = SpringForce().setStiffness(2000f).setDampingRatio(1f)
             )
-        )
-
-        cancelledEdgeCornerRadius = getDimen(R.dimen.navigation_edge_cancelled_edge_corners)
-
-        cancelledArrowDimens = ArrowDimens(
-            length = getDimen(R.dimen.navigation_edge_cancelled_arrow_length),
-            height = getDimen(R.dimen.navigation_edge_cancelled_arrow_height)
-        )
+        }
     }
 }
+
+fun createSpring(stiffness: Float, dampingRatio: Float): SpringForce {
+    return SpringForce().setStiffness(stiffness).setDampingRatio(dampingRatio)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index 64a8a14..ad00069 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -171,8 +171,9 @@
             getHost().collapsePanels();
         };
 
-        Dialog dialog = mController.createScreenRecordDialog(mContext, mFlags,
+        final Dialog dialog = mController.createScreenRecordDialog(mContext, mFlags,
                 mDialogLaunchAnimator, mActivityStarter, onStartRecordingClicked);
+
         ActivityStarter.OnDismissAction dismissAction = () -> {
             if (shouldAnimateFromView) {
                 mDialogLaunchAnimator.showFromView(dialog, view, new DialogCuj(
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index 431b28f..acb6d96 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -38,6 +38,8 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserTracker;
@@ -48,6 +50,8 @@
 
 import javax.inject.Inject;
 
+import dagger.Lazy;
+
 /**
  * Helper class to initiate a screen recording
  */
@@ -63,6 +67,8 @@
     private CountDownTimer mCountDownTimer = null;
     private final Executor mMainExecutor;
     private final BroadcastDispatcher mBroadcastDispatcher;
+    private final Context mContext;
+    private final FeatureFlags mFlags;
     private final UserContextProvider mUserContextProvider;
     private final UserTracker mUserTracker;
 
@@ -73,6 +79,8 @@
     private CopyOnWriteArrayList<RecordingStateChangeCallback> mListeners =
             new CopyOnWriteArrayList<>();
 
+    private final Lazy<ScreenCaptureDevicePolicyResolver> mDevicePolicyResolver;
+
     @VisibleForTesting
     final UserTracker.Callback mUserChangedCallback =
             new UserTracker.Callback() {
@@ -103,9 +111,15 @@
     @Inject
     public RecordingController(@Main Executor mainExecutor,
             BroadcastDispatcher broadcastDispatcher,
+            Context context,
+            FeatureFlags flags,
             UserContextProvider userContextProvider,
+            Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver,
             UserTracker userTracker) {
         mMainExecutor = mainExecutor;
+        mContext = context;
+        mFlags = flags;
+        mDevicePolicyResolver = devicePolicyResolver;
         mBroadcastDispatcher = broadcastDispatcher;
         mUserContextProvider = userContextProvider;
         mUserTracker = userTracker;
@@ -115,14 +129,30 @@
         mInteractiveBroadcastOption = options.toBundle();
     }
 
-    /** Create a dialog to show screen recording options to the user. */
+    /**
+     * MediaProjection host is SystemUI for the screen recorder, so return 'my user handle'
+     */
+    private UserHandle getHostUserHandle() {
+        return UserHandle.of(UserHandle.myUserId());
+    }
+
+    /** Create a dialog to show screen recording options to the user.
+     *  If screen capturing is currently not allowed it will return a dialog
+     *  that warns users about it. */
     public Dialog createScreenRecordDialog(Context context, FeatureFlags flags,
                                            DialogLaunchAnimator dialogLaunchAnimator,
                                            ActivityStarter activityStarter,
                                            @Nullable Runnable onStartRecordingClicked) {
+        if (mFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)
+                && mDevicePolicyResolver.get()
+                        .isScreenCaptureCompletelyDisabled(getHostUserHandle())) {
+            return new ScreenCaptureDisabledDialog(mContext);
+        }
+
         return flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING)
-                ? new ScreenRecordPermissionDialog(context, this, activityStarter,
-                        dialogLaunchAnimator, mUserContextProvider, onStartRecordingClicked)
+                ? new ScreenRecordPermissionDialog(context,  getHostUserHandle(), this,
+                    activityStarter, dialogLaunchAnimator, mUserContextProvider,
+                    onStartRecordingClicked)
                 : new ScreenRecordDialog(context, this, activityStarter,
                 mUserContextProvider, flags, dialogLaunchAnimator, onStartRecordingClicked);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
index 68e3dcd..dd21be9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialog.kt
@@ -42,6 +42,7 @@
 /** Dialog to select screen recording options */
 class ScreenRecordPermissionDialog(
     context: Context?,
+    private val hostUserHandle: UserHandle,
     private val controller: RecordingController,
     private val activityStarter: ActivityStarter,
     private val dialogLaunchAnimator: DialogLaunchAnimator,
@@ -79,11 +80,9 @@
                     CaptureTargetResultReceiver()
                 )
 
-                // Send SystemUI's user handle as the host app user handle because SystemUI
-                // is the 'host app' (the app that receives screen capture data)
                 intent.putExtra(
                     MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
-                    UserHandle.of(UserHandle.myUserId())
+                    hostUserHandle
                 )
 
                 val animationController = dialogLaunchAnimator.createActivityLaunchController(v!!)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 2ac7f7a..6ee0a46 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -6135,6 +6135,11 @@
 
             switch (event.getActionMasked()) {
                 case MotionEvent.ACTION_DOWN:
+                    if (mTracking) {
+                        // TODO(b/247126247) fix underlying issue. Should be ACTION_POINTER_DOWN.
+                        mShadeLog.d("Don't intercept down event while already tracking");
+                        return false;
+                    }
                     mCentralSurfaces.userActivity();
                     mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation;
                     mMinExpandHeight = 0.0f;
@@ -6222,6 +6227,11 @@
                             "onTouch: duplicate down event detected... ignoring");
                     return true;
                 }
+                if (mTracking) {
+                    // TODO(b/247126247) fix underlying issue. Should be ACTION_POINTER_DOWN.
+                    mShadeLog.d("Don't handle down event while already tracking");
+                    return true;
+                }
                 mLastTouchDownTime = event.getDownTime();
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 02d0f94..fe76c7d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -35,6 +35,7 @@
 import android.view.ContextThemeWrapper
 import android.view.View
 import android.view.ViewGroup
+import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.settingslib.Utils
 import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
@@ -83,6 +84,7 @@
         private val statusBarStateController: StatusBarStateController,
         private val deviceProvisionedController: DeviceProvisionedController,
         private val bypassController: KeyguardBypassController,
+        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
         private val execution: Execution,
         @Main private val uiExecutor: Executor,
         @Background private val bgExecutor: Executor,
@@ -176,10 +178,10 @@
         }
         if (weatherTarget != null) {
             val weatherData = Weather.fromBundle(weatherTarget.baseAction.extras)
+            keyguardUpdateMonitor.sendWeatherData(weatherData)
         }
     }
 
-
     private val userTrackerCallback = object : UserTracker.Callback {
         override fun onUserChanged(newUser: Int, userContext: Context) {
             execution.assertIsMainThread()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index 5f6a5cb..26f97de 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -63,7 +63,7 @@
  * are not.
  */
 public class NotificationLogger implements StateListener {
-    private static final String TAG = "NotificationLogger";
+    static final String TAG = "NotificationLogger";
     private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
 
     /** The minimum delay in ms between reports of notification visibility. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
index ec8501a..cc1103d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.statusbar.notification.logging
 
 import android.app.StatsManager
+import android.util.Log
 import android.util.StatsEvent
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -25,6 +26,7 @@
 import com.android.systemui.shared.system.SysUiStatsLog
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.util.traceSection
+import java.lang.Exception
 import java.util.concurrent.Executor
 import javax.inject.Inject
 import kotlin.math.roundToInt
@@ -82,43 +84,56 @@
                 return StatsManager.PULL_SKIP
             }
 
-            // Notifications can only be retrieved on the main thread, so switch to that thread.
-            val notifications = getAllNotificationsOnMainThread()
-            val notificationMemoryUse =
-                NotificationMemoryMeter.notificationMemoryUse(notifications)
-                    .sortedWith(
-                        compareBy(
-                            { it.packageName },
-                            { it.objectUsage.style },
-                            { it.notificationKey }
+            try {
+                // Notifications can only be retrieved on the main thread, so switch to that thread.
+                val notifications = getAllNotificationsOnMainThread()
+                val notificationMemoryUse =
+                    NotificationMemoryMeter.notificationMemoryUse(notifications)
+                        .sortedWith(
+                            compareBy(
+                                { it.packageName },
+                                { it.objectUsage.style },
+                                { it.notificationKey }
+                            )
+                        )
+                val usageData = aggregateMemoryUsageData(notificationMemoryUse)
+                usageData.forEach { (_, use) ->
+                    data.add(
+                        SysUiStatsLog.buildStatsEvent(
+                            SysUiStatsLog.NOTIFICATION_MEMORY_USE,
+                            use.uid,
+                            use.style,
+                            use.count,
+                            use.countWithInflatedViews,
+                            toKb(use.smallIconObject),
+                            use.smallIconBitmapCount,
+                            toKb(use.largeIconObject),
+                            use.largeIconBitmapCount,
+                            toKb(use.bigPictureObject),
+                            use.bigPictureBitmapCount,
+                            toKb(use.extras),
+                            toKb(use.extenders),
+                            toKb(use.smallIconViews),
+                            toKb(use.largeIconViews),
+                            toKb(use.systemIconViews),
+                            toKb(use.styleViews),
+                            toKb(use.customViews),
+                            toKb(use.softwareBitmaps),
+                            use.seenCount
                         )
                     )
-            val usageData = aggregateMemoryUsageData(notificationMemoryUse)
-            usageData.forEach { (_, use) ->
-                data.add(
-                    SysUiStatsLog.buildStatsEvent(
-                        SysUiStatsLog.NOTIFICATION_MEMORY_USE,
-                        use.uid,
-                        use.style,
-                        use.count,
-                        use.countWithInflatedViews,
-                        toKb(use.smallIconObject),
-                        use.smallIconBitmapCount,
-                        toKb(use.largeIconObject),
-                        use.largeIconBitmapCount,
-                        toKb(use.bigPictureObject),
-                        use.bigPictureBitmapCount,
-                        toKb(use.extras),
-                        toKb(use.extenders),
-                        toKb(use.smallIconViews),
-                        toKb(use.largeIconViews),
-                        toKb(use.systemIconViews),
-                        toKb(use.styleViews),
-                        toKb(use.customViews),
-                        toKb(use.softwareBitmaps),
-                        use.seenCount
-                    )
-                )
+                }
+            } catch (e: InterruptedException) {
+                // This can happen if the device is sleeping or view walking takes too long.
+                // The statsd collector will interrupt the thread and we need to handle it
+                // gracefully.
+                Log.w(NotificationLogger.TAG, "Timed out when measuring notification memory.", e)
+                return@traceSection StatsManager.PULL_SKIP
+            } catch (e: Exception) {
+                // Error while collecting data, this should not crash prod SysUI. Just
+                // log WTF and move on.
+                Log.wtf(NotificationLogger.TAG, "Failed to measure notification memory.", e)
+                return@traceSection StatsManager.PULL_SKIP
             }
 
             return StatsManager.PULL_SUCCESS
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
index 2d04211..6491223 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt
@@ -184,19 +184,21 @@
     private fun computeDrawableUse(drawable: Drawable, seenObjects: HashSet<Int>): Int =
         when (drawable) {
             is BitmapDrawable -> {
-                val ref = System.identityHashCode(drawable.bitmap)
-                if (seenObjects.contains(ref)) {
-                    0
-                } else {
-                    seenObjects.add(ref)
-                    drawable.bitmap.allocationByteCount
-                }
+                drawable.bitmap?.let {
+                    val ref = System.identityHashCode(it)
+                    if (seenObjects.contains(ref)) {
+                        0
+                    } else {
+                        seenObjects.add(ref)
+                        it.allocationByteCount
+                    }
+                } ?: 0
             }
             else -> 0
         }
 
     private fun isDrawableSoftwareBitmap(drawable: Drawable) =
-        drawable is BitmapDrawable && drawable.bitmap.config != Bitmap.Config.HARDWARE
+        drawable is BitmapDrawable && drawable.bitmap?.config != Bitmap.Config.HARDWARE
 
     private fun identifierForView(view: View) =
         if (view.id == View.NO_ID) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index 01af486..c163a89 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -89,14 +89,19 @@
     private float mPanelExpansion;
 
     /**
-     * Burn-in prevention x translation.
+     * Max burn-in prevention x translation.
      */
-    private int mBurnInPreventionOffsetX;
+    private int mMaxBurnInPreventionOffsetX;
 
     /**
-     * Burn-in prevention y translation for clock layouts.
+     * Max burn-in prevention y translation for clock layouts.
      */
-    private int mBurnInPreventionOffsetYClock;
+    private int mMaxBurnInPreventionOffsetYClock;
+
+    /**
+     * Current burn-in prevention y translation.
+     */
+    private float mCurrentBurnInOffsetY;
 
     /**
      * Doze/AOD transition amount.
@@ -155,9 +160,9 @@
 
         mContainerTopPadding =
                 res.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
-        mBurnInPreventionOffsetX = res.getDimensionPixelSize(
+        mMaxBurnInPreventionOffsetX = res.getDimensionPixelSize(
                 R.dimen.burn_in_prevention_offset_x);
-        mBurnInPreventionOffsetYClock = res.getDimensionPixelSize(
+        mMaxBurnInPreventionOffsetYClock = res.getDimensionPixelSize(
                 R.dimen.burn_in_prevention_offset_y_clock);
     }
 
@@ -215,7 +220,10 @@
         if (mBypassEnabled) {
             return (int) (mUnlockedStackScrollerPadding + mOverStretchAmount);
         } else if (mIsSplitShade) {
-            return clockYPosition - mSplitShadeTopNotificationsMargin + mUserSwitchHeight;
+            // mCurrentBurnInOffsetY is subtracted to make notifications not follow clock adjustment
+            // for burn-in. It can make pulsing notification go too high and it will get clipped
+            return clockYPosition - mSplitShadeTopNotificationsMargin + mUserSwitchHeight
+                    - (int) mCurrentBurnInOffsetY;
         } else {
             return clockYPosition + mKeyguardStatusHeight;
         }
@@ -255,11 +263,11 @@
 
         // This will keep the clock at the top but out of the cutout area
         float shift = 0;
-        if (clockY - mBurnInPreventionOffsetYClock < mCutoutTopInset) {
-            shift = mCutoutTopInset - (clockY - mBurnInPreventionOffsetYClock);
+        if (clockY - mMaxBurnInPreventionOffsetYClock < mCutoutTopInset) {
+            shift = mCutoutTopInset - (clockY - mMaxBurnInPreventionOffsetYClock);
         }
 
-        int burnInPreventionOffsetY = mBurnInPreventionOffsetYClock; // requested offset
+        int burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock; // requested offset
         final boolean hasUdfps = mUdfpsTop > -1;
         if (hasUdfps && !mIsClockTopAligned) {
             // ensure clock doesn't overlap with the udfps icon
@@ -267,8 +275,8 @@
                 // sometimes the clock textView extends beyond udfps, so let's just use the
                 // space above the KeyguardStatusView/clock as our burn-in offset
                 burnInPreventionOffsetY = (int) (clockY - mCutoutTopInset) / 2;
-                if (mBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
-                    burnInPreventionOffsetY = mBurnInPreventionOffsetYClock;
+                if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
+                    burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock;
                 }
                 shift = -burnInPreventionOffsetY;
             } else {
@@ -276,16 +284,18 @@
                 float lowerSpace = mUdfpsTop - mClockBottom;
                 // center the burn-in offset within the upper + lower space
                 burnInPreventionOffsetY = (int) (lowerSpace + upperSpace) / 2;
-                if (mBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
-                    burnInPreventionOffsetY = mBurnInPreventionOffsetYClock;
+                if (mMaxBurnInPreventionOffsetYClock < burnInPreventionOffsetY) {
+                    burnInPreventionOffsetY = mMaxBurnInPreventionOffsetYClock;
                 }
                 shift = (lowerSpace - upperSpace) / 2;
             }
         }
 
+        float fullyDarkBurnInOffset = burnInPreventionOffsetY(burnInPreventionOffsetY);
         float clockYDark = clockY
-                + burnInPreventionOffsetY(burnInPreventionOffsetY)
+                + fullyDarkBurnInOffset
                 + shift;
+        mCurrentBurnInOffsetY = MathUtils.lerp(0, fullyDarkBurnInOffset, darkAmount);
         return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mOverStretchAmount);
     }
 
@@ -325,7 +335,7 @@
     }
 
     private float burnInPreventionOffsetX() {
-        return getBurnInOffset(mBurnInPreventionOffsetX, true /* xAxis */);
+        return getBurnInOffset(mMaxBurnInPreventionOffsetX, true /* xAxis */);
     }
 
     public static class Result {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index d912793..082c8cc 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
@@ -134,4 +134,10 @@
     keyguardPasswordViewController.startAppearAnimation()
     verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean())
   }
+
+  @Test
+  fun testMessageIsSetWhenReset() {
+    keyguardPasswordViewController.resetState()
+    verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password)
+  }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index b742100..0881e61 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -120,4 +120,10 @@
     public void testGetInitialMessageResId() {
         assertThat(mKeyguardPinViewController.getInitialMessageResId()).isNotEqualTo(0);
     }
+
+    @Test
+    public void testMessageIsSetWhenReset() {
+        mKeyguardPinViewController.resetState();
+        verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 075ef9d..0d65f12 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -583,6 +583,8 @@
         verify(mKeyguardSecurityViewFlipperController).clearViews();
         verify(mKeyguardSecurityViewFlipperController).getSecurityView(any(SecurityMode.class),
                 any(KeyguardSecurityCallback.class));
+        verify(mView).reset();
+        verify(mKeyguardSecurityViewFlipperController).reset();
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
index fc11148..4cf5a4b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java
@@ -34,8 +34,10 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -104,8 +106,12 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -119,7 +125,7 @@
     private WindowManager mWindowManager;
     private DisplayManager mDisplayManager;
     private SecureSettings mSecureSettings;
-    private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+    private FakeExecutor mExecutor;
     private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
     private FakeThreadFactory mThreadFactory;
     private ArrayList<DecorProvider> mPrivacyDecorProviders;
@@ -161,6 +167,8 @@
     private PrivacyDotViewController.ShowingListener mPrivacyDotShowingListener;
     @Mock
     private CutoutDecorProviderFactory mCutoutFactory;
+    @Captor
+    private ArgumentCaptor<AuthController.Callback> mAuthControllerCallback;
     private List<DecorProvider> mMockCutoutList;
 
     @Before
@@ -169,6 +177,7 @@
 
         Handler mainHandler = new Handler(TestableLooper.get(this).getLooper());
         mSecureSettings = new FakeSettings();
+        mExecutor = new FakeExecutor(new FakeSystemClock());
         mThreadFactory = new FakeThreadFactory(mExecutor);
         mThreadFactory.setHandler(mainHandler);
 
@@ -1166,6 +1175,44 @@
     }
 
     @Test
+    public void faceSensorLocationChangesReloadsFaceScanningOverlay() {
+        mFaceScanningProviders = new ArrayList<>();
+        mFaceScanningProviders.add(mFaceScanningDecorProvider);
+        when(mFaceScanningProviderFactory.getProviders()).thenReturn(mFaceScanningProviders);
+        when(mFaceScanningProviderFactory.getHasProviders()).thenReturn(true);
+        ScreenDecorations screenDecorations = new ScreenDecorations(mContext, mExecutor,
+                mSecureSettings, mTunerService, mUserTracker, mDisplayTracker, mDotViewController,
+                mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory,
+                new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mAuthController);
+        screenDecorations.start();
+        verify(mAuthController).addCallback(mAuthControllerCallback.capture());
+        when(mContext.getDisplay()).thenReturn(mDisplay);
+        when(mDisplay.getDisplayInfo(any())).thenAnswer(new Answer<Boolean>() {
+            @Override
+            public Boolean answer(InvocationOnMock invocation) throws Throwable {
+                DisplayInfo displayInfo = invocation.getArgument(0);
+                int modeId = 1;
+                displayInfo.modeId = modeId;
+                displayInfo.supportedModes = new Display.Mode[]{new Display.Mode(modeId, 1024, 1024,
+                        90)};
+                return false;
+            }
+        });
+        mExecutor.runAllReady();
+        clearInvocations(mFaceScanningDecorProvider);
+
+        AuthController.Callback callback = mAuthControllerCallback.getValue();
+        callback.onFaceSensorLocationChanged();
+        mExecutor.runAllReady();
+
+        verify(mFaceScanningDecorProvider).onReloadResAndMeasure(any(),
+                anyInt(),
+                anyInt(),
+                anyInt(),
+                any());
+    }
+
+    @Test
     public void testPrivacyDotShowingListenerWorkWellWithNullParameter() {
         mPrivacyDotShowingListener.onPrivacyDotShown(null);
         mPrivacyDotShowingListener.onPrivacyDotHidden(null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 5afe49e..0d00e8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -85,6 +85,7 @@
 import com.android.internal.R;
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.settingslib.udfps.UdfpsUtils;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.domain.interactor.BiometricPromptCredentialInteractor;
 import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
@@ -167,6 +168,8 @@
     private BiometricPromptCredentialInteractor mBiometricPromptCredentialInteractor;
     @Mock
     private CredentialViewModel mCredentialViewModel;
+    @Mock
+    private UdfpsUtils mUdfpsUtils;
 
     @Captor
     private ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mFpAuthenticatorsRegisteredCaptor;
@@ -958,7 +961,7 @@
                     mPanelInteractionDetector, mUserManager, mLockPatternUtils, mUdfpsLogger,
                     mLogContextInteractor, () -> mBiometricPromptCredentialInteractor,
                     () -> mCredentialViewModel, mInteractionJankMonitor, mHandler,
-                    mBackgroundExecutor, mVibratorHelper);
+                    mBackgroundExecutor, mVibratorHelper, mUdfpsUtils);
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 36ed6d5..9866163 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -38,6 +38,8 @@
 import android.view.accessibility.AccessibilityManager
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.settingslib.udfps.UdfpsOverlayParams
+import com.android.settingslib.udfps.UdfpsUtils
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
@@ -110,6 +112,7 @@
     @Mock private lateinit var featureFlags: FeatureFlags
     @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
     @Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
+    @Mock private lateinit var udfpsUtils: UdfpsUtils
     @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
 
     private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
@@ -144,7 +147,7 @@
             configurationController, keyguardStateController, unlockedScreenOffAnimationController,
             udfpsDisplayMode, secureSettings, REQUEST_ID, reason,
             controllerCallback, onTouch, activityLaunchAnimator, featureFlags,
-            primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable
+            primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable, udfpsUtils
         )
         block()
     }
@@ -400,109 +403,6 @@
         assertThat(controllerOverlay.matchesRequestId(REQUEST_ID)).isTrue()
         assertThat(controllerOverlay.matchesRequestId(REQUEST_ID + 1)).isFalse()
     }
-
-    @Test
-    fun testTouchOutsideAreaNoRotation() = withReason(REASON_ENROLL_ENROLLING) {
-        val touchHints =
-            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
-        val rotation = Surface.ROTATION_0
-        // touch at 0 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[0])
-        // touch at 90 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[1])
-        // touch at 180 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                -1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[2])
-        // touch at 270 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[3])
-    }
-
-    fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) {
-        val touchHints =
-            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
-        val rotation = Surface.ROTATION_90
-        // touch at 0 degrees -> 90 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[1])
-        // touch at 90 degrees -> 180 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[2])
-        // touch at 180 degrees -> 270 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                -1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[3])
-        // touch at 270 degrees -> 0 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[0])
-    }
-
-    fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) {
-        val touchHints =
-            context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
-        val rotation = Surface.ROTATION_270
-        // touch at 0 degrees -> 270 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                0.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[3])
-        // touch at 90 degrees -> 0 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                0.0f /* x */, -1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[0])
-        // touch at 180 degrees -> 90 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                -1.0f /* x */, 0.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[1])
-        // touch at 270 degrees -> 180 degrees
-        assertThat(
-            controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-                0.0f /* x */, 1.0f /* y */,
-                0.0f /* sensorX */, 0.0f /* sensorY */, rotation
-            )
-        ).isEqualTo(touchHints[2])
-    }
 }
 
 private class EnrollListener(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index dd7082a..17c262d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -71,6 +71,8 @@
 import com.android.internal.logging.InstanceIdSequence;
 import com.android.internal.util.LatencyTracker;
 import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.settingslib.udfps.UdfpsOverlayParams;
+import com.android.settingslib.udfps.UdfpsUtils;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.ActivityLaunchAnimator;
@@ -230,10 +232,12 @@
     private ScreenLifecycle.Observer mScreenObserver;
     private FingerprintSensorPropertiesInternal mOpticalProps;
     private FingerprintSensorPropertiesInternal mUltrasonicProps;
+    private UdfpsUtils mUdfpsUtils;
 
     @Before
     public void setUp() {
         Execution execution = new FakeExecution();
+        mUdfpsUtils = new UdfpsUtils();
 
         when(mLayoutInflater.inflate(R.layout.udfps_view, null, false))
                 .thenReturn(mUdfpsView);
@@ -305,7 +309,7 @@
                 mUnlockedScreenOffAnimationController, mSystemUIDialogManager, mLatencyTracker,
                 mActivityLaunchAnimator, alternateTouchProvider, mBiometricExecutor,
                 mPrimaryBouncerInteractor, mSinglePointerTouchProcessor, mSessionTracker,
-                mAlternateBouncerInteractor, mSecureSettings);
+                mAlternateBouncerInteractor, mSecureSettings, mUdfpsUtils);
         verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
         mOverlayController = mOverlayCaptor.getValue();
         verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
index 44fa4eb..07b4a64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
@@ -25,6 +25,7 @@
 import android.view.LayoutInflater
 import android.view.Surface
 import androidx.test.filters.SmallTest
+import com.android.settingslib.udfps.UdfpsOverlayParams
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.any
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
index 8e20303..c40fd4fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt
@@ -23,8 +23,8 @@
 import android.view.Surface
 import android.view.Surface.Rotation
 import androidx.test.filters.SmallTest
+import com.android.settingslib.udfps.UdfpsOverlayParams
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.UdfpsOverlayParams
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index 8127ccc..6e6833d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -16,13 +16,15 @@
 
 package com.android.systemui.screenrecord;
 
+import static com.google.common.truth.Truth.assertThat;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
-
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
+import android.app.Dialog;
 import android.app.PendingIntent;
 import android.content.Intent;
 import android.os.Looper;
@@ -31,7 +33,13 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
+import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog;
+import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.util.concurrency.FakeExecutor;
@@ -61,8 +69,15 @@
     @Mock
     private UserContextProvider mUserContextProvider;
     @Mock
+    private ScreenCaptureDevicePolicyResolver mDevicePolicyResolver;
+    @Mock
+    private DialogLaunchAnimator mDialogLaunchAnimator;
+    @Mock
+    private ActivityStarter mActivityStarter;
+    @Mock
     private UserTracker mUserTracker;
 
+    private FakeFeatureFlags mFeatureFlags;
     private RecordingController mController;
 
     private static final int USER_ID = 10;
@@ -70,8 +85,9 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mController = new RecordingController(mMainExecutor, mBroadcastDispatcher,
-                mUserContextProvider, mUserTracker);
+        mFeatureFlags = new FakeFeatureFlags();
+        mController = new RecordingController(mMainExecutor, mBroadcastDispatcher, mContext,
+                mFeatureFlags, mUserContextProvider, () -> mDevicePolicyResolver, mUserTracker);
         mController.addCallback(mCallback);
     }
 
@@ -190,4 +206,67 @@
         verify(mCallback).onRecordingEnd();
         assertFalse(mController.isRecording());
     }
+
+    @Test
+    public void testPoliciesFlagDisabled_screenCapturingNotAllowed_returnsNullDevicePolicyDialog() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, false);
+        when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(true);
+
+        Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+                mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+        assertThat(dialog).isInstanceOf(ScreenRecordPermissionDialog.class);
+    }
+
+    @Test
+    public void testPartialScreenSharingDisabled_returnsLegacyDialog() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, false);
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, false);
+
+        Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+                mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+        assertThat(dialog).isInstanceOf(ScreenRecordDialog.class);
+    }
+
+    @Test
+    public void testPoliciesFlagEnabled_screenCapturingNotAllowed_returnsDevicePolicyDialog() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true);
+        when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(true);
+
+        Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+                mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+        assertThat(dialog).isInstanceOf(ScreenCaptureDisabledDialog.class);
+    }
+
+    @Test
+    public void testPoliciesFlagEnabled_screenCapturingAllowed_returnsNullDevicePolicyDialog() {
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true);
+        mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true);
+        when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(false);
+
+        Dialog dialog = mController.createScreenRecordDialog(mContext, mFeatureFlags,
+                mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null);
+
+        assertThat(dialog).isInstanceOf(ScreenRecordPermissionDialog.class);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
index 0aa3621..5b094c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.screenrecord
 
+import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.View
@@ -59,6 +60,7 @@
         dialog =
             ScreenRecordPermissionDialog(
                 context,
+                UserHandle.of(0),
                 controller,
                 starter,
                 dialogLaunchAnimator,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 0a576de..9c69a6a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -32,6 +32,7 @@
 import android.view.View
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
@@ -104,6 +105,9 @@
     private lateinit var keyguardBypassController: KeyguardBypassController
 
     @Mock
+    private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+
+    @Mock
     private lateinit var deviceProvisionedController: DeviceProvisionedController
 
     @Mock
@@ -223,6 +227,7 @@
                 statusBarStateController,
                 deviceProvisionedController,
                 keyguardBypassController,
+                keyguardUpdateMonitor,
                 execution,
                 executor,
                 bgExecutor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
index 33b94e3..bd03903 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import java.lang.RuntimeException
 import kotlinx.coroutines.Dispatchers
 import org.junit.Before
 import org.junit.Test
@@ -113,6 +114,24 @@
         assertThat(data).hasSize(2)
     }
 
+    @Test
+    fun onPullAtom_throwsInterruptedException_failsGracefully() {
+        val pipeline: NotifPipeline = mock()
+        whenever(pipeline.allNotifs).thenAnswer { throw InterruptedException("Timeout") }
+        val logger = NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
+        assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf()))
+            .isEqualTo(StatsManager.PULL_SKIP)
+    }
+
+    @Test
+    fun onPullAtom_throwsRuntimeException_failsGracefully() {
+        val pipeline: NotifPipeline = mock()
+        whenever(pipeline.allNotifs).thenThrow(RuntimeException("Something broke!"))
+        val logger = NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor)
+        assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, mutableListOf()))
+            .isEqualTo(StatsManager.PULL_SKIP)
+    }
+
     private fun createLoggerWithNotifications(
         notifications: List<Notification>
     ): NotificationMemoryLogger {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
index ed3f710..7e69efa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
@@ -264,6 +264,19 @@
     }
 
     @Test
+    public void notifPositionAlignedWithClockAndBurnInOffsetInSplitShadeMode() {
+        setSplitShadeTopMargin(100); // this makes clock to be at 100
+        givenAOD();
+        mIsSplitShade = true;
+        givenMaxBurnInOffset(100);
+        givenHighestBurnInOffset(); // this makes clock to be at 200
+        // WHEN the position algorithm is run
+        positionClock();
+        // THEN the notif padding adjusts for burn-in offset: clock position - burn-in offset
+        assertThat(mClockPosition.stackScrollerPadding).isEqualTo(100);
+    }
+
+    @Test
     public void clockPositionedDependingOnMarginInSplitShade() {
         setSplitShadeTopMargin(400);
         givenLockScreen();
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
index 37069dc..595cdec 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java
@@ -52,6 +52,7 @@
 import android.view.animation.DecelerateInterpolator;
 
 import com.android.internal.R;
+import com.android.internal.accessibility.common.MagnificationConstants;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.function.pooled.PooledLambda;
@@ -1157,11 +1158,14 @@
     }
 
     /**
-     * Persists the default display magnification scale to the current user's settings.
+     * Persists the default display magnification scale to the current user's settings
+     * <strong>if scale is >= {@link MagnificationConstants.PERSISTED_SCALE_MIN_VALUE}</strong>.
+     * We assume if the scale is < {@link MagnificationConstants.PERSISTED_SCALE_MIN_VALUE}, there
+     * will be no obvious magnification effect.
      */
     public void persistScale(int displayId) {
         final float scale = getScale(Display.DEFAULT_DISPLAY);
-        if (scale < 2.0f) {
+        if (scale < MagnificationConstants.PERSISTED_SCALE_MIN_VALUE) {
             return;
         }
         mScaleProvider.putScale(scale, displayId);
@@ -1176,7 +1180,8 @@
      */
     public float getPersistedScale(int displayId) {
         return MathUtils.constrain(mScaleProvider.getScale(displayId),
-                2.0f, MagnificationScaleProvider.MAX_SCALE);
+                MagnificationConstants.PERSISTED_SCALE_MIN_VALUE,
+                MagnificationScaleProvider.MAX_SCALE);
     }
 
     /**
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
index 6bf37a1..9fc9d57 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java
@@ -992,9 +992,8 @@
                 mFullScreenMagnificationController.getPersistedScale(mDisplayId),
                 MIN_SCALE, MAX_SCALE);
 
-        final float scale = MathUtils.constrain(Math.max(currentScale + 1.0f, persistedScale),
-                MIN_SCALE, MAX_SCALE);
-
+        final boolean isActivated = mFullScreenMagnificationController.isActivated(mDisplayId);
+        final float scale = isActivated ? (currentScale + 1.0f) : persistedScale;
         zoomToScale(scale, centerX, centerY);
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
index 2d5f894..d9391f4 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationManager.java
@@ -46,6 +46,7 @@
 import android.view.accessibility.IWindowMagnificationConnectionCallback;
 import android.view.accessibility.MagnificationAnimationCallback;
 
+import com.android.internal.accessibility.common.MagnificationConstants;
 import com.android.internal.accessibility.util.AccessibilityStatsLogUtils;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -716,17 +717,20 @@
      */
     float getPersistedScale(int displayId) {
         return MathUtils.constrain(mScaleProvider.getScale(displayId),
-                2.0f, MagnificationScaleProvider.MAX_SCALE);
+                MagnificationConstants.PERSISTED_SCALE_MIN_VALUE,
+                MagnificationScaleProvider.MAX_SCALE);
     }
 
     /**
      * Persists the default display magnification scale to the current user's settings
-     *     <strong>if scale is >= 2.0</strong>. Only the
-     * value of the default display is persisted in user's settings.
+     * <strong>if scale is >= {@link MagnificationConstants.PERSISTED_SCALE_MIN_VALUE}</strong>.
+     * We assume if the scale is < {@link MagnificationConstants.PERSISTED_SCALE_MIN_VALUE}, there
+     * will be no obvious magnification effect.
+     * Only the value of the default display is persisted in user's settings.
      */
     void persistScale(int displayId) {
         float scale = getScale(displayId);
-        if (scale < 2.0f) {
+        if (scale < MagnificationConstants.PERSISTED_SCALE_MIN_VALUE) {
             return;
         }
         mScaleProvider.putScale(scale, displayId);
diff --git a/services/api/current.txt b/services/api/current.txt
index 70ee3b8..a4deed3 100644
--- a/services/api/current.txt
+++ b/services/api/current.txt
@@ -227,8 +227,9 @@
 
 package com.android.server.security {
 
-  public final class FileIntegrityLocal {
-    method public static void setUpFsVerity(@NonNull String) throws java.io.IOException;
+  public final class FileIntegrity {
+    method public static void setUpFsVerity(@NonNull java.io.File) throws java.io.IOException;
+    method public static void setUpFsVerity(@NonNull android.os.ParcelFileDescriptor) throws java.io.IOException;
   }
 
 }
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index 5f1da7b..590f472 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -315,7 +315,12 @@
                 event.mFillRequestSentTimestampMs,
                 event.mFillResponseReceivedTimestampMs,
                 event.mSuggestionSentTimestampMs,
-                event.mSuggestionPresentedTimestampMs);
+                event.mSuggestionPresentedTimestampMs,
+                //TODO(b/265051751): add new framework logging.
+                /* selected_dataset_id= */ 0,
+                /* dialog_dismissed= */ false,
+                /* negative_cta_button_clicked= */ false,
+                /* positive_cta_button_clicked= */ false);
         mEventInternal = Optional.empty();
     }
 
diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index 5eb0db1..6fd6afe 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -182,7 +182,6 @@
     public void onStart() {
         // Do init on a separate thread, will join in PHASE_ACTIVITY_MANAGER_READY
         SystemServerInitThreadPool.submit(() -> {
-            mAllowedUid = getAllowedUid();
             enforceChecksumValidity();
             formatIfOemUnlockEnabled();
             publishBinderService(Context.PERSISTENT_DATA_BLOCK_SERVICE, mService);
@@ -202,6 +201,8 @@
                 Thread.currentThread().interrupt();
                 throw new IllegalStateException("Service " + TAG + " init interrupted", e);
             }
+            // The user responsible for FRP should exist by now.
+            mAllowedUid = getAllowedUid();
             LocalServices.addService(PersistentDataBlockManagerInternal.class, mInternalService);
         }
         super.onBootPhase(phase);
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 2689193..913f151 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -742,9 +742,6 @@
                         "compactApp " + app.mOptRecord.getReqCompactSource().name() + " "
                                 + app.mOptRecord.getReqCompactProfile().name() + " " + processName);
             }
-            Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, ATRACE_COMPACTION_TRACK,
-                    "compactApp " + app.mOptRecord.getReqCompactSource().name() + " "
-                            + app.mOptRecord.getReqCompactProfile().name() + " " + processName);
             app.mOptRecord.setHasPendingCompact(true);
             app.mOptRecord.setForceCompact(force);
             mPendingCompactionProcesses.add(app);
@@ -1820,7 +1817,8 @@
                     try {
                         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                                 "Compact " + resolvedAction.name() + ": " + name
-                                        + " lastOomAdjReason: " + oomAdjReason);
+                                        + " lastOomAdjReason: " + oomAdjReason
+                                        + " source: " + compactSource.name());
                         long zramUsedKbBefore = getUsedZramMemory();
                         long startCpuTime = threadCpuTimeNs();
                         mProcessDependencies.performCompaction(resolvedAction, pid);
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index f22624c..12784bf 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -80,6 +80,7 @@
     @VisibleForTesting
     static final String[] sDeviceConfigScopes = new String[] {
         DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
+        DeviceConfig.NAMESPACE_CAMERA_NATIVE,
         DeviceConfig.NAMESPACE_CONFIGURATION,
         DeviceConfig.NAMESPACE_CONNECTIVITY,
         DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT,
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index b2e4740..0a019f3 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -2246,7 +2246,7 @@
         // If there is no challenge set, dismiss the keyguard right away
         if (isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId)) {
             // Wait until the keyguard is dismissed to unfreeze
-            mInjector.dismissKeyguard(runnable, "User Switch");
+            mInjector.dismissKeyguard(runnable);
         } else {
             runnable.run();
         }
@@ -3714,7 +3714,7 @@
             return IStorageManager.Stub.asInterface(ServiceManager.getService("mount"));
         }
 
-        protected void dismissKeyguard(Runnable runnable, String reason) {
+        protected void dismissKeyguard(Runnable runnable) {
             final AtomicBoolean isFirst = new AtomicBoolean(true);
             final Runnable runOnce = () -> {
                 if (isFirst.getAndSet(false)) {
@@ -3738,7 +3738,7 @@
                 public void onDismissCancelled() throws RemoteException {
                     mHandler.post(runOnce);
                 }
-            }, reason);
+            }, /* message= */ null);
         }
 
         boolean isUsersOnSecondaryDisplaysEnabled() {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index d888c81..c01424d 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -3647,11 +3647,12 @@
         synchronized (VolumeStreamState.class) {
             List<Integer> streamsToMute = new ArrayList<>();
             for (int stream = 0; stream < mStreamStates.length; stream++) {
-                if (streamAlias == mStreamVolumeAlias[stream]) {
+                VolumeStreamState vss = mStreamStates[stream];
+                if (streamAlias == mStreamVolumeAlias[stream] && vss.isMutable()) {
                     if (!(readCameraSoundForced()
-                            && (mStreamStates[stream].getStreamType()
+                            && (vss.getStreamType()
                                     == AudioSystem.STREAM_SYSTEM_ENFORCED))) {
-                        boolean changed = mStreamStates[stream].mute(state, /* apply= */ false);
+                        boolean changed = vss.mute(state, /* apply= */ false);
                         if (changed) {
                             streamsToMute.add(stream);
                         }
@@ -5329,7 +5330,8 @@
             if (!shouldMute) {
                 // unmute
                 // ring and notifications volume should never be 0 when not silenced
-                if (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING) {
+                if (mStreamVolumeAlias[streamType] == AudioSystem.STREAM_RING
+                        || mStreamVolumeAlias[streamType] == AudioSystem.STREAM_NOTIFICATION) {
                     synchronized (VolumeStreamState.class) {
                         final VolumeStreamState vss = mStreamStates[streamType];
                         for (int i = 0; i < vss.mIndexMap.size(); i++) {
@@ -6060,6 +6062,8 @@
             }
         }
 
+        readVolumeGroupsSettings(userSwitch);
+
         // apply new ringer mode before checking volume for alias streams so that streams
         // muted by ringer mode have the correct volume
         setRingerModeInt(getRingerModeInternal(), false);
@@ -6071,8 +6075,6 @@
         mSoundDoseHelper.restoreMusicActiveMs();
         mSoundDoseHelper.enforceSafeMediaVolumeIfActive(TAG);
 
-        readVolumeGroupsSettings(userSwitch);
-
         if (DEBUG_VOL) {
             Log.d(TAG, "Restoring device volume behavior");
         }
@@ -8478,9 +8480,10 @@
                     }
                     mVolumeGroupState.updateVolumeIndex(groupIndex, device);
                     // Only propage mute of stream when applicable
-                    if (mIndexMin == 0 || isCallStream(mStreamType)) {
+                    if (isMutable()) {
                         // For call stream, align mute only when muted, not when index is set to 0
-                        mVolumeGroupState.mute(forceMuteState ? mIsMuted : groupIndex == 0);
+                        mVolumeGroupState.mute(
+                                forceMuteState ? mIsMuted : groupIndex == 0 || mIsMuted);
                     }
                 }
             }
@@ -8529,6 +8532,12 @@
             return mIsMuted || mIsMutedInternally;
         }
 
+
+        private boolean isMutable() {
+            return isStreamAffectedByMute(mStreamType)
+                    && (mIndexMin == 0 || isCallStream(mStreamType));
+        }
+
         /**
          * Mute/unmute the stream
          * @param state the new mute state
diff --git a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
index a921a54..7026529 100644
--- a/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
+++ b/services/core/java/com/android/server/display/DeviceStateToLayoutMap.java
@@ -22,6 +22,7 @@
 import android.util.IndentingPrintWriter;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.view.Display;
 import android.view.DisplayAddress;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -114,6 +115,7 @@
                 Slog.i(TAG, "Display layout config not found: " + configFile);
                 return;
             }
+            int leadDisplayId = Display.DEFAULT_DISPLAY;
             for (com.android.server.display.config.layout.Layout l : layouts.getLayout()) {
                 final int state = l.getState().intValue();
                 final Layout layout = createLayout(state);
@@ -124,7 +126,8 @@
                             d.isDefaultDisplay(),
                             d.isEnabled(),
                             mIdProducer,
-                            d.getBrightnessThrottlingMapId());
+                            d.getBrightnessThrottlingMapId(),
+                            leadDisplayId);
 
                     if (FRONT_STRING.equals(d.getPosition())) {
                         display.setPosition(POSITION_FRONT);
@@ -133,6 +136,7 @@
                     } else {
                         display.setPosition(POSITION_UNKNOWN);
                     }
+                    display.setRefreshRateZoneId(d.getRefreshRateZoneId());
                 }
             }
         } catch (IOException | DatatypeConfigurationException | XmlPullParserException e) {
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 2af995b..a107f33 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -33,6 +33,7 @@
 import android.util.Slog;
 import android.util.Spline;
 import android.view.DisplayAddress;
+import android.view.SurfaceControl;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
@@ -53,6 +54,7 @@
 import com.android.server.display.config.Point;
 import com.android.server.display.config.RefreshRateConfigs;
 import com.android.server.display.config.RefreshRateRange;
+import com.android.server.display.config.RefreshRateZone;
 import com.android.server.display.config.SdrHdrRatioMap;
 import com.android.server.display.config.SdrHdrRatioPoint;
 import com.android.server.display.config.SensorDetails;
@@ -503,10 +505,10 @@
     /**
      * Array of light sensor lux values to define our levels for auto backlight
      * brightness support.
-
+     *
      * The N + 1 entries of this array define N control points defined in mBrightnessLevelsNits,
      * with first value always being 0 lux
-
+     *
      * The control points must be strictly increasing.  Each control point
      * corresponds to an entry in the brightness backlight values arrays.
      * For example, if lux == level[1] (second element of the levels array)
@@ -515,7 +517,6 @@
      *
      * Spline interpolation is used to determine the auto-brightness
      * backlight values for lux levels between these control points.
-     *
      */
     private float[] mBrightnessLevelsLux;
 
@@ -619,6 +620,10 @@
      */
     private int mDefaultLowBlockingZoneRefreshRate = DEFAULT_LOW_REFRESH_RATE;
 
+    // Refresh rate profiles, currently only for concurrent mode profile and controlled by Layout
+    private final Map<String, SurfaceControl.RefreshRateRange> mRefreshRateZoneProfiles =
+            new HashMap<>();
+
     /**
      * The display uses different gamma curves for different refresh rates. It's hard for panel
      * vendors to tune the curves to have exact same brightness for different refresh rate. So
@@ -1354,6 +1359,23 @@
     }
 
     /**
+     * @return Refresh rate range for specific profile id or null
+     */
+    @Nullable
+    public SurfaceControl.RefreshRateRange getRefreshRange(@Nullable String id) {
+        if (TextUtils.isEmpty(id)) {
+            return null;
+        }
+        return mRefreshRateZoneProfiles.get(id);
+    }
+
+    @NonNull
+    @VisibleForTesting
+    Map<String, SurfaceControl.RefreshRateRange> getRefreshRangeProfiles() {
+        return mRefreshRateZoneProfiles;
+    }
+
+    /**
      * @return An array of lower display brightness thresholds. This, in combination with lower
      * ambient brightness thresholds help define buckets in which the refresh rate switching is not
      * allowed
@@ -1500,6 +1522,7 @@
                 + ", mDefaultHighBlockingZoneRefreshRate= " + mDefaultHighBlockingZoneRefreshRate
                 + ", mDefaultPeakRefreshRate= " + mDefaultPeakRefreshRate
                 + ", mDefaultRefreshRate= " + mDefaultRefreshRate
+                + ", mRefreshRateZoneProfiles= " + mRefreshRateZoneProfiles
                 + ", mLowDisplayBrightnessThresholds= "
                 + Arrays.toString(mLowDisplayBrightnessThresholds)
                 + ", mLowAmbientBrightnessThresholds= "
@@ -1828,6 +1851,7 @@
         loadDefaultRefreshRate(refreshRateConfigs);
         loadLowerRefreshRateBlockingZones(lowerBlockingZoneConfig);
         loadHigherRefreshRateBlockingZones(higherBlockingZoneConfig);
+        loadRefreshRateZoneProfiles(refreshRateConfigs);
     }
 
     private void loadPeakDefaultRefreshRate(RefreshRateConfigs refreshRateConfigs) {
@@ -1850,6 +1874,21 @@
         }
     }
 
+    /** Loads the refresh rate profiles. */
+    private void loadRefreshRateZoneProfiles(RefreshRateConfigs refreshRateConfigs) {
+        if (refreshRateConfigs == null) {
+            return;
+        }
+        for (RefreshRateZone zone :
+                refreshRateConfigs.getRefreshRateZoneProfiles().getRefreshRateZoneProfile()) {
+            RefreshRateRange range = zone.getRefreshRateRange();
+            mRefreshRateZoneProfiles.put(
+                    zone.getId(),
+                    new SurfaceControl.RefreshRateRange(
+                            range.getMinimum().floatValue(), range.getMaximum().floatValue()));
+        }
+    }
+
     /**
      * Loads the refresh rate configurations pertaining to the upper blocking zones.
      */
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index 5f6660b..29caefb 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -159,7 +159,6 @@
         mSupportedModesByDisplay = new SparseArray<>();
         mDefaultModeByDisplay = new SparseArray<>();
         mAppRequestObserver = new AppRequestObserver();
-        mDisplayObserver = new DisplayObserver(context, handler);
         mDeviceConfig = injector.getDeviceConfig();
         mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings();
         mSettingsObserver = new SettingsObserver(context, handler);
@@ -170,6 +169,7 @@
                 updateVoteLocked(displayId, priority, vote);
             }
         };
+        mDisplayObserver = new DisplayObserver(context, handler, ballotBox);
         mSensorObserver = new SensorObserver(context, ballotBox, injector);
         mSkinThermalStatusObserver = new SkinThermalStatusObserver(injector, ballotBox);
         mHbmObserver = new HbmObserver(injector, ballotBox, BackgroundThread.getHandler(),
@@ -1186,26 +1186,29 @@
         // rest of low priority voters. It votes [0, max(PEAK, MIN)]
         public static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 7;
 
+        // For concurrent displays we want to limit refresh rate on all displays
+        public static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 8;
+
         // LOW_POWER_MODE force the render frame rate to [0, 60HZ] if
         // Settings.Global.LOW_POWER_MODE is on.
-        public static final int PRIORITY_LOW_POWER_MODE = 8;
+        public static final int PRIORITY_LOW_POWER_MODE = 9;
 
         // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
         // higher priority voters' result is a range, it will fix the rate to a single choice.
         // It's used to avoid refresh rate switches in certain conditions which may result in the
         // user seeing the display flickering when the switches occur.
-        public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 9;
+        public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 10;
 
         // Force display to [0, 60HZ] if skin temperature is at or above CRITICAL.
-        public static final int PRIORITY_SKIN_TEMPERATURE = 10;
+        public static final int PRIORITY_SKIN_TEMPERATURE = 11;
 
         // The proximity sensor needs the refresh rate to be locked in order to function, so this is
         // set to a high priority.
-        public static final int PRIORITY_PROXIMITY = 11;
+        public static final int PRIORITY_PROXIMITY = 12;
 
         // The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
         // to function, so this needs to be the highest priority of all votes.
-        public static final int PRIORITY_UDFPS = 12;
+        public static final int PRIORITY_UDFPS = 13;
 
         // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
         // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
@@ -1657,10 +1660,12 @@
         // calling into us already holding its own lock.
         private final Context mContext;
         private final Handler mHandler;
+        private final BallotBox mBallotBox;
 
-        DisplayObserver(Context context, Handler handler) {
+        DisplayObserver(Context context, Handler handler, BallotBox ballotBox) {
             mContext = context;
             mHandler = handler;
+            mBallotBox = ballotBox;
         }
 
         public void observe() {
@@ -1689,7 +1694,9 @@
 
         @Override
         public void onDisplayAdded(int displayId) {
-            updateDisplayModes(displayId);
+            DisplayInfo displayInfo = getDisplayInfo(displayId);
+            updateDisplayModes(displayId, displayInfo);
+            updateLayoutLimitedFrameRate(displayId, displayInfo);
         }
 
         @Override
@@ -1698,23 +1705,41 @@
                 mSupportedModesByDisplay.remove(displayId);
                 mDefaultModeByDisplay.remove(displayId);
             }
+            updateLayoutLimitedFrameRate(displayId, null);
         }
 
         @Override
         public void onDisplayChanged(int displayId) {
-            updateDisplayModes(displayId);
+            DisplayInfo displayInfo = getDisplayInfo(displayId);
+            updateDisplayModes(displayId, displayInfo);
+            updateLayoutLimitedFrameRate(displayId, displayInfo);
         }
 
-        private void updateDisplayModes(int displayId) {
+        @Nullable
+        private DisplayInfo getDisplayInfo(int displayId) {
             Display d = mContext.getSystemService(DisplayManager.class).getDisplay(displayId);
             if (d == null) {
                 // We can occasionally get a display added or changed event for a display that was
                 // subsequently removed, which means this returns null. Check this case and bail
                 // out early; if it gets re-attached we'll eventually get another call back for it.
-                return;
+                return null;
             }
             DisplayInfo info = new DisplayInfo();
             d.getDisplayInfo(info);
+            return info;
+        }
+
+        private void updateLayoutLimitedFrameRate(int displayId, @Nullable DisplayInfo info) {
+            Vote vote = info != null && info.layoutLimitedRefreshRate != null
+                    ? Vote.forPhysicalRefreshRates(info.layoutLimitedRefreshRate.min,
+                    info.layoutLimitedRefreshRate.max) : null;
+            mBallotBox.vote(displayId, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE, vote);
+        }
+
+        private void updateDisplayModes(int displayId, @Nullable DisplayInfo info) {
+            if (info == null) {
+                return;
+            }
             boolean changed = false;
             synchronized (mLock) {
                 if (!Arrays.equals(mSupportedModesByDisplay.get(displayId), info.supportedModes)) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 40eec33..b58d907 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1011,7 +1011,7 @@
         }
         mBrightnessSettingListener = brightnessValue -> {
             Message msg = mHandler.obtainMessage(MSG_UPDATE_BRIGHTNESS, brightnessValue);
-            mHandler.sendMessage(msg);
+            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
         };
 
         mBrightnessSetting.registerListener(mBrightnessSettingListener);
@@ -1040,7 +1040,7 @@
 
         final boolean isIdleScreenBrightnessEnabled = resources.getBoolean(
                 R.bool.config_enableIdleScreenBrightnessMode);
-        mInteractiveModeBrightnessMapper = BrightnessMappingStrategy.create(resources,
+        mInteractiveModeBrightnessMapper = mInjector.getInteractiveModeBrightnessMapper(resources,
                 mDisplayDeviceConfig, mDisplayWhiteBalanceController);
         if (isIdleScreenBrightnessEnabled) {
             mIdleModeBrightnessMapper = BrightnessMappingStrategy.createForIdleMode(resources,
@@ -1065,7 +1065,7 @@
                     mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
             float ambientBrighteningMinThreshold =
                     mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
-            HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels(
+            HysteresisLevels ambientBrightnessThresholds = mInjector.getHysteresisLevels(
                     ambientBrighteningThresholds, ambientDarkeningThresholds,
                     ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
                     ambientBrighteningMinThreshold);
@@ -1083,7 +1083,7 @@
                     mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
             float screenBrighteningMinThreshold =
                     mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
-            HysteresisLevels screenBrightnessThresholds = new HysteresisLevels(
+            HysteresisLevels screenBrightnessThresholds = mInjector.getHysteresisLevels(
                     screenBrighteningThresholds, screenDarkeningThresholds,
                     screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
                     screenBrighteningMinThreshold, true);
@@ -1101,7 +1101,7 @@
                     mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
             float[] ambientDarkeningLevelsIdle =
                     mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
-            HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels(
+            HysteresisLevels ambientBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
                     ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
                     ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
                     ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
@@ -1119,7 +1119,7 @@
                     mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
             float[] screenDarkeningLevelsIdle =
                     mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
-            HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
+            HysteresisLevels screenBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
                     screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
                     screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
                     screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
@@ -1155,8 +1155,8 @@
             if (mAutomaticBrightnessController != null) {
                 mAutomaticBrightnessController.stop();
             }
-            mAutomaticBrightnessController = new AutomaticBrightnessController(this,
-                    handler.getLooper(), mSensorManager, mLightSensor,
+            mAutomaticBrightnessController = mInjector.getAutomaticBrightnessController(
+                    this, handler.getLooper(), mSensorManager, mLightSensor,
                     mInteractiveModeBrightnessMapper, lightSensorWarmUpTimeConfig,
                     PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, dozeScaleFactor,
                     lightSensorRate, initialLightSensorRate, brighteningLightDebounce,
@@ -1257,7 +1257,7 @@
         public void onAnimationEnd() {
             sendUpdatePowerState();
             Message msg = mHandler.obtainMessage(MSG_BRIGHTNESS_RAMP_DONE);
-            mHandler.sendMessage(msg);
+            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
         }
     };
 
@@ -2949,7 +2949,8 @@
                 msg.what = MSG_STATSD_HBM_BRIGHTNESS;
                 msg.arg1 = Float.floatToIntBits(brightness);
                 msg.arg2 = mDisplayStatsId;
-                mHandler.sendMessageDelayed(msg, BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS);
+                mHandler.sendMessageAtTime(msg, mClock.uptimeMillis()
+                        + BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS);
             }
         }
     }
@@ -3105,7 +3106,7 @@
         @Override
         public void onScreenOn() {
             Message msg = mHandler.obtainMessage(MSG_SCREEN_ON_UNBLOCKED, this);
-            mHandler.sendMessage(msg);
+            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
         }
     }
 
@@ -3113,7 +3114,7 @@
         @Override
         public void onScreenOff() {
             Message msg = mHandler.obtainMessage(MSG_SCREEN_OFF_UNBLOCKED, this);
-            mHandler.sendMessage(msg);
+            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
         }
     }
 
@@ -3195,6 +3196,58 @@
                 FloatProperty<DisplayPowerState> secondProperty) {
             return new DualRampAnimator(dps, firstProperty, secondProperty);
         }
+
+        AutomaticBrightnessController getAutomaticBrightnessController(
+                AutomaticBrightnessController.Callbacks callbacks, Looper looper,
+                SensorManager sensorManager, Sensor lightSensor,
+                BrightnessMappingStrategy interactiveModeBrightnessMapper,
+                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+                boolean resetAmbientLuxAfterWarmUpConfig,
+                HysteresisLevels ambientBrightnessThresholds,
+                HysteresisLevels screenBrightnessThresholds,
+                HysteresisLevels ambientBrightnessThresholdsIdle,
+                HysteresisLevels screenBrightnessThresholdsIdle, Context context,
+                HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler,
+                BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
+                int ambientLightHorizonLong, float userLux, float userBrightness) {
+            return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
+                    interactiveModeBrightnessMapper, lightSensorWarmUpTime, brightnessMin,
+                    brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
+                    brighteningLightDebounceConfig, darkeningLightDebounceConfig,
+                    resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
+                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+                    screenBrightnessThresholdsIdle, context, hbmController, brightnessThrottler,
+                    idleModeBrightnessMapper, ambientLightHorizonShort, ambientLightHorizonLong,
+                    userLux, userBrightness);
+        }
+
+        BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+                DisplayDeviceConfig displayDeviceConfig,
+                DisplayWhiteBalanceController displayWhiteBalanceController) {
+            return BrightnessMappingStrategy.create(resources,
+                    displayDeviceConfig, displayWhiteBalanceController);
+        }
+
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold) {
+            return new HysteresisLevels(brighteningThresholdsPercentages,
+                    darkeningThresholdsPercentages, brighteningThresholdLevels,
+                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold);
+        }
+
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+            return new HysteresisLevels(brighteningThresholdsPercentages,
+                    darkeningThresholdsPercentages, brighteningThresholdLevels,
+                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold,
+                    potentialOldBrightnessRange);
+        }
     }
 
     static class CachedBrightnessInfo {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 6092ad7..23ef680 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -850,7 +850,7 @@
 
         BrightnessSetting.BrightnessSettingListener brightnessSettingListener = brightnessValue -> {
             Message msg = mHandler.obtainMessage(MSG_UPDATE_BRIGHTNESS, brightnessValue);
-            mHandler.sendMessage(msg);
+            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
         };
         mDisplayBrightnessController
                 .registerBrightnessSettingChangeListener(brightnessSettingListener);
@@ -880,7 +880,7 @@
 
         final boolean isIdleScreenBrightnessEnabled = resources.getBoolean(
                 R.bool.config_enableIdleScreenBrightnessMode);
-        mInteractiveModeBrightnessMapper = BrightnessMappingStrategy.create(resources,
+        mInteractiveModeBrightnessMapper = mInjector.getInteractiveModeBrightnessMapper(resources,
                 mDisplayDeviceConfig, mDisplayWhiteBalanceController);
         if (isIdleScreenBrightnessEnabled) {
             mIdleModeBrightnessMapper = BrightnessMappingStrategy.createForIdleMode(resources,
@@ -905,7 +905,7 @@
                     mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
             float ambientBrighteningMinThreshold =
                     mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
-            HysteresisLevels ambientBrightnessThresholds = new HysteresisLevels(
+            HysteresisLevels ambientBrightnessThresholds = mInjector.getHysteresisLevels(
                     ambientBrighteningThresholds, ambientDarkeningThresholds,
                     ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
                     ambientBrighteningMinThreshold);
@@ -923,7 +923,7 @@
                     mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
             float screenBrighteningMinThreshold =
                     mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
-            HysteresisLevels screenBrightnessThresholds = new HysteresisLevels(
+            HysteresisLevels screenBrightnessThresholds = mInjector.getHysteresisLevels(
                     screenBrighteningThresholds, screenDarkeningThresholds,
                     screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
                     screenBrighteningMinThreshold, true);
@@ -941,7 +941,7 @@
                     mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
             float[] ambientDarkeningLevelsIdle =
                     mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
-            HysteresisLevels ambientBrightnessThresholdsIdle = new HysteresisLevels(
+            HysteresisLevels ambientBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
                     ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
                     ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
                     ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
@@ -959,7 +959,7 @@
                     mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
             float[] screenDarkeningLevelsIdle =
                     mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
-            HysteresisLevels screenBrightnessThresholdsIdle = new HysteresisLevels(
+            HysteresisLevels screenBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
                     screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
                     screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
                     screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
@@ -995,8 +995,8 @@
             if (mAutomaticBrightnessController != null) {
                 mAutomaticBrightnessController.stop();
             }
-            mAutomaticBrightnessController = new AutomaticBrightnessController(this,
-                    handler.getLooper(), mSensorManager, mLightSensor,
+            mAutomaticBrightnessController = mInjector.getAutomaticBrightnessController(
+                    this, handler.getLooper(), mSensorManager, mLightSensor,
                     mInteractiveModeBrightnessMapper, lightSensorWarmUpTimeConfig,
                     PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, dozeScaleFactor,
                     lightSensorRate, initialLightSensorRate, brighteningLightDebounce,
@@ -1094,7 +1094,7 @@
         public void onAnimationEnd() {
             sendUpdatePowerState();
             Message msg = mHandler.obtainMessage(MSG_BRIGHTNESS_RAMP_DONE);
-            mHandler.sendMessage(msg);
+            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
         }
     };
 
@@ -2458,7 +2458,8 @@
                 msg.what = MSG_STATSD_HBM_BRIGHTNESS;
                 msg.arg1 = Float.floatToIntBits(brightness);
                 msg.arg2 = mDisplayStatsId;
-                mHandler.sendMessageDelayed(msg, BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS);
+                mHandler.sendMessageAtTime(msg, mClock.uptimeMillis()
+                        + BRIGHTNESS_CHANGE_STATSD_REPORT_INTERVAL_MS);
             }
         }
     }
@@ -2589,7 +2590,7 @@
         @Override
         public void onScreenOn() {
             Message msg = mHandler.obtainMessage(MSG_SCREEN_ON_UNBLOCKED, this);
-            mHandler.sendMessage(msg);
+            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
         }
     }
 
@@ -2597,7 +2598,7 @@
         @Override
         public void onScreenOff() {
             Message msg = mHandler.obtainMessage(MSG_SCREEN_OFF_UNBLOCKED, this);
-            mHandler.sendMessage(msg);
+            mHandler.sendMessageAtTime(msg, mClock.uptimeMillis());
         }
     }
 
@@ -2671,6 +2672,58 @@
                     looper, nudgeUpdatePowerState,
                     displayId, sensorManager, /* injector= */ null);
         }
+
+        AutomaticBrightnessController getAutomaticBrightnessController(
+                AutomaticBrightnessController.Callbacks callbacks, Looper looper,
+                SensorManager sensorManager, Sensor lightSensor,
+                BrightnessMappingStrategy interactiveModeBrightnessMapper,
+                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+                boolean resetAmbientLuxAfterWarmUpConfig,
+                HysteresisLevels ambientBrightnessThresholds,
+                HysteresisLevels screenBrightnessThresholds,
+                HysteresisLevels ambientBrightnessThresholdsIdle,
+                HysteresisLevels screenBrightnessThresholdsIdle, Context context,
+                HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler,
+                BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,
+                int ambientLightHorizonLong, float userLux, float userBrightness) {
+            return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
+                    interactiveModeBrightnessMapper, lightSensorWarmUpTime, brightnessMin,
+                    brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
+                    brighteningLightDebounceConfig, darkeningLightDebounceConfig,
+                    resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
+                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+                    screenBrightnessThresholdsIdle, context, hbmController, brightnessThrottler,
+                    idleModeBrightnessMapper, ambientLightHorizonShort, ambientLightHorizonLong,
+                    userLux, userBrightness);
+        }
+
+        BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+                DisplayDeviceConfig displayDeviceConfig,
+                DisplayWhiteBalanceController displayWhiteBalanceController) {
+            return BrightnessMappingStrategy.create(resources,
+                    displayDeviceConfig, displayWhiteBalanceController);
+        }
+
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold) {
+            return new HysteresisLevels(brighteningThresholdsPercentages,
+                    darkeningThresholdsPercentages, brighteningThresholdLevels,
+                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold);
+        }
+
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+            return new HysteresisLevels(brighteningThresholdsPercentages,
+                    darkeningThresholdsPercentages, brighteningThresholdLevels,
+                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold,
+                    potentialOldBrightnessRange);
+        }
     }
 
     static class CachedBrightnessInfo {
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 4bb1f0e..473317c 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -77,6 +77,12 @@
     private final int mDisplayId;
     private final int mLayerStack;
 
+    // Indicates which display leads this logical display, in terms of brightness or other
+    // properties.
+    // {@link Layout.NO_LEAD_DISPLAY} means that this display is not lead by any others, and could
+    // be a leader itself.
+    private int mLeadDisplayId = Layout.NO_LEAD_DISPLAY;
+
     private int mDisplayGroupId = Display.INVALID_DISPLAY_GROUP;
 
     /**
@@ -150,7 +156,7 @@
 
     // Indicates the display is part of a transition from one device-state ({@link
     // DeviceStateManager}) to another. Being a "part" of a transition means that either
-    // the {@link mIsEnabled} is changing, or the underlying mPrimiaryDisplayDevice is changing.
+    // the {@link mIsEnabled} is changing, or the underlying mPrimaryDisplayDevice is changing.
     private boolean mIsInTransition;
 
     // Indicates the position of the display, POSITION_UNKNOWN could mean it hasn't been specified,
@@ -826,6 +832,27 @@
                 brightnessThrottlingDataId;
     }
 
+    /**
+     * Sets the display of which this display is a follower, regarding brightness or other
+     * properties. If set to {@link Layout#NO_LEAD_DISPLAY}, this display does not follow any
+     * others, and has the potential to be a lead display to others.
+     *
+     * A display cannot be a leader or follower of itself, and there cannot be cycles.
+     * A display cannot be both a leader and a follower, ie, there must not be any chains.
+     *
+     * @param displayId logical display id
+     */
+    public void setLeadDisplayLocked(int displayId) {
+        if (mDisplayId != mLeadDisplayId && mDisplayId != displayId) {
+            mLeadDisplayId = displayId;
+        }
+    }
+
+    public int getLeadDisplayLocked() {
+        return mLeadDisplayId;
+
+    }
+
     public void dumpLocked(PrintWriter pw) {
         pw.println("mDisplayId=" + mDisplayId);
         pw.println("mIsEnabled=" + mIsEnabled);
@@ -845,6 +872,7 @@
         pw.println("mFrameRateOverrides=" + Arrays.toString(mFrameRateOverrides));
         pw.println("mPendingFrameRateOverrideUids=" + mPendingFrameRateOverrideUids);
         pw.println("mBrightnessThrottlingDataId=" + mBrightnessThrottlingDataId);
+        pw.println("mLeadDisplayId=" + mLeadDisplayId);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index a67644b..56c9056 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -18,6 +18,8 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.server.display.layout.Layout.NO_LEAD_DISPLAY;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
@@ -639,7 +641,7 @@
                         && !nextDeviceInfo.address.equals(deviceInfo.address)) {
                     layout.createDisplayLocked(nextDeviceInfo.address,
                             /* isDefault= */ true, /* isEnabled= */ true, mIdProducer,
-                            /* brightnessThrottlingMapId= */ null);
+                            /* brightnessThrottlingMapId= */ null, DEFAULT_DISPLAY);
                     applyLayoutLocked();
                     return;
                 }
@@ -991,15 +993,22 @@
             }
 
             newDisplay.setPositionLocked(displayLayout.getPosition());
+            newDisplay.setLeadDisplayLocked(displayLayout.getLeadDisplayId());
+            setLayoutLimitedRefreshRate(newDisplay, device, displayLayout);
             setEnabledLocked(newDisplay, displayLayout.isEnabled());
             newDisplay.setBrightnessThrottlingDataIdLocked(
                     displayLayout.getBrightnessThrottlingMapId() == null
                             ? DisplayDeviceConfig.DEFAULT_BRIGHTNESS_THROTTLING_DATA_ID
                             : displayLayout.getBrightnessThrottlingMapId());
         }
-
     }
 
+    private void setLayoutLimitedRefreshRate(@NonNull LogicalDisplay logicalDisplay,
+            @NonNull DisplayDevice device, @NonNull Layout.Display display) {
+        DisplayDeviceConfig config = device.getDisplayDeviceConfig();
+        DisplayInfo info = logicalDisplay.getDisplayInfoLocked();
+        info.layoutLimitedRefreshRate = config.getRefreshRange(display.getRefreshRateZoneId());
+    }
 
     /**
      * Creates a new logical display for the specified device and display Id and adds it to the list
@@ -1070,7 +1079,7 @@
         }
         final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
         layout.createDisplayLocked(info.address, /* isDefault= */ true, /* isEnabled= */ true,
-                mIdProducer, /* brightnessThrottlingMapId= */ null);
+                mIdProducer, /* brightnessThrottlingMapId= */ null, NO_LEAD_DISPLAY);
     }
 
     private int assignLayerStackLocked(int displayId) {
diff --git a/services/core/java/com/android/server/display/layout/Layout.java b/services/core/java/com/android/server/display/layout/Layout.java
index 01aa97a..59d95a6 100644
--- a/services/core/java/com/android/server/display/layout/Layout.java
+++ b/services/core/java/com/android/server/display/layout/Layout.java
@@ -39,6 +39,10 @@
     private static final String TAG = "Layout";
     private static int sNextNonDefaultDisplayId = DEFAULT_DISPLAY + 1;
 
+    // Lead display Id is set to this if this is not a follower display, and therefore
+    // has no lead.
+    public static final int NO_LEAD_DISPLAY = -1;
+
     private final List<Display> mDisplays = new ArrayList<>(2);
 
     /**
@@ -75,13 +79,16 @@
      * @param address Address of the device.
      * @param isDefault Indicates if the device is meant to be the default display.
      * @param isEnabled Indicates if this display is usable and can be switched on
-     * @return The new layout.
+     * @param idProducer Produces the logical display id.
+     * @param brightnessThrottlingMapId Name of which throttling policy should be used.
+     * @param leadDisplayId Display that this one follows (-1 if none).
+     * @return The new Display.
      */
     public Display createDisplayLocked(
             @NonNull DisplayAddress address, boolean isDefault, boolean isEnabled,
-            DisplayIdProducer idProducer, String brightnessThrottlingMapId) {
+            DisplayIdProducer idProducer, String brightnessThrottlingMapId, int leadDisplayId) {
         return createDisplayLocked(address, isDefault, isEnabled, idProducer,
-                brightnessThrottlingMapId, POSITION_UNKNOWN);
+                brightnessThrottlingMapId, POSITION_UNKNOWN, leadDisplayId);
     }
 
     /**
@@ -90,12 +97,16 @@
      * @param address Address of the device.
      * @param isDefault Indicates if the device is meant to be the default display.
      * @param isEnabled Indicates if this display is usable and can be switched on
+     * @param idProducer Produces the logical display id.
+     * @param brightnessThrottlingMapId Name of which throttling policy should be used.
      * @param position Indicates the position this display is facing in this layout.
-     * @return The new layout.
+     * @param leadDisplayId Display that this one follows (-1 if none).
+     * @return The new Display.
      */
     public Display createDisplayLocked(
             @NonNull DisplayAddress address, boolean isDefault, boolean isEnabled,
-            DisplayIdProducer idProducer, String brightnessThrottlingMapId, int position) {
+            DisplayIdProducer idProducer, String brightnessThrottlingMapId, int position,
+            int leadDisplayId) {
         if (contains(address)) {
             Slog.w(TAG, "Attempting to add second definition for display-device: " + address);
             return null;
@@ -113,7 +124,7 @@
         // same logical display ID.
         final int logicalDisplayId = idProducer.getId(isDefault);
         final Display display = new Display(address, logicalDisplayId, isEnabled,
-                brightnessThrottlingMapId, position);
+                brightnessThrottlingMapId, position, leadDisplayId);
 
         mDisplays.add(display);
         return display;
@@ -221,13 +232,27 @@
         @Nullable
         private final String mBrightnessThrottlingMapId;
 
+        // The ID of the lead display that this display will follow in a layout. -1 means no lead.
+        private int mLeadDisplayId;
+
+        // Refresh rate zone id for specific layout
+        @Nullable
+        private String mRefreshRateZoneId;
+
         Display(@NonNull DisplayAddress address, int logicalDisplayId, boolean isEnabled,
-                String brightnessThrottlingMapId, int position) {
+                String brightnessThrottlingMapId, int position, int leadDisplayId) {
             mAddress = address;
             mLogicalDisplayId = logicalDisplayId;
             mIsEnabled = isEnabled;
             mPosition = position;
             mBrightnessThrottlingMapId = brightnessThrottlingMapId;
+
+            if (leadDisplayId == mLogicalDisplayId) {
+                mLeadDisplayId = NO_LEAD_DISPLAY;
+            } else {
+                mLeadDisplayId = leadDisplayId;
+            }
+
         }
 
         @Override
@@ -238,6 +263,8 @@
                     + ", addr: " + mAddress
                     +  ((mPosition == POSITION_UNKNOWN) ? "" : ", position: " + mPosition)
                     + ", brightnessThrottlingMapId: " + mBrightnessThrottlingMapId
+                    + ", mRefreshRateZoneId: " + mRefreshRateZoneId
+                    + ", mLeadDisplayId: " + mLeadDisplayId
                     + "}";
         }
 
@@ -254,7 +281,9 @@
                     && otherDisplay.mLogicalDisplayId == this.mLogicalDisplayId
                     && this.mAddress.equals(otherDisplay.mAddress)
                     && Objects.equals(mBrightnessThrottlingMapId,
-                    otherDisplay.mBrightnessThrottlingMapId);
+                    otherDisplay.mBrightnessThrottlingMapId)
+                    && Objects.equals(otherDisplay.mRefreshRateZoneId, this.mRefreshRateZoneId)
+                    && this.mLeadDisplayId == otherDisplay.mLeadDisplayId;
         }
 
         @Override
@@ -265,6 +294,8 @@
             result = 31 * result + mLogicalDisplayId;
             result = 31 * result + mAddress.hashCode();
             result = 31 * result + mBrightnessThrottlingMapId.hashCode();
+            result = 31 * result + Objects.hashCode(mRefreshRateZoneId);
+            result = 31 * result + mLeadDisplayId;
             return result;
         }
 
@@ -280,6 +311,20 @@
             return mIsEnabled;
         }
 
+
+        public void setRefreshRateZoneId(@Nullable String refreshRateZoneId) {
+            mRefreshRateZoneId = refreshRateZoneId;
+        }
+
+        @Nullable
+        public String getRefreshRateZoneId() {
+            return mRefreshRateZoneId;
+        }
+
+        /**
+         * Sets the position that this display is facing.
+         * @param position the display is facing.
+         */
         public void setPosition(int position) {
             mPosition = position;
         }
@@ -291,8 +336,31 @@
             return mBrightnessThrottlingMapId;
         }
 
+        /**
+         *
+         * @return the position that this display is facing.
+         */
         public int getPosition() {
             return mPosition;
         }
+
+        /**
+         * Set the display that this display should follow certain properties of, for example,
+         * brightness
+         * @param displayId of the lead display.
+         */
+        public void setLeadDisplay(int displayId) {
+            if (displayId != mLogicalDisplayId) {
+                mLeadDisplayId = displayId;
+            }
+        }
+
+        /**
+         *
+         * @return logical displayId of the display that this one follows.
+         */
+        public int getLeadDisplayId() {
+            return mLeadDisplayId;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index e2caeec..84e63ec 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -32,7 +32,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
-import android.database.ContentObserver;
 import android.graphics.PointF;
 import android.hardware.SensorPrivacyManager;
 import android.hardware.SensorPrivacyManager.Sensors;
@@ -78,8 +77,6 @@
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationEffectSegment;
 import android.provider.DeviceConfig;
-import android.provider.Settings;
-import android.provider.Settings.SettingNotFoundException;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.IndentingPrintWriter;
@@ -147,8 +144,6 @@
     private static final String EXCLUDED_DEVICES_PATH = "etc/excluded-input-devices.xml";
     private static final String PORT_ASSOCIATIONS_PATH = "etc/input-port-associations.xml";
 
-    // Feature flag name for the deep press feature
-    private static final String DEEP_PRESS_ENABLED = "deep_press_enabled";
     // Feature flag name for the strategy to be used in VelocityTracker
     private static final String VELOCITYTRACKER_STRATEGY_PROPERTY = "velocitytracker_strategy";
 
@@ -307,6 +302,9 @@
     @GuardedBy("mInputMonitors")
     final Map<IBinder, GestureMonitorSpyWindow> mInputMonitors = new HashMap<>();
 
+    // Watches for settings changes and updates the native side appropriately.
+    private final InputSettingsObserver mSettingsObserver;
+
     // Manages Keyboard layouts for Physical keyboards
     private final KeyboardLayoutManager mKeyboardLayoutManager;
 
@@ -428,6 +426,7 @@
         mContext = injector.getContext();
         mHandler = new InputManagerHandler(injector.getLooper());
         mNative = injector.getNativeService(this);
+        mSettingsObserver = new InputSettingsObserver(mContext, mHandler, mNative);
         mKeyboardLayoutManager = new KeyboardLayoutManager(mContext, mNative, mDataStore,
                 injector.getLooper());
         mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());
@@ -493,39 +492,7 @@
         // Add ourselves to the Watchdog monitors.
         Watchdog.getInstance().addMonitor(this);
 
-        registerMousePointerSpeedSettingObserver();
-        registerTouchpadPointerSpeedSettingObserver();
-        registerTouchpadNaturalScrollingEnabledObserver();
-        registerTouchpadTapToClickEnabledObserver();
-        registerTouchpadRightClickZoneEnabledObserver();
-        registerShowTouchesSettingObserver();
-        registerAccessibilityLargePointerSettingObserver();
-        registerLongPressTimeoutObserver();
-        registerMaximumObscuringOpacityForTouchSettingObserver();
-
-        mContext.registerReceiver(new BroadcastReceiver() {
-            @Override
-            public void onReceive(Context context, Intent intent) {
-                updateMousePointerSpeedFromSettings();
-                updateTouchpadPointerSpeedFromSettings();
-                updateTouchpadNaturalScrollingEnabledFromSettings();
-                updateTouchpadTapToClickEnabledFromSettings();
-                updateTouchpadRightClickZoneEnabledFromSettings();
-                updateShowTouchesFromSettings();
-                updateAccessibilityLargePointerFromSettings();
-                updateDeepPressStatusFromSettings("user switched");
-            }
-        }, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler);
-
-        updateMousePointerSpeedFromSettings();
-        updateTouchpadPointerSpeedFromSettings();
-        updateTouchpadNaturalScrollingEnabledFromSettings();
-        updateTouchpadTapToClickEnabledFromSettings();
-        updateTouchpadRightClickZoneEnabledFromSettings();
-        updateShowTouchesFromSettings();
-        updateAccessibilityLargePointerFromSettings();
-        updateDeepPressStatusFromSettings("just booted");
-        updateMaximumObscuringOpacityForTouchFromSettings();
+        mSettingsObserver.registerAndUpdate();
     }
 
     // TODO(BT) Pass in parameter for bluetooth system
@@ -1349,11 +1316,6 @@
         setPointerSpeedUnchecked(speed);
     }
 
-    private void updateMousePointerSpeedFromSettings() {
-        int speed = getMousePointerSpeedSetting();
-        setPointerSpeedUnchecked(speed);
-    }
-
     private void setPointerSpeedUnchecked(int speed) {
         speed = Math.min(Math.max(speed, InputManager.MIN_POINTER_SPEED),
                 InputManager.MAX_POINTER_SPEED);
@@ -1370,194 +1332,6 @@
                 properties -> properties.pointerIconVisible = visible);
     }
 
-    private void registerMousePointerSpeedSettingObserver() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(Settings.System.POINTER_SPEED), true,
-                new ContentObserver(mHandler) {
-                    @Override
-                    public void onChange(boolean selfChange) {
-                        updateMousePointerSpeedFromSettings();
-                    }
-                }, UserHandle.USER_ALL);
-    }
-
-    private int getMousePointerSpeedSetting() {
-        int speed = InputManager.DEFAULT_POINTER_SPEED;
-        try {
-            speed = Settings.System.getIntForUser(mContext.getContentResolver(),
-                    Settings.System.POINTER_SPEED, UserHandle.USER_CURRENT);
-        } catch (SettingNotFoundException ignored) {
-        }
-        return speed;
-    }
-
-    private void registerTouchpadPointerSpeedSettingObserver() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(Settings.System.TOUCHPAD_POINTER_SPEED), true,
-                new ContentObserver(mHandler) {
-                    @Override
-                    public void onChange(boolean selfChange) {
-                        updateTouchpadPointerSpeedFromSettings();
-                    }
-                }, UserHandle.USER_ALL);
-    }
-
-    private void updateTouchpadPointerSpeedFromSettings() {
-        int speed = Settings.System.getIntForUser(mContext.getContentResolver(),
-                Settings.System.TOUCHPAD_POINTER_SPEED, InputManager.DEFAULT_POINTER_SPEED,
-                UserHandle.USER_CURRENT);
-        speed = Math.min(Math.max(speed, InputManager.MIN_POINTER_SPEED),
-                InputManager.MAX_POINTER_SPEED);
-        mNative.setTouchpadPointerSpeed(speed);
-    }
-
-    private void registerTouchpadNaturalScrollingEnabledObserver() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(Settings.System.TOUCHPAD_NATURAL_SCROLLING), true,
-                new ContentObserver(mHandler) {
-                    @Override
-                    public void onChange(boolean selfChange) {
-                        updateTouchpadNaturalScrollingEnabledFromSettings();
-                    }
-                }, UserHandle.USER_ALL);
-    }
-
-    private void updateTouchpadNaturalScrollingEnabledFromSettings() {
-        int setting = Settings.System.getIntForUser(mContext.getContentResolver(),
-                Settings.System.TOUCHPAD_NATURAL_SCROLLING, 0, UserHandle.USER_CURRENT);
-        mNative.setTouchpadNaturalScrollingEnabled(setting != 0);
-    }
-
-    private void registerTouchpadTapToClickEnabledObserver() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_TO_CLICK), true,
-                new ContentObserver(mHandler) {
-                    @Override
-                    public void onChange(boolean selfChange) {
-                        updateTouchpadTapToClickEnabledFromSettings();
-                    }
-                }, UserHandle.USER_ALL);
-    }
-
-    private void updateTouchpadTapToClickEnabledFromSettings() {
-        int setting = Settings.System.getIntForUser(mContext.getContentResolver(),
-                Settings.System.TOUCHPAD_TAP_TO_CLICK, 0, UserHandle.USER_CURRENT);
-        mNative.setTouchpadTapToClickEnabled(setting != 0);
-    }
-
-    private void registerTouchpadRightClickZoneEnabledObserver() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE), true,
-                new ContentObserver(mHandler) {
-                    @Override
-                    public void onChange(boolean selfChange) {
-                        updateTouchpadRightClickZoneEnabledFromSettings();
-                    }
-                }, UserHandle.USER_ALL);
-    }
-
-    private void updateTouchpadRightClickZoneEnabledFromSettings() {
-        int setting = Settings.System.getIntForUser(mContext.getContentResolver(),
-                Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE, 0, UserHandle.USER_CURRENT);
-        mNative.setTouchpadRightClickZoneEnabled(setting != 0);
-    }
-
-    private void updateShowTouchesFromSettings() {
-        int setting = getShowTouchesSetting(0);
-        mNative.setShowTouches(setting != 0);
-    }
-
-    private void registerShowTouchesSettingObserver() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(Settings.System.SHOW_TOUCHES), true,
-                new ContentObserver(mHandler) {
-                    @Override
-                    public void onChange(boolean selfChange) {
-                        updateShowTouchesFromSettings();
-                    }
-                }, UserHandle.USER_ALL);
-    }
-
-    private void updateAccessibilityLargePointerFromSettings() {
-        final int accessibilityConfig = Settings.Secure.getIntForUser(
-                mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON,
-                0, UserHandle.USER_CURRENT);
-        PointerIcon.setUseLargeIcons(accessibilityConfig == 1);
-        mNative.reloadPointerIcons();
-    }
-
-    private void registerAccessibilityLargePointerSettingObserver() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON), true,
-                new ContentObserver(mHandler) {
-                    @Override
-                    public void onChange(boolean selfChange) {
-                        updateAccessibilityLargePointerFromSettings();
-                    }
-                }, UserHandle.USER_ALL);
-    }
-
-    private void updateDeepPressStatusFromSettings(String reason) {
-        // Not using ViewConfiguration.getLongPressTimeout here because it may return a stale value
-        final int timeout = Settings.Secure.getIntForUser(mContext.getContentResolver(),
-                Settings.Secure.LONG_PRESS_TIMEOUT, ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT,
-                UserHandle.USER_CURRENT);
-        final boolean featureEnabledFlag =
-                DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT,
-                        DEEP_PRESS_ENABLED, true /* default */);
-        final boolean enabled =
-                featureEnabledFlag && timeout <= ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT;
-        Log.i(TAG,
-                (enabled ? "Enabling" : "Disabling") + " motion classifier because " + reason
-                + ": feature " + (featureEnabledFlag ? "enabled" : "disabled")
-                + ", long press timeout = " + timeout);
-        mNative.setMotionClassifierEnabled(enabled);
-    }
-
-    private void registerLongPressTimeoutObserver() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Secure.getUriFor(Settings.Secure.LONG_PRESS_TIMEOUT), true,
-                new ContentObserver(mHandler) {
-                    @Override
-                    public void onChange(boolean selfChange) {
-                        updateDeepPressStatusFromSettings("timeout changed");
-                    }
-                }, UserHandle.USER_ALL);
-    }
-
-    private void registerMaximumObscuringOpacityForTouchSettingObserver() {
-        mContext.getContentResolver().registerContentObserver(
-                Settings.Global.getUriFor(Settings.Global.MAXIMUM_OBSCURING_OPACITY_FOR_TOUCH),
-                /* notifyForDescendants */ true,
-                new ContentObserver(mHandler) {
-                    @Override
-                    public void onChange(boolean selfChange) {
-                        updateMaximumObscuringOpacityForTouchFromSettings();
-                    }
-                }, UserHandle.USER_ALL);
-    }
-
-    private void updateMaximumObscuringOpacityForTouchFromSettings() {
-        InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
-        final float opacity = im.getMaximumObscuringOpacityForTouch();
-        if (opacity < 0 || opacity > 1) {
-            Log.e(TAG, "Invalid maximum obscuring opacity " + opacity
-                    + ", it should be >= 0 and <= 1, rejecting update.");
-            return;
-        }
-        mNative.setMaximumObscuringOpacityForTouch(opacity);
-    }
-
-    private int getShowTouchesSetting(int defaultValue) {
-        int result = defaultValue;
-        try {
-            result = Settings.System.getIntForUser(mContext.getContentResolver(),
-                    Settings.System.SHOW_TOUCHES, UserHandle.USER_CURRENT);
-        } catch (SettingNotFoundException snfe) {
-        }
-        return result;
-    }
-
     /**
      * Update the display on which the mouse pointer is shown.
      *
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
new file mode 100644
index 0000000..8ee3a72
--- /dev/null
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.input;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.hardware.input.InputManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.PointerIcon;
+import android.view.ViewConfiguration;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/** Observes settings changes and propagates them to the native side. */
+class InputSettingsObserver extends ContentObserver {
+    static final String TAG = "InputManager";
+
+    /** Feature flag name for the deep press feature */
+    private static final String DEEP_PRESS_ENABLED = "deep_press_enabled";
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final NativeInputManagerService mNative;
+    private final Map<Uri, Consumer<String /* reason*/>> mObservers;
+
+    InputSettingsObserver(Context context, Handler handler, NativeInputManagerService nativeIms) {
+        super(handler);
+        mContext = context;
+        mHandler = handler;
+        mNative = nativeIms;
+        mObservers = Map.ofEntries(
+            Map.entry(Settings.System.getUriFor(Settings.System.POINTER_SPEED),
+                    (reason) -> updateMousePointerSpeed()),
+            Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_POINTER_SPEED),
+                    (reason) -> updateTouchpadPointerSpeed()),
+            Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_NATURAL_SCROLLING),
+                    (reason) -> updateTouchpadNaturalScrollingEnabled()),
+            Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_TO_CLICK),
+                    (reason) -> updateTouchpadTapToClickEnabled()),
+            Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE),
+                    (reason) -> updateTouchpadRightClickZoneEnabled()),
+            Map.entry(Settings.System.getUriFor(Settings.System.SHOW_TOUCHES),
+                    (reason) -> updateShowTouches()),
+            Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON),
+                    (reason) -> updateAccessibilityLargePointer()),
+            Map.entry(Settings.Secure.getUriFor(Settings.Secure.LONG_PRESS_TIMEOUT),
+                    (reason) -> updateDeepPressStatus(reason)),
+            Map.entry(
+                    Settings.Global.getUriFor(Settings.Global.MAXIMUM_OBSCURING_OPACITY_FOR_TOUCH),
+                    (reason) -> updateMaximumObscuringOpacityForTouch()));
+    }
+
+    /**
+     * Registers observers for input-related settings and updates the input subsystem with their
+     * current values.
+     */
+    public void registerAndUpdate() {
+        for (Uri uri : mObservers.keySet()) {
+            mContext.getContentResolver().registerContentObserver(
+                    uri, true /* notifyForDescendants */, this, UserHandle.USER_ALL);
+        }
+
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                for (Consumer<String> observer : mObservers.values()) {
+                    observer.accept("user switched");
+                }
+            }
+        }, new IntentFilter(Intent.ACTION_USER_SWITCHED), null, mHandler);
+
+        for (Consumer<String> observer : mObservers.values()) {
+            observer.accept("just booted");
+        }
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        mObservers.get(uri).accept("setting changed");
+    }
+
+    private boolean getBoolean(String settingName) {
+        final int setting = Settings.System.getIntForUser(mContext.getContentResolver(),
+                settingName, 0, UserHandle.USER_CURRENT);
+        return setting != 0;
+    }
+
+    private int getPointerSpeedValue(String settingName) {
+        int speed = Settings.System.getIntForUser(mContext.getContentResolver(),
+                settingName, InputManager.DEFAULT_POINTER_SPEED, UserHandle.USER_CURRENT);
+        return Math.min(Math.max(speed, InputManager.MIN_POINTER_SPEED),
+                InputManager.MAX_POINTER_SPEED);
+    }
+
+    private void updateMousePointerSpeed() {
+        mNative.setPointerSpeed(getPointerSpeedValue(Settings.System.POINTER_SPEED));
+    }
+
+    private void updateTouchpadPointerSpeed() {
+        mNative.setTouchpadPointerSpeed(
+                getPointerSpeedValue(Settings.System.TOUCHPAD_POINTER_SPEED));
+    }
+
+    private void updateTouchpadNaturalScrollingEnabled() {
+        mNative.setTouchpadNaturalScrollingEnabled(
+                getBoolean(Settings.System.TOUCHPAD_NATURAL_SCROLLING));
+    }
+
+    private void updateTouchpadTapToClickEnabled() {
+        mNative.setTouchpadTapToClickEnabled(getBoolean(Settings.System.TOUCHPAD_TAP_TO_CLICK));
+    }
+
+    private void updateTouchpadRightClickZoneEnabled() {
+        mNative.setTouchpadRightClickZoneEnabled(
+                getBoolean(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE));
+    }
+
+    private void updateShowTouches() {
+        mNative.setShowTouches(getBoolean(Settings.System.SHOW_TOUCHES));
+    }
+
+    private void updateAccessibilityLargePointer() {
+        final int accessibilityConfig = Settings.Secure.getIntForUser(
+                mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON,
+                0, UserHandle.USER_CURRENT);
+        PointerIcon.setUseLargeIcons(accessibilityConfig == 1);
+        mNative.reloadPointerIcons();
+    }
+
+    private void updateDeepPressStatus(String reason) {
+        // Not using ViewConfiguration.getLongPressTimeout here because it may return a stale value
+        final int timeout = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+                Settings.Secure.LONG_PRESS_TIMEOUT, ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT,
+                UserHandle.USER_CURRENT);
+        final boolean featureEnabledFlag =
+                DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT,
+                        DEEP_PRESS_ENABLED, true /* default */);
+        final boolean enabled =
+                featureEnabledFlag && timeout <= ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT;
+        Log.i(TAG,
+                (enabled ? "Enabling" : "Disabling") + " motion classifier because " + reason
+                + ": feature " + (featureEnabledFlag ? "enabled" : "disabled")
+                + ", long press timeout = " + timeout);
+        mNative.setMotionClassifierEnabled(enabled);
+    }
+
+    private void updateMaximumObscuringOpacityForTouch() {
+        InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
+        final float opacity = im.getMaximumObscuringOpacityForTouch();
+        if (opacity < 0 || opacity > 1) {
+            Log.e(TAG, "Invalid maximum obscuring opacity " + opacity
+                    + ", it should be >= 0 and <= 1, rejecting update.");
+            return;
+        }
+        mNative.setMaximumObscuringOpacityForTouch(opacity);
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index cd4a8f3..c7f4a49 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -29,6 +29,7 @@
 import static android.content.Context.KEYGUARD_SERVICE;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.UserHandle.USER_ALL;
+import static android.os.UserHandle.USER_SYSTEM;
 import static android.provider.DeviceConfig.NAMESPACE_AUTO_PIN_CONFIRMATION;
 
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
@@ -70,6 +71,7 @@
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
+import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.database.sqlite.SQLiteDatabase;
 import android.hardware.authsecret.IAuthSecret;
@@ -219,6 +221,8 @@
     private static final String PROFILE_KEY_NAME_ENCRYPT = "profile_key_name_encrypt_";
     private static final String PROFILE_KEY_NAME_DECRYPT = "profile_key_name_decrypt_";
 
+    private static final int HEADLESS_VENDOR_AUTH_SECRET_LENGTH = 32;
+
     // Order of holding lock: mSeparateChallengeLock -> mSpManager -> this
     // Do not call into ActivityManager while holding mSpManager lock.
     private final Object mSeparateChallengeLock = new Object();
@@ -267,6 +271,13 @@
     @VisibleForTesting
     protected boolean mHasSecureLockScreen;
 
+    @VisibleForTesting
+    protected final Object mHeadlessAuthSecretLock = new Object();
+
+    @VisibleForTesting
+    @GuardedBy("mHeadlessAuthSecretLock")
+    protected byte[] mAuthSecret;
+
     protected IGateKeeperService mGateKeeperService;
     protected IAuthSecret mAuthSecretService;
 
@@ -563,6 +574,15 @@
                 java.security.KeyStore ks) {
             return new ManagedProfilePasswordCache(ks, getUserManager());
         }
+
+        public boolean isHeadlessSystemUserMode() {
+            return UserManager.isHeadlessSystemUserMode();
+        }
+
+        public boolean isMainUserPermanentAdmin() {
+            return Resources.getSystem()
+                    .getBoolean(com.android.internal.R.bool.config_isMainUserPermanentAdmin);
+        }
     }
 
     public LockSettingsService(Context context) {
@@ -1697,7 +1717,7 @@
                 throw new IllegalStateException("password change failed");
             }
 
-            onSyntheticPasswordKnown(userId, sp);
+            onSyntheticPasswordUnlocked(userId, sp);
             setLockCredentialWithSpLocked(credential, sp, userId);
             sendCredentialsOnChangeIfRequired(credential, userId, isLockTiedToParent);
             return true;
@@ -2009,7 +2029,7 @@
                 Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId);
                 return;
             }
-            onSyntheticPasswordKnown(userId, result.syntheticPassword);
+            onSyntheticPasswordUnlocked(userId, result.syntheticPassword);
             unlockUserKey(userId, result.syntheticPassword);
         }
     }
@@ -2602,43 +2622,112 @@
         }
     }
 
-    private void onSyntheticPasswordKnown(@UserIdInt int userId, SyntheticPassword sp) {
+    private void onSyntheticPasswordCreated(@UserIdInt int userId, SyntheticPassword sp) {
+        onSyntheticPasswordKnown(userId, sp, true);
+    }
+
+    private void onSyntheticPasswordUnlocked(@UserIdInt int userId, SyntheticPassword sp) {
+        onSyntheticPasswordKnown(userId, sp, false);
+    }
+
+    private void onSyntheticPasswordKnown(
+            @UserIdInt int userId, SyntheticPassword sp, boolean justCreated) {
         if (mInjector.isGsiRunning()) {
             Slog.w(TAG, "Running in GSI; skipping calls to AuthSecret and RebootEscrow");
             return;
         }
 
-        mRebootEscrowManager.callToRebootEscrowIfNeeded(userId, sp.getVersion(),
-                sp.getSyntheticPassword());
-
-        callToAuthSecretIfNeeded(userId, sp);
+        mRebootEscrowManager.callToRebootEscrowIfNeeded(
+                userId, sp.getVersion(), sp.getSyntheticPassword());
+        callToAuthSecretIfNeeded(userId, sp, justCreated);
     }
 
-    private void callToAuthSecretIfNeeded(@UserIdInt int userId, SyntheticPassword sp) {
-        // If the given user is the primary user, pass the auth secret to the HAL.  Only the system
-        // user can be primary.  Check for the system user ID before calling getUserInfo(), as other
-        // users may still be under construction.
+    /**
+     * Handles generation, storage, and sending of the vendor auth secret. Here we try to retrieve
+     * the auth secret to send it to the auth secret HAL, generate a fresh secret if need be, store
+     * it encrypted on disk so that the given user can unlock it in future, and stash it in memory
+     * so that when future users are created they can also unlock it.
+     *
+     * <p>Called whenever the SP of a user is available, except in GSI.
+     */
+    private void callToAuthSecretIfNeeded(
+            @UserIdInt int userId, SyntheticPassword sp, boolean justCreated) {
         if (mAuthSecretService == null) {
+            // If there's no IAuthSecret service, we don't need to maintain a auth secret
             return;
         }
-        if (userId == UserHandle.USER_SYSTEM &&
-                mUserManager.getUserInfo(userId).isPrimary()) {
-            final byte[] secret = sp.deriveVendorAuthSecret();
-            try {
-                mAuthSecretService.setPrimaryUserCredential(secret);
-            } catch (RemoteException e) {
-                Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL", e);
+        // User may be partially created, so use the internal user manager interface
+        final UserManagerInternal userManagerInternal = mInjector.getUserManagerInternal();
+        final UserInfo userInfo = userManagerInternal.getUserInfo(userId);
+        if (userInfo == null) {
+            // User may be partially deleted, skip this.
+            return;
+        }
+        final byte[] authSecret;
+        if (!mInjector.isHeadlessSystemUserMode()) {
+            // On non-headless systems, the auth secret is derived from user 0's
+            // SP, and only user 0 passes it to the HAL.
+            if (userId != USER_SYSTEM) {
+                return;
             }
+            authSecret = sp.deriveVendorAuthSecret();
+        } else if (!mInjector.isMainUserPermanentAdmin() || !userInfo.isFull()) {
+            // Only full users can receive or pass on the auth secret.
+            // If there is no main permanent admin user, we don't try to create or send
+            // an auth secret, since there may sometimes be no full users.
+            return;
+        } else if (justCreated) {
+            if (userInfo.isMain()) {
+                // The first user is just being created, so we create a new auth secret
+                // at the same time.
+                Slog.i(TAG, "Generating new vendor auth secret and storing for user: " + userId);
+                authSecret = SecureRandomUtils.randomBytes(HEADLESS_VENDOR_AUTH_SECRET_LENGTH);
+                // Store it in memory, for when new users are created.
+                synchronized (mHeadlessAuthSecretLock) {
+                    mAuthSecret = authSecret;
+                }
+            } else {
+                // A new user is being created. Another user should already have logged in at
+                // this point, and therefore the auth secret should be stored in memory.
+                synchronized (mHeadlessAuthSecretLock) {
+                    authSecret = mAuthSecret;
+                }
+                if (authSecret == null) {
+                    Slog.e(TAG, "Creating non-main user " + userId
+                            + " but vendor auth secret is not in memory");
+                    return;
+                }
+            }
+            // Store the auth secret encrypted using the user's SP (which was just created).
+            mSpManager.writeVendorAuthSecret(authSecret, sp, userId);
+        } else {
+            // The user already exists, so the auth secret should be stored encrypted
+            // with that user's SP.
+            authSecret = mSpManager.readVendorAuthSecret(sp, userId);
+            if (authSecret == null) {
+                Slog.e(TAG, "Unable to read vendor auth secret for user: " + userId);
+                return;
+            }
+            // Store it in memory, for when new users are created.
+            synchronized (mHeadlessAuthSecretLock) {
+                mAuthSecret = authSecret;
+            }
+        }
+        Slog.i(TAG, "Sending vendor auth secret to IAuthSecret HAL as user: " + userId);
+        try {
+            mAuthSecretService.setPrimaryUserCredential(authSecret);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Failed to send vendor auth secret to IAuthSecret HAL", e);
         }
     }
 
     /**
      * Creates the synthetic password (SP) for the given user, protects it with an empty LSKF, and
      * protects the user's CE key with a key derived from the SP.
-     * <p>
-     * This is called just once in the lifetime of the user: at user creation time (possibly delayed
-     * until the time when Weaver is guaranteed to be available), or when upgrading from Android 13
-     * or earlier where users with no LSKF didn't necessarily have an SP.
+     *
+     * <p>This is called just once in the lifetime of the user: at user creation time (possibly
+     * delayed until the time when Weaver is guaranteed to be available), or when upgrading from
+     * Android 13 or earlier where users with no LSKF didn't necessarily have an SP.
      */
     @VisibleForTesting
     SyntheticPassword initializeSyntheticPassword(int userId) {
@@ -2653,7 +2742,7 @@
                     LockscreenCredential.createNone(), sp, userId);
             setCurrentLskfBasedProtectorId(protectorId, userId);
             setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey());
-            onSyntheticPasswordKnown(userId, sp);
+            onSyntheticPasswordCreated(userId, sp);
             return sp;
         }
     }
@@ -2720,7 +2809,7 @@
         }
         mStrongAuth.reportSuccessfulStrongAuthUnlock(userId);
 
-        onSyntheticPasswordKnown(userId, sp);
+        onSyntheticPasswordUnlocked(userId, sp);
     }
 
     private void setDeviceUnlockedForUser(int userId) {
@@ -3008,7 +3097,7 @@
                     + "verification.");
             return false;
         }
-        onSyntheticPasswordKnown(userId, result.syntheticPassword);
+        onSyntheticPasswordUnlocked(userId, result.syntheticPassword);
         setLockCredentialWithSpLocked(credential, result.syntheticPassword, userId);
         return true;
     }
diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
index ea000a0..c21c945 100644
--- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -21,6 +21,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UserIdInt;
 import android.app.admin.PasswordMetrics;
 import android.content.Context;
 import android.content.pm.UserInfo;
@@ -93,6 +94,9 @@
  *                     while the LSKF is nonempty.
  *     SP_E0_NAME, SP_P1_NAME: Information needed to create and use escrow token-based protectors.
  *                             Deleted when escrow token support is disabled for the user.
+ *     VENDOR_AUTH_SECRET_NAME: A copy of the secret passed using the IAuthSecret interface,
+ *                              encrypted using a secret derived from the SP using
+ *                              PERSONALIZATION_AUTHSECRET_ENCRYPTION_KEY.
  *
  *     For each protector, stored under the corresponding protector ID:
  *       SP_BLOB_NAME: The encrypted SP secret (the SP itself or the P0 value).  Always exists.
@@ -120,6 +124,7 @@
     private static final String PASSWORD_DATA_NAME = "pwd";
     private static final String WEAVER_SLOT_NAME = "weaver";
     private static final String PASSWORD_METRICS_NAME = "metrics";
+    private static final String VENDOR_AUTH_SECRET_NAME = "vendor_auth_secret";
 
     // used for files associated with the SP itself, not with a particular protector
     public static final long NULL_PROTECTOR_ID = 0L;
@@ -158,6 +163,8 @@
     private static final byte[] PERSONALIZATION_SP_GK_AUTH = "sp-gk-authentication".getBytes();
     private static final byte[] PERSONALIZATION_FBE_KEY = "fbe-key".getBytes();
     private static final byte[] PERSONALIZATION_AUTHSECRET_KEY = "authsecret-hal".getBytes();
+    private static final byte[] PERSONALIZATION_AUTHSECRET_ENCRYPTION_KEY =
+            "vendor-authsecret-encryption-key".getBytes();
     private static final byte[] PERSONALIZATION_SP_SPLIT = "sp-split".getBytes();
     private static final byte[] PERSONALIZATION_PASSWORD_HASH = "pw-hash".getBytes();
     private static final byte[] PERSONALIZATION_E0 = "e0-encryption".getBytes();
@@ -249,6 +256,10 @@
             return deriveSubkey(PERSONALIZATION_PASSWORD_METRICS);
         }
 
+        public byte[] deriveVendorAuthSecretEncryptionKey() {
+            return deriveSubkey(PERSONALIZATION_AUTHSECRET_ENCRYPTION_KEY);
+        }
+
         /**
          * Assigns escrow data to this synthetic password. This is a prerequisite to call
          * {@link SyntheticPassword#recreateFromEscrow}.
@@ -1737,4 +1748,25 @@
             mListeners.finishBroadcast();
         }
     }
+
+    public void writeVendorAuthSecret(
+            @NonNull final byte[] vendorAuthSecret,
+            @NonNull final SyntheticPassword sp,
+            @UserIdInt final int userId) {
+        final byte[] encrypted =
+                SyntheticPasswordCrypto.encrypt(
+                        sp.deriveVendorAuthSecretEncryptionKey(), new byte[0], vendorAuthSecret);
+        saveState(VENDOR_AUTH_SECRET_NAME, encrypted, NULL_PROTECTOR_ID, userId);
+        syncState(userId);
+    }
+
+    public @Nullable byte[] readVendorAuthSecret(
+            @NonNull final SyntheticPassword sp, @UserIdInt final int userId) {
+        final byte[] encrypted = loadState(VENDOR_AUTH_SECRET_NAME, NULL_PROTECTOR_ID, userId);
+        if (encrypted == null) {
+            return null;
+        }
+        return SyntheticPasswordCrypto.decrypt(
+                sp.deriveVendorAuthSecretEncryptionKey(), new byte[0], encrypted);
+    }
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 14ae2a7..ce921d6 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -24,6 +24,7 @@
 import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
 import static android.app.Notification.FLAG_INSISTENT;
 import static android.app.Notification.FLAG_NO_CLEAR;
+import static android.app.Notification.FLAG_NO_DISMISS;
 import static android.app.Notification.FLAG_ONGOING_EVENT;
 import static android.app.Notification.FLAG_ONLY_ALERT_ONCE;
 import static android.app.NotificationChannel.CONVERSATION_CHANNEL_ID_FORMAT;
@@ -576,6 +577,9 @@
     private float mInCallNotificationVolume;
     private Binder mCallNotificationToken = null;
 
+    private static final boolean ONGOING_DISMISSAL = SystemProperties.getBoolean(
+            "persist.sysui.notification.ongoing_dismissal", true);
+
     // used as a mutex for access to all active notifications & listeners
     final Object mNotificationLock = new Object();
     @GuardedBy("mNotificationLock")
@@ -1201,10 +1205,13 @@
                     id = r.getSbn().getId();
                 }
             }
-            int mustNotHaveFlags = FLAG_ONGOING_EVENT;
-            cancelNotification(callingUid, callingPid, pkg, tag, id, 0,
-                    mustNotHaveFlags,
-                    true, userId, REASON_CANCEL, nv.rank, nv.count,null);
+
+            int mustNotHaveFlags = ONGOING_DISMISSAL ? FLAG_NO_DISMISS : FLAG_ONGOING_EVENT;
+            cancelNotification(callingUid, callingPid, pkg, tag, id,
+                    /* mustHaveFlags= */ 0,
+                    /* mustNotHaveFlags= */ mustNotHaveFlags,
+                    /* sendDelete= */ true,
+                    userId, REASON_CANCEL, nv.rank, nv.count, /* listener= */ null);
             nv.recycle();
         }
 
@@ -6689,6 +6696,16 @@
                 (userId == UserHandle.USER_ALL) ? USER_SYSTEM : userId);
         Notification.addFieldsFromContext(ai, notification);
 
+        // Only notifications that can be non-dismissible can have the flag FLAG_NO_DISMISS
+        if (ONGOING_DISMISSAL) {
+            if (((notification.flags & FLAG_ONGOING_EVENT) > 0)
+                    && canBeNonDismissible(ai, notification)) {
+                notification.flags |= FLAG_NO_DISMISS;
+            } else {
+                notification.flags &= ~FLAG_NO_DISMISS;
+            }
+        }
+
         int canColorize = mPackageManagerClient.checkPermission(
                 android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, pkg);
         if (canColorize == PERMISSION_GRANTED) {
@@ -6775,6 +6792,23 @@
         checkRemoteViews(pkg, tag, id, notification);
     }
 
+    /**
+     * Whether a notification can be non-dismissible.
+     * A notification should be dismissible, unless it's exempted for some reason.
+     */
+    private boolean canBeNonDismissible(ApplicationInfo ai, Notification notification) {
+        // Check if the app is on the system partition, or an update to an app on the system
+        // partition.
+        boolean isSystemAppExempt = (ai.flags
+                & (ApplicationInfo.FLAG_UPDATED_SYSTEM_APP | ApplicationInfo.FLAG_SYSTEM)) > 0;
+        return isSystemAppExempt || notification.isMediaNotification() || isEnterpriseExempted();
+    }
+
+    // TODO: b/266237746 Enterprise app exemptions
+    private boolean isEnterpriseExempted() {
+        return false;
+    }
+
     private void checkRemoteViews(String pkg, String tag, int id, Notification notification) {
         if (removeRemoteView(pkg, tag, id, notification.contentView)) {
             notification.contentView = null;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index f77d38f..55dcaf6 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -3996,7 +3996,14 @@
                 return;
             }
         }
-        r.run();
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            // This will call into StagingManager which might trigger external callbacks
+            r.run();
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
     }
 
     @Override
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 9c91879..7e7205d 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -120,7 +120,7 @@
 import com.android.server.pm.verify.domain.DomainVerificationLegacySettings;
 import com.android.server.pm.verify.domain.DomainVerificationManagerInternal;
 import com.android.server.pm.verify.domain.DomainVerificationPersistence;
-import com.android.server.security.FileIntegrityLocal;
+import com.android.server.security.FileIntegrity;
 import com.android.server.utils.Slogf;
 import com.android.server.utils.Snappable;
 import com.android.server.utils.SnapshotCache;
@@ -2714,8 +2714,8 @@
             }
 
             try {
-                FileIntegrityLocal.setUpFsVerity(mSettingsFilename.getAbsolutePath());
-                FileIntegrityLocal.setUpFsVerity(mSettingsReserveCopyFilename.getAbsolutePath());
+                FileIntegrity.setUpFsVerity(mSettingsFilename);
+                FileIntegrity.setUpFsVerity(mSettingsReserveCopyFilename);
             } catch (IOException e) {
                 Slog.e(TAG, "Failed to verity-protect settings", e);
             }
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index 26a990c..a9edce1 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -553,11 +553,12 @@
      * switched to.
      *
      * <p>Otherwise, in {@link UserManager#isHeadlessSystemUserMode() headless system user mode},
-     * this will be the user who was last in the foreground on this device. If there is no
-     * switchable user on the device, a new user will be created and its id will be returned.
+     * this will be the user who was last in the foreground on this device.
      *
-     * <p>In non-headless system user mode, the return value will be {@link UserHandle#USER_SYSTEM}.
+     * <p>In non-headless system user mode, the return value will be
+     * {@link android.os.UserHandle#USER_SYSTEM}.
+
+     * @throws UserManager.CheckedUserOperationException if no switchable user can be found
      */
-    public abstract @UserIdInt int getBootUser()
-            throws UserManager.CheckedUserOperationException;
+    public abstract @UserIdInt int getBootUser() throws UserManager.CheckedUserOperationException;
 }
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 762d1f6..ce7dc5b 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -5049,6 +5049,8 @@
         //...then external ones
         Intent addedIntent = new Intent(Intent.ACTION_USER_ADDED);
         addedIntent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+        // In HSUM, MainUser might be created before PHASE_ACTIVITY_MANAGER_READY has been sent.
+        addedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
         addedIntent.putExtra(Intent.EXTRA_USER_HANDLE, userInfo.id);
         // Also, add the UserHandle for mainline modules which can't use the @hide
         // EXTRA_USER_HANDLE.
@@ -6758,18 +6760,6 @@
         return mLocalService.isUserInitialized(userId);
     }
 
-    /**
-     * Creates a new user, intended to be the initial user on a device in headless system user mode.
-     */
-    private UserInfo createInitialUserForHsum() throws UserManager.CheckedUserOperationException {
-        final int flags = UserInfo.FLAG_ADMIN | UserInfo.FLAG_MAIN;
-
-        // Null name will be replaced with "Owner" on-demand to allow for localisation.
-        return createUserInternalUnchecked(/* name= */ null, UserManager.USER_TYPE_FULL_SECONDARY,
-                flags, UserHandle.USER_NULL, /* preCreate= */ false,
-                /* disallowedPackages= */ null, /* token= */ null);
-    }
-
     private class LocalService extends UserManagerInternal {
         @Override
         public void setDevicePolicyUserRestrictions(@UserIdInt int originatingUserId,
@@ -7249,15 +7239,9 @@
                         }
                     }
                 }
-                // No switchable users. Create the initial user.
-                final UserInfo newInitialUser = createInitialUserForHsum();
-                if (newInitialUser == null) {
-                    throw new UserManager.CheckedUserOperationException(
-                            "Initial user creation failed", USER_OPERATION_ERROR_UNKNOWN);
-                }
-                Slogf.i(LOG_TAG,
-                        "No switchable users. Boot user is new user %d", newInitialUser.id);
-                return newInitialUser.id;
+                // No switchable users found. Uh oh!
+                throw new UserManager.CheckedUserOperationException(
+                        "No switchable users found", USER_OPERATION_ERROR_UNKNOWN);
             }
             // Not HSUM, return system user.
             return UserHandle.USER_SYSTEM;
@@ -7437,14 +7421,14 @@
 
     /**
      * Returns true, when user has {@link UserInfo#FLAG_MAIN} and system property
-     * {@link com.android.internal.R.bool.isMainUserPermanentAdmin} is true.
+     * {@link com.android.internal.R.bool#config_isMainUserPermanentAdmin} is true.
      */
     private boolean isNonRemovableMainUser(UserInfo userInfo) {
         return userInfo.isMain() && isMainUserPermanentAdmin();
     }
 
     /**
-     * Returns true, when {@link com.android.internal.R.bool.isMainUserPermanentAdmin} is true.
+     * Returns true if {@link com.android.internal.R.bool#config_isMainUserPermanentAdmin} is true.
      * If the main user is a permanent admin user it can't be deleted
      * or downgraded to non-admin status.
      */
diff --git a/services/core/java/com/android/server/security/FileIntegrityLocal.java b/services/core/java/com/android/server/security/FileIntegrity.java
similarity index 63%
rename from services/core/java/com/android/server/security/FileIntegrityLocal.java
rename to services/core/java/com/android/server/security/FileIntegrity.java
index 8c7219b..7b87d99 100644
--- a/services/core/java/com/android/server/security/FileIntegrityLocal.java
+++ b/services/core/java/com/android/server/security/FileIntegrity.java
@@ -18,19 +18,22 @@
 
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
+import android.os.ParcelFileDescriptor;
 
 import com.android.internal.security.VerityUtils;
 
+import java.io.File;
 import java.io.IOException;
 
+
 /**
  * In-process API for server side FileIntegrity related infrastructure.
  *
  * @hide
  */
 @SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
-public final class FileIntegrityLocal {
-    private FileIntegrityLocal() {}
+public final class FileIntegrity {
+    private FileIntegrity() {}
 
     /**
      * Enables fs-verity, if supported by the filesystem.
@@ -38,7 +41,18 @@
      * @hide
      */
     @SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
-    public static void setUpFsVerity(@NonNull String filePath) throws IOException {
-        VerityUtils.setUpFsverity(filePath);
+    public static void setUpFsVerity(@NonNull File file) throws IOException {
+        VerityUtils.setUpFsverity(file.getAbsolutePath());
+    }
+
+    /**
+     * Enables fs-verity, if supported by the filesystem.
+     * @see <a href="https://www.kernel.org/doc/html/latest/filesystems/fsverity.html">
+     * @hide
+     */
+    @SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
+    public static void setUpFsVerity(@NonNull ParcelFileDescriptor parcelFileDescriptor)
+            throws IOException {
+        VerityUtils.setUpFsverity(parcelFileDescriptor.getFd());
     }
 }
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 5fd57e3..541e0d8 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -3986,6 +3986,56 @@
                 args, callback, resultReceiver);
     }
 
+    private void dumpWallpaper(WallpaperData wallpaper, PrintWriter pw) {
+        pw.print(" User "); pw.print(wallpaper.userId);
+        pw.print(": id="); pw.print(wallpaper.wallpaperId);
+        pw.print(": mWhich="); pw.print(wallpaper.mWhich);
+        pw.print(": mSystemWasBoth="); pw.println(wallpaper.mSystemWasBoth);
+        pw.println(" Display state:");
+        mWallpaperDisplayHelper.forEachDisplayData(wpSize -> {
+            pw.print("  displayId=");
+            pw.println(wpSize.mDisplayId);
+            pw.print("  mWidth=");
+            pw.print(wpSize.mWidth);
+            pw.print("  mHeight=");
+            pw.println(wpSize.mHeight);
+            pw.print("  mPadding="); pw.println(wpSize.mPadding);
+        });
+        pw.print("  mCropHint="); pw.println(wallpaper.cropHint);
+        pw.print("  mName=");  pw.println(wallpaper.name);
+        pw.print("  mAllowBackup="); pw.println(wallpaper.allowBackup);
+        pw.print("  mWallpaperComponent="); pw.println(wallpaper.wallpaperComponent);
+        pw.print("  mWallpaperDimAmount="); pw.println(wallpaper.mWallpaperDimAmount);
+        pw.print("  isColorExtracted="); pw.println(wallpaper.mIsColorExtractedFromDim);
+        pw.println("  mUidToDimAmount:");
+        for (Map.Entry<Integer, Float> entry : wallpaper.mUidToDimAmount.entrySet()) {
+            pw.print("    UID="); pw.print(entry.getKey());
+            pw.print(" dimAmount="); pw.println(entry.getValue());
+        }
+        if (wallpaper.connection != null) {
+            WallpaperConnection conn = wallpaper.connection;
+            pw.print("  Wallpaper connection ");
+            pw.print(conn);
+            pw.println(":");
+            if (conn.mInfo != null) {
+                pw.print("    mInfo.component=");
+                pw.println(conn.mInfo.getComponent());
+            }
+            conn.forEachDisplayConnector(connector -> {
+                pw.print("     mDisplayId=");
+                pw.println(connector.mDisplayId);
+                pw.print("     mToken=");
+                pw.println(connector.mToken);
+                pw.print("     mEngine=");
+                pw.println(connector.mEngine);
+            });
+            pw.print("    mService=");
+            pw.println(conn.mService);
+            pw.print("    mLastDiedTime=");
+            pw.println(wallpaper.lastDiedTime - SystemClock.uptimeMillis());
+        }
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
@@ -3996,90 +4046,15 @@
         synchronized (mLock) {
             pw.println("System wallpaper state:");
             for (int i = 0; i < mWallpaperMap.size(); i++) {
-                WallpaperData wallpaper = mWallpaperMap.valueAt(i);
-                pw.print(" User "); pw.print(wallpaper.userId);
-                pw.print(": id="); pw.println(wallpaper.wallpaperId);
-                pw.println(" Display state:");
-                mWallpaperDisplayHelper.forEachDisplayData(wpSize -> {
-                    pw.print("  displayId=");
-                    pw.println(wpSize.mDisplayId);
-                    pw.print("  mWidth=");
-                    pw.print(wpSize.mWidth);
-                    pw.print("  mHeight=");
-                    pw.println(wpSize.mHeight);
-                    pw.print("  mPadding="); pw.println(wpSize.mPadding);
-                });
-                pw.print("  mCropHint="); pw.println(wallpaper.cropHint);
-                pw.print("  mName=");  pw.println(wallpaper.name);
-                pw.print("  mAllowBackup="); pw.println(wallpaper.allowBackup);
-                pw.print("  mWallpaperComponent="); pw.println(wallpaper.wallpaperComponent);
-                pw.print("  mWallpaperDimAmount="); pw.println(wallpaper.mWallpaperDimAmount);
-                pw.print("  isColorExtracted="); pw.println(wallpaper.mIsColorExtractedFromDim);
-                pw.println("  mUidToDimAmount:");
-                for (Map.Entry<Integer, Float> entry : wallpaper.mUidToDimAmount.entrySet()) {
-                    pw.print("    UID="); pw.print(entry.getKey());
-                    pw.print(" dimAmount="); pw.println(entry.getValue());
-                }
-                if (wallpaper.connection != null) {
-                    WallpaperConnection conn = wallpaper.connection;
-                    pw.print("  Wallpaper connection ");
-                    pw.print(conn);
-                    pw.println(":");
-                    if (conn.mInfo != null) {
-                        pw.print("    mInfo.component=");
-                        pw.println(conn.mInfo.getComponent());
-                    }
-                    conn.forEachDisplayConnector(connector -> {
-                        pw.print("     mDisplayId=");
-                        pw.println(connector.mDisplayId);
-                        pw.print("     mToken=");
-                        pw.println(connector.mToken);
-                        pw.print("     mEngine=");
-                        pw.println(connector.mEngine);
-                    });
-                    pw.print("    mService=");
-                    pw.println(conn.mService);
-                    pw.print("    mLastDiedTime=");
-                    pw.println(wallpaper.lastDiedTime - SystemClock.uptimeMillis());
-                }
+                dumpWallpaper(mWallpaperMap.valueAt(i), pw);
             }
             pw.println("Lock wallpaper state:");
             for (int i = 0; i < mLockWallpaperMap.size(); i++) {
-                WallpaperData wallpaper = mLockWallpaperMap.valueAt(i);
-                pw.print(" User "); pw.print(wallpaper.userId);
-                pw.print(": id="); pw.println(wallpaper.wallpaperId);
-                pw.print("  mCropHint="); pw.println(wallpaper.cropHint);
-                pw.print("  mName=");  pw.println(wallpaper.name);
-                pw.print("  mAllowBackup="); pw.println(wallpaper.allowBackup);
-                pw.print("  mWallpaperDimAmount="); pw.println(wallpaper.mWallpaperDimAmount);
+                dumpWallpaper(mLockWallpaperMap.valueAt(i), pw);
             }
             pw.println("Fallback wallpaper state:");
-            pw.print(" User "); pw.print(mFallbackWallpaper.userId);
-            pw.print(": id="); pw.println(mFallbackWallpaper.wallpaperId);
-            pw.print("  mCropHint="); pw.println(mFallbackWallpaper.cropHint);
-            pw.print("  mName=");  pw.println(mFallbackWallpaper.name);
-            pw.print("  mAllowBackup="); pw.println(mFallbackWallpaper.allowBackup);
-            if (mFallbackWallpaper.connection != null) {
-                WallpaperConnection conn = mFallbackWallpaper.connection;
-                pw.print("  Fallback Wallpaper connection ");
-                pw.print(conn);
-                pw.println(":");
-                if (conn.mInfo != null) {
-                    pw.print("    mInfo.component=");
-                    pw.println(conn.mInfo.getComponent());
-                }
-                conn.forEachDisplayConnector(connector -> {
-                    pw.print("     mDisplayId=");
-                    pw.println(connector.mDisplayId);
-                    pw.print("     mToken=");
-                    pw.println(connector.mToken);
-                    pw.print("     mEngine=");
-                    pw.println(connector.mEngine);
-                });
-                pw.print("    mService=");
-                pw.println(conn.mService);
-                pw.print("    mLastDiedTime=");
-                pw.println(mFallbackWallpaper.lastDiedTime - SystemClock.uptimeMillis());
+            if (mFallbackWallpaper != null) {
+                dumpWallpaper(mFallbackWallpaper, pw);
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 2f82167..80f660e 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -4748,16 +4748,27 @@
         updateResumedAppTrace(r);
         mLastResumedActivity = r;
 
-        final boolean changed = r.mDisplayContent.setFocusedApp(r);
-        if (changed) {
-            mWindowManager.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL,
-                    true /*updateInputWindows*/);
-        }
-        if (prevTask == null || task != prevTask) {
-            if (prevTask != null) {
-                mTaskChangeNotificationController.notifyTaskFocusChanged(prevTask.mTaskId, false);
+        // Don't take focus when transient launching. We don't want the app to know anything
+        // until we've committed to the gesture. The focus will be transferred at the end of
+        // the transition (if the transient launch is committed) or early if explicitly requested
+        // via `setFocused*`.
+        if (!getTransitionController().isTransientCollect(r)) {
+            final Task prevFocusTask = r.mDisplayContent.mFocusedApp != null
+                    ? r.mDisplayContent.mFocusedApp.getTask() : null;
+            final boolean changed = r.mDisplayContent.setFocusedApp(r);
+            if (changed) {
+                mWindowManager.updateFocusedWindowLocked(UPDATE_FOCUS_NORMAL,
+                        true /*updateInputWindows*/);
             }
-            mTaskChangeNotificationController.notifyTaskFocusChanged(task.mTaskId, true);
+            if (task != prevFocusTask) {
+                if (prevFocusTask != null) {
+                    mTaskChangeNotificationController.notifyTaskFocusChanged(
+                            prevFocusTask.mTaskId, false);
+                }
+                mTaskChangeNotificationController.notifyTaskFocusChanged(task.mTaskId, true);
+            }
+        }
+        if (task != prevTask) {
             mTaskSupervisor.mRecentTasks.add(task);
         }
 
diff --git a/services/core/java/com/android/server/wm/DeviceStateController.java b/services/core/java/com/android/server/wm/DeviceStateController.java
index 7d9a4ec..3f28522 100644
--- a/services/core/java/com/android/server/wm/DeviceStateController.java
+++ b/services/core/java/com/android/server/wm/DeviceStateController.java
@@ -24,6 +24,7 @@
 import android.os.HandlerExecutor;
 
 import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.ArrayUtils;
 
 import java.util.ArrayList;
@@ -48,9 +49,12 @@
     private final int[] mRearDisplayDeviceStates;
     @NonNull
     private final int[] mReverseRotationAroundZAxisStates;
+    @GuardedBy("this")
     @NonNull
     private final List<Consumer<DeviceState>> mDeviceStateCallbacks = new ArrayList<>();
 
+    private final boolean mMatchBuiltInDisplayOrientationToDefaultDisplay;
+
     @Nullable
     private DeviceState mLastDeviceState;
     private int mCurrentState;
@@ -72,20 +76,19 @@
                 .getIntArray(R.array.config_rearDisplayDeviceStates);
         mReverseRotationAroundZAxisStates = context.getResources()
                 .getIntArray(R.array.config_deviceStatesToReverseDefaultDisplayRotationAroundZAxis);
+        mMatchBuiltInDisplayOrientationToDefaultDisplay = context.getResources()
+                .getBoolean(R.bool
+                        .config_matchSecondaryInternalDisplaysOrientationToReverseDefaultDisplay);
 
         if (mDeviceStateManager != null) {
             mDeviceStateManager.registerCallback(new HandlerExecutor(handler), this);
         }
     }
 
-    void unregisterFromDeviceStateManager() {
-        if (mDeviceStateManager != null) {
-            mDeviceStateManager.unregisterCallback(this);
-        }
-    }
-
     void registerDeviceStateCallback(@NonNull Consumer<DeviceState> callback) {
-        mDeviceStateCallbacks.add(callback);
+        synchronized (this) {
+            mDeviceStateCallbacks.add(callback);
+        }
     }
 
     /**
@@ -95,6 +98,15 @@
         return ArrayUtils.contains(mReverseRotationAroundZAxisStates, mCurrentState);
     }
 
+    /**
+     * @return true if non-default built-in displays should match the default display's rotation.
+     */
+    boolean shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay() {
+        // TODO(b/265991392): This should come from display_settings.xml once it's easier to
+        //  extend with complex configurations.
+        return mMatchBuiltInDisplayOrientationToDefaultDisplay;
+    }
+
     @Override
     public void onStateChanged(int state) {
         mCurrentState = state;
@@ -115,8 +127,10 @@
         if (mLastDeviceState == null || !mLastDeviceState.equals(deviceState)) {
             mLastDeviceState = deviceState;
 
-            for (Consumer<DeviceState> callback : mDeviceStateCallbacks) {
-                callback.accept(mLastDeviceState);
+            synchronized (this) {
+                for (Consumer<DeviceState> callback : mDeviceStateCallbacks) {
+                    callback.accept(mLastDeviceState);
+                }
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java
index de63191..c2ddb41 100644
--- a/services/core/java/com/android/server/wm/DisplayArea.java
+++ b/services/core/java/com/android/server/wm/DisplayArea.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
@@ -246,7 +247,22 @@
                 || orientation == ActivityInfo.SCREEN_ORIENTATION_NOSENSOR) {
             return false;
         }
-        return getIgnoreOrientationRequest();
+        return getIgnoreOrientationRequest()
+                && !shouldRespectOrientationRequestDueToPerAppOverride();
+    }
+
+    private boolean shouldRespectOrientationRequestDueToPerAppOverride() {
+        if (mDisplayContent == null) {
+            return false;
+        }
+        ActivityRecord activity = mDisplayContent.topRunningActivity(
+                /* considerKeyguardState= */ true);
+        return activity != null && activity.getTaskFragment() != null
+                // Checking TaskFragment rather than ActivityRecord to ensure that transition
+                // between fullscreen and PiP would work well. Checking TaskFragment rather than
+                // Task to ensure that Activity Embedding is excluded.
+                && activity.getTaskFragment().getWindowingMode() == WINDOWING_MODE_FULLSCREEN
+                && activity.mLetterboxUiController.isOverrideRespectRequestedOrientationEnabled();
     }
 
     boolean getIgnoreOrientationRequest() {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 4174bfe..faed11b 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -595,7 +595,8 @@
     final FixedRotationTransitionListener mFixedRotationTransitionListener =
             new FixedRotationTransitionListener();
 
-    private final DeviceStateController mDeviceStateController;
+    @VisibleForTesting
+    final DeviceStateController mDeviceStateController;
     private final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
     final RemoteDisplayChangeController mRemoteDisplayChangeController;
 
@@ -1091,7 +1092,8 @@
      * @param display May not be null.
      * @param root {@link RootWindowContainer}
      */
-    DisplayContent(Display display, RootWindowContainer root) {
+    DisplayContent(Display display, RootWindowContainer root,
+            @NonNull DeviceStateController deviceStateController) {
         super(root.mWindowManager, "DisplayContent", FEATURE_ROOT);
         if (mWmService.mRoot.getDisplayContent(display.getDisplayId()) != null) {
             throw new IllegalArgumentException("Display with ID=" + display.getDisplayId()
@@ -1151,11 +1153,11 @@
                     mWmService.mAtmService.getRecentTasks().getInputListener());
         }
 
-        mDeviceStateController = new DeviceStateController(mWmService.mContext, mWmService.mH);
+        mDeviceStateController = deviceStateController;
 
         mDisplayPolicy = new DisplayPolicy(mWmService, this);
         mDisplayRotation = new DisplayRotation(mWmService, this, mDisplayInfo.address,
-                mDeviceStateController);
+                mDeviceStateController, root.getDisplayRotationCoordinator());
 
         final Consumer<DeviceStateController.DeviceState> deviceStateConsumer =
                 (@NonNull DeviceStateController.DeviceState newFoldState) -> {
@@ -2163,6 +2165,10 @@
                 w.seamlesslyRotateIfAllowed(transaction, oldRotation, rotation, rotateSeamlessly);
             }, true /* traverseTopToBottom */);
             mPinnedTaskController.startSeamlessRotationIfNeeded(transaction, oldRotation, rotation);
+            if (!mDisplayRotation.hasSeamlessRotatingWindow()) {
+                // Make sure DisplayRotation#isRotatingSeamlessly() will return false.
+                mDisplayRotation.cancelSeamlessRotation();
+            }
         }
 
         mWmService.mDisplayManagerInternal.performTraversal(transaction);
@@ -3322,7 +3328,7 @@
             mTransitionController.unregisterLegacyListener(mFixedRotationTransitionListener);
             handleAnimatingStoppedAndTransition();
             mWmService.stopFreezingDisplayLocked();
-            mDeviceStateController.unregisterFromDeviceStateManager();
+            mDisplayRotation.removeDefaultDisplayRotationChangedCallback();
             super.removeImmediately();
             if (DEBUG_DISPLAY) Slog.v(TAG_WM, "Removing display=" + this);
             mPointerEventDispatcher.dispose();
@@ -3804,6 +3810,17 @@
      */
     boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows,
             int topFocusedDisplayId) {
+        // Don't re-assign focus automatically away from a should-keep-focus app window.
+        // `findFocusedWindow` will always grab the transient-launch app since it is "on top" which
+        // would create a mismatch, so just early-out here.
+        if (mCurrentFocus != null && mTransitionController.shouldKeepFocus(mCurrentFocus)
+                // This is only keeping focus, so don't early-out if the focused-app has been
+                // explicitly changed (eg. via setFocusedTask).
+                && mFocusedApp != null && mCurrentFocus.isDescendantOf(mFocusedApp)
+                && mCurrentFocus.isVisible() && mCurrentFocus.isFocusable()) {
+            ProtoLog.v(WM_DEBUG_FOCUS, "Current transition prevents automatic focus change");
+            return false;
+        }
         WindowState newFocus = findFocusedWindowIfNeeded(topFocusedDisplayId);
         if (mCurrentFocus == newFocus) {
             return false;
@@ -4927,7 +4944,7 @@
         mInsetsStateController.getImeSourceProvider().checkShowImePostLayout();
 
         mLastHasContent = mTmpApplySurfaceChangesTransactionState.displayHasContent;
-        if (!mWmService.mDisplayFrozen) {
+        if (!mWmService.mDisplayFrozen && !mDisplayRotation.isRotatingSeamlessly()) {
             mWmService.mDisplayManagerInternal.setDisplayProperties(mDisplayId,
                     mLastHasContent,
                     mTmpApplySurfaceChangesTransactionState.preferredRefreshRate,
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 3404279..7071aa7 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -58,6 +58,7 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.ArraySet;
+import android.util.RotationUtils;
 import android.util.Slog;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
@@ -120,6 +121,11 @@
     private FoldController mFoldController;
     @NonNull
     private final DeviceStateController mDeviceStateController;
+    @NonNull
+    private final DisplayRotationCoordinator mDisplayRotationCoordinator;
+    @NonNull
+    @VisibleForTesting
+    final Runnable mDefaultDisplayRotationChangedCallback;
 
     @ScreenOrientation
     private int mCurrentAppOrientation = SCREEN_ORIENTATION_UNSPECIFIED;
@@ -221,17 +227,19 @@
     private boolean mDemoRotationLock;
 
     DisplayRotation(WindowManagerService service, DisplayContent displayContent,
-            DisplayAddress displayAddress, @NonNull DeviceStateController deviceStateController) {
+            DisplayAddress displayAddress, @NonNull DeviceStateController deviceStateController,
+            @NonNull DisplayRotationCoordinator displayRotationCoordinator) {
         this(service, displayContent, displayAddress, displayContent.getDisplayPolicy(),
                 service.mDisplayWindowSettings, service.mContext, service.getWindowManagerLock(),
-                deviceStateController);
+                deviceStateController, displayRotationCoordinator);
     }
 
     @VisibleForTesting
     DisplayRotation(WindowManagerService service, DisplayContent displayContent,
             DisplayAddress displayAddress, DisplayPolicy displayPolicy,
             DisplayWindowSettings displayWindowSettings, Context context, Object lock,
-            @NonNull DeviceStateController deviceStateController) {
+            @NonNull DeviceStateController deviceStateController,
+            @NonNull DisplayRotationCoordinator displayRotationCoordinator) {
         mService = service;
         mDisplayContent = displayContent;
         mDisplayPolicy = displayPolicy;
@@ -252,6 +260,19 @@
         int defaultRotation = readDefaultDisplayRotation(displayAddress);
         mRotation = defaultRotation;
 
+        mDisplayRotationCoordinator = displayRotationCoordinator;
+        if (isDefaultDisplay) {
+            mDisplayRotationCoordinator.setDefaultDisplayDefaultRotation(mRotation);
+        }
+        mDefaultDisplayRotationChangedCallback = this::updateRotationAndSendNewConfigIfChanged;
+
+        if (DisplayRotationCoordinator.isSecondaryInternalDisplay(displayContent)
+                && mDeviceStateController
+                        .shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay()) {
+            mDisplayRotationCoordinator.setDefaultDisplayRotationChangedCallback(
+                    mDefaultDisplayRotationChangedCallback);
+        }
+
         if (isDefaultDisplay) {
             final Handler uiHandler = UiThread.getHandler();
             mOrientationListener =
@@ -494,8 +515,11 @@
             return false;
         }
 
+        @Surface.Rotation
         final int oldRotation = mRotation;
+        @ScreenOrientation
         final int lastOrientation = mLastOrientation;
+        @Surface.Rotation
         int rotation = rotationForOrientation(lastOrientation, oldRotation);
         // Use the saved rotation for tabletop mode, if set.
         if (mFoldController != null && mFoldController.shouldRevertOverriddenRotation()) {
@@ -507,6 +531,14 @@
                     Surface.rotationToString(oldRotation),
                     Surface.rotationToString(prevRotation));
         }
+
+        if (DisplayRotationCoordinator.isSecondaryInternalDisplay(mDisplayContent)
+                && mDeviceStateController
+                        .shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay()) {
+            rotation = RotationUtils.reverseRotationDirectionAroundZAxis(
+                    mDisplayRotationCoordinator.getDefaultDisplayCurrentRotation());
+        }
+
         ProtoLog.v(WM_DEBUG_ORIENTATION,
                 "Computed rotation=%s (%d) for display id=%d based on lastOrientation=%s (%d) and "
                         + "oldRotation=%s (%d)",
@@ -525,6 +557,10 @@
             return false;
         }
 
+        if (isDefaultDisplay) {
+            mDisplayRotationCoordinator.onDefaultDisplayRotationChanged(rotation);
+        }
+
         // Preemptively cancel the running recents animation -- SysUI can't currently handle this
         // case properly since the signals it receives all happen post-change. We do this earlier
         // in the rotation flow, since DisplayContent.updateDisplayOverrideConfigurationLocked seems
@@ -1142,17 +1178,12 @@
             return mUserRotation;
         }
 
+        @Surface.Rotation
         int sensorRotation = mOrientationListener != null
                 ? mOrientationListener.getProposedRotation() // may be -1
                 : -1;
         if (mDeviceStateController.shouldReverseRotationDirectionAroundZAxis()) {
-            // Flipping 270 and 90 has the same effect as changing the direction which rotation is
-            // applied.
-            if (sensorRotation == Surface.ROTATION_90) {
-                sensorRotation = Surface.ROTATION_270;
-            } else if (sensorRotation == Surface.ROTATION_270) {
-                sensorRotation = Surface.ROTATION_90;
-            }
+            sensorRotation = RotationUtils.reverseRotationDirectionAroundZAxis(sensorRotation);
         }
         mLastSensorRotation = sensorRotation;
         if (sensorRotation < 0) {
@@ -1167,6 +1198,7 @@
         final boolean deskDockEnablesAccelerometer =
                 mDisplayPolicy.isDeskDockEnablesAccelerometer();
 
+        @Surface.Rotation
         final int preferredRotation;
         if (!isDefaultDisplay) {
             // For secondary displays we ignore things like displays sensors, docking mode and
@@ -1536,6 +1568,12 @@
         return shouldUpdateRotation;
     }
 
+    void removeDefaultDisplayRotationChangedCallback() {
+        if (DisplayRotationCoordinator.isSecondaryInternalDisplay(mDisplayContent)) {
+            mDisplayRotationCoordinator.removeDefaultDisplayRotationChangedCallback();
+        }
+    }
+
     void dump(String prefix, PrintWriter pw) {
         pw.println(prefix + "DisplayRotation");
         pw.println(prefix + "  mCurrentAppOrientation="
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCoordinator.java b/services/core/java/com/android/server/wm/DisplayRotationCoordinator.java
new file mode 100644
index 0000000..ae3787c
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DisplayRotationCoordinator.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.Display;
+import android.view.Surface;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Singleton for coordinating rotation across multiple displays. Used to notify non-default
+ * displays when the default display rotates.
+ *
+ * Note that this class does not need locking because it is always protected by WindowManagerService
+ * mGlobalLock.
+ */
+class DisplayRotationCoordinator {
+
+    private static final String TAG = "DisplayRotationCoordinator";
+
+    @Surface.Rotation
+    private int mDefaultDisplayDefaultRotation;
+
+    @Nullable
+    @VisibleForTesting
+    Runnable mDefaultDisplayRotationChangedCallback;
+
+    @Surface.Rotation
+    private int mDefaultDisplayCurrentRotation;
+
+    /**
+     * Notifies clients when the default display rotation changes.
+     */
+    void onDefaultDisplayRotationChanged(@Surface.Rotation int rotation) {
+        mDefaultDisplayCurrentRotation = rotation;
+
+        if (mDefaultDisplayRotationChangedCallback != null) {
+            mDefaultDisplayRotationChangedCallback.run();
+        }
+    }
+
+    void setDefaultDisplayDefaultRotation(@Surface.Rotation int rotation) {
+        mDefaultDisplayDefaultRotation = rotation;
+    }
+
+    @Surface.Rotation
+    int getDefaultDisplayCurrentRotation() {
+        return mDefaultDisplayCurrentRotation;
+    }
+
+    /**
+     * Register a callback to be notified when the default display's rotation changes. Clients can
+     * query the default display's current rotation via {@link #getDefaultDisplayCurrentRotation()}.
+     */
+    void setDefaultDisplayRotationChangedCallback(@NonNull Runnable callback) {
+        if (mDefaultDisplayRotationChangedCallback != null) {
+            throw new UnsupportedOperationException("Multiple clients unsupported");
+        }
+
+        mDefaultDisplayRotationChangedCallback = callback;
+
+        if (mDefaultDisplayCurrentRotation != mDefaultDisplayDefaultRotation) {
+            callback.run();
+        }
+    }
+
+    /**
+     * Removes the callback that was added via
+     * {@link #setDefaultDisplayRotationChangedCallback(Runnable)}.
+     */
+    void removeDefaultDisplayRotationChangedCallback() {
+        mDefaultDisplayRotationChangedCallback = null;
+    }
+
+    static boolean isSecondaryInternalDisplay(@NonNull DisplayContent displayContent) {
+        if (displayContent.isDefaultDisplay) {
+            return false;
+        } else if (displayContent.mDisplay == null) {
+            return false;
+        }
+        return displayContent.mDisplay.getType() == Display.TYPE_INTERNAL;
+    }
+}
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index 3e1105b..8c59548 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -405,8 +405,12 @@
             // Apply recents input consumer when the focusing window is in recents animation.
             final boolean shouldApplyRecentsInputConsumer = (recentsAnimationController != null
                     && recentsAnimationController.shouldApplyInputConsumer(focus.mActivityRecord))
-                    // Shell transitions doesn't use RecentsAnimationController
-                    || getWeak(mActiveRecentsActivity) != null && focus.inTransition();
+                    // Shell transitions doesn't use RecentsAnimationController but we still
+                    // have carryover legacy logic that relies on the consumer.
+                    || (getWeak(mActiveRecentsActivity) != null && focus.inTransition()
+                            // only take focus from the recents activity to avoid intercepting
+                            // events before the gesture officially starts.
+                            && focus.isActivityTypeHomeOrRecents());
             if (shouldApplyRecentsInputConsumer) {
                 if (mInputFocus != recentsAnimationInputConsumer.mWindowHandle.token) {
                     requestFocus(recentsAnimationInputConsumer.mWindowHandle.token,
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 8d6de58..9681789 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -25,6 +25,7 @@
 import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION;
 import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
 import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
+import static android.content.pm.ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION;
 import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
 import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
 import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
@@ -143,6 +144,8 @@
     private final boolean mIsOverrideOrientationOnlyForCameraEnabled;
     // Corresponds to OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION
     private final boolean mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled;
+    // Corresponds to OVERRIDE_RESPECT_REQUESTED_ORIENTATION
+    private final boolean mIsOverrideRespectRequestedOrientationEnabled;
 
     // Corresponds to OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION
     private final boolean mIsOverrideCameraCompatDisableForceRotationEnabled;
@@ -272,6 +275,8 @@
                 isCompatChangeEnabled(OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA);
         mIsOverrideUseDisplayLandscapeNaturalOrientationEnabled =
                 isCompatChangeEnabled(OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION);
+        mIsOverrideRespectRequestedOrientationEnabled =
+                isCompatChangeEnabled(OVERRIDE_RESPECT_REQUESTED_ORIENTATION);
 
         mIsOverrideCameraCompatDisableForceRotationEnabled =
                 isCompatChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION);
@@ -418,6 +423,10 @@
         mIsRefreshAfterRotationRequested = isRequested;
     }
 
+    boolean isOverrideRespectRequestedOrientationEnabled() {
+        return mIsOverrideRespectRequestedOrientationEnabled;
+    }
+
     /**
      * Whether should fix display orientation to landscape natural orientation when a task is
      * fullscreen and the display is ignoring orientation requests.
diff --git a/services/core/java/com/android/server/wm/RefreshRatePolicy.java b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
index de42c55..f952adb 100644
--- a/services/core/java/com/android/server/wm/RefreshRatePolicy.java
+++ b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
@@ -59,6 +59,8 @@
         }
     }
 
+    private final DisplayInfo mDisplayInfo;
+    private final Mode mDefaultMode;
     private final Mode mLowRefreshRateMode;
     private final PackageRefreshRate mNonHighRefreshRatePackages = new PackageRefreshRate();
     private final HighRefreshRateDenylist mHighRefreshRateDenylist;
@@ -89,7 +91,9 @@
 
     RefreshRatePolicy(WindowManagerService wmService, DisplayInfo displayInfo,
             HighRefreshRateDenylist denylist) {
-        mLowRefreshRateMode = findLowRefreshRateMode(displayInfo);
+        mDisplayInfo = displayInfo;
+        mDefaultMode = displayInfo.getDefaultMode();
+        mLowRefreshRateMode = findLowRefreshRateMode(displayInfo, mDefaultMode);
         mHighRefreshRateDenylist = denylist;
         mWmService = wmService;
     }
@@ -98,10 +102,9 @@
      * Finds the mode id with the lowest refresh rate which is >= 60hz and same resolution as the
      * default mode.
      */
-    private Mode findLowRefreshRateMode(DisplayInfo displayInfo) {
-        Mode mode = displayInfo.getDefaultMode();
+    private Mode findLowRefreshRateMode(DisplayInfo displayInfo, Mode defaultMode) {
         float[] refreshRates = displayInfo.getDefaultRefreshRates();
-        float bestRefreshRate = mode.getRefreshRate();
+        float bestRefreshRate = defaultMode.getRefreshRate();
         mMinSupportedRefreshRate = bestRefreshRate;
         mMaxSupportedRefreshRate = bestRefreshRate;
         for (int i = refreshRates.length - 1; i >= 0; i--) {
@@ -127,13 +130,39 @@
     }
 
     int getPreferredModeId(WindowState w) {
-        // If app is animating, it's not able to control refresh rate because we want the animation
-        // to run in default refresh rate.
-        if (w.isAnimating(TRANSITION | PARENTS)) {
+        final int preferredDisplayModeId = w.mAttrs.preferredDisplayModeId;
+        if (preferredDisplayModeId <= 0) {
+            // Unspecified, use default mode.
             return 0;
         }
 
-        return w.mAttrs.preferredDisplayModeId;
+        // If app is animating, it's not able to control refresh rate because we want the animation
+        // to run in default refresh rate. But if the display size of default mode is different
+        // from the using preferred mode, then still keep the preferred mode to avoid disturbing
+        // the animation.
+        if (w.isAnimating(TRANSITION | PARENTS)) {
+            Display.Mode preferredMode = null;
+            for (Display.Mode mode : mDisplayInfo.supportedModes) {
+                if (preferredDisplayModeId == mode.getModeId()) {
+                    preferredMode = mode;
+                    break;
+                }
+            }
+            if (preferredMode != null) {
+                final int pW = preferredMode.getPhysicalWidth();
+                final int pH = preferredMode.getPhysicalHeight();
+                if ((pW != mDefaultMode.getPhysicalWidth()
+                        || pH != mDefaultMode.getPhysicalHeight())
+                        && pW == mDisplayInfo.getNaturalWidth()
+                        && pH == mDisplayInfo.getNaturalHeight()) {
+                    // Prefer not to change display size when animating.
+                    return preferredDisplayModeId;
+                }
+            }
+            return 0;
+        }
+
+        return preferredDisplayModeId;
     }
 
     /**
@@ -234,14 +263,10 @@
         if (refreshRateSwitchingType != SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY) {
             final int preferredModeId = w.mAttrs.preferredDisplayModeId;
             if (preferredModeId > 0) {
-                DisplayInfo info = w.getDisplayInfo();
-                if (info != null) {
-                    for (Display.Mode mode : info.supportedModes) {
-                        if (preferredModeId == mode.getModeId()) {
-                            return w.mFrameRateVote.update(mode.getRefreshRate(),
-                                    Surface.FRAME_RATE_COMPATIBILITY_EXACT);
-
-                        }
+                for (Display.Mode mode : mDisplayInfo.supportedModes) {
+                    if (preferredModeId == mode.getModeId()) {
+                        return w.mFrameRateVote.update(mode.getRefreshRate(),
+                                Surface.FRAME_RATE_COMPATIBILITY_EXACT);
                     }
                 }
             }
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index e8aa2c8..2343906 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -136,6 +136,7 @@
 import android.view.DisplayInfo;
 import android.view.SurfaceControl;
 import android.view.WindowManager;
+import android.window.TaskFragmentAnimationParams;
 import android.window.WindowContainerToken;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -234,6 +235,10 @@
     WindowManagerService mWindowManager;
     DisplayManager mDisplayManager;
     private DisplayManagerInternal mDisplayManagerInternal;
+    @NonNull
+    private final DeviceStateController mDeviceStateController;
+    @NonNull
+    private final DisplayRotationCoordinator mDisplayRotationCoordinator;
 
     /** Reference to default display so we can quickly look it up. */
     private DisplayContent mDefaultDisplay;
@@ -440,6 +445,8 @@
         mTaskSupervisor = mService.mTaskSupervisor;
         mTaskSupervisor.mRootWindowContainer = this;
         mDisplayOffTokenAcquirer = mService.new SleepTokenAcquirerImpl(DISPLAY_OFF_SLEEP_TOKEN_TAG);
+        mDeviceStateController = new DeviceStateController(service.mContext, service.mH);
+        mDisplayRotationCoordinator = new DisplayRotationCoordinator();
     }
 
     /**
@@ -1279,7 +1286,8 @@
         final Display[] displays = mDisplayManager.getDisplays();
         for (int displayNdx = 0; displayNdx < displays.length; ++displayNdx) {
             final Display display = displays[displayNdx];
-            final DisplayContent displayContent = new DisplayContent(display, this);
+            final DisplayContent displayContent =
+                    new DisplayContent(display, this, mDeviceStateController);
             addChild(displayContent, POSITION_BOTTOM);
             if (displayContent.mDisplayId == DEFAULT_DISPLAY) {
                 mDefaultDisplay = displayContent;
@@ -1297,6 +1305,10 @@
         return mDefaultDisplay;
     }
 
+    DisplayRotationCoordinator getDisplayRotationCoordinator() {
+        return mDisplayRotationCoordinator;
+    }
+
     /**
      * Get the default display area on the device dedicated to app windows. This one should be used
      * only as a fallback location for activity launches when no target display area is specified,
@@ -1358,7 +1370,7 @@
             return null;
         }
         // The display hasn't been added to ActivityManager yet, create a new record now.
-        displayContent = new DisplayContent(display, this);
+        displayContent = new DisplayContent(display, this, mDeviceStateController);
         addChild(displayContent, POSITION_BOTTOM);
         return displayContent;
     }
@@ -2082,6 +2094,8 @@
                     return;
                 }
                 tf.resetAdjacentTaskFragment();
+                tf.setCompanionTaskFragment(null /* companionTaskFragment */);
+                tf.setAnimationParams(TaskFragmentAnimationParams.DEFAULT);
                 if (tf.getTopNonFinishingActivity() != null) {
                     // When the Task is entering picture-in-picture, we should clear all override
                     // from the client organizer, so the PIP activity can get the correct config
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index eb06b91..294e90b 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2813,7 +2813,7 @@
     void removeImmediately() {
         mIsRemovalRequested = false;
         resetAdjacentTaskFragment();
-        cleanUp();
+        cleanUpEmbeddedTaskFragment();
         final boolean shouldExecuteAppTransition =
                 mClearedTaskFragmentForPip && isTaskVisibleRequested();
         super.removeImmediately();
@@ -2830,10 +2830,20 @@
     }
 
     /** Called on remove to cleanup. */
-    private void cleanUp() {
-        if (mIsEmbedded) {
-            mAtmService.mWindowOrganizerController.cleanUpEmbeddedTaskFragment(this);
+    private void cleanUpEmbeddedTaskFragment() {
+        if (!mIsEmbedded) {
+            return;
         }
+        mAtmService.mWindowOrganizerController.cleanUpEmbeddedTaskFragment(this);
+        final Task task = getTask();
+        if (task == null) {
+            return;
+        }
+        task.forAllLeafTaskFragments(taskFragment -> {
+            if (taskFragment.getCompanionTaskFragment() == this) {
+                taskFragment.setCompanionTaskFragment(null /* companionTaskFragment */);
+            }
+        }, false /* traverseTopToBottom */);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index d2f30ce..f0c099f 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -277,6 +277,17 @@
         return false;
     }
 
+    /** @return whether `wc` is a descendent of a transient-hide window. */
+    boolean isInTransientHide(@NonNull WindowContainer wc) {
+        if (mTransientLaunches == null) return false;
+        for (int i = 0; i < mTransientLaunches.size(); ++i) {
+            if (wc.isDescendantOf(mTransientLaunches.valueAt(i))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     boolean isTransientLaunch(@NonNull ActivityRecord activity) {
         return mTransientLaunches != null && mTransientLaunches.containsKey(activity);
     }
@@ -825,6 +836,11 @@
                         && ar.isVisible()) {
                     // Transient launch was committed, so report enteringAnimation
                     ar.mEnteringAnimation = true;
+                    // Since transient launches don't automatically take focus, make sure we
+                    // synchronize focus since we committed to the launch.
+                    if (ar.isTopRunningActivity()) {
+                        ar.moveFocusableActivityToTop("transitionFinished");
+                    }
                 }
                 continue;
             }
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 5e116ba..18788bf 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -353,11 +353,37 @@
     }
 
     /**
+     * During transient-launch, the "behind" app should retain focus during the transition unless
+     * something takes focus from it explicitly (eg. by calling ATMS.setFocusedTask or by another
+     * transition interrupting this one.
+     *
+     * The rules around this are basically: if there is exactly one active transition and `wc` is
+     * the "behind" of a transient launch, then it can retain focus.
+     */
+    boolean shouldKeepFocus(@NonNull WindowContainer wc) {
+        if (mCollectingTransition != null) {
+            if (!mPlayingTransitions.isEmpty()) return false;
+            return mCollectingTransition.isInTransientHide(wc);
+        } else if (mPlayingTransitions.size() == 1) {
+            return mPlayingTransitions.get(0).isInTransientHide(wc);
+        }
+        return false;
+    }
+
+    /**
+     * @return {@code true} if {@param ar} is part of a transient-launch activity in the
+     * collecting transition.
+     */
+    boolean isTransientCollect(@NonNull ActivityRecord ar) {
+        return mCollectingTransition != null && mCollectingTransition.isTransientLaunch(ar);
+    }
+
+    /**
      * @return {@code true} if {@param ar} is part of a transient-launch activity in an active
      * transition.
      */
     boolean isTransientLaunch(@NonNull ActivityRecord ar) {
-        if (mCollectingTransition != null && mCollectingTransition.isTransientLaunch(ar)) {
+        if (isTransientCollect(ar)) {
             return true;
         }
         for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index abdc708..2e9c9cf 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -431,6 +431,7 @@
                 applyTransaction(wct, -1 /* syncId */, transition, caller);
                 mTransitionController.requestStartTransition(transition, null /* startTask */,
                         null /* remoteTransition */, null /* displayChange */);
+                transition.setAllReady();
                 return;
             }
 
@@ -469,6 +470,7 @@
                             mTransitionController.requestStartTransition(nextTransition,
                                     null /* startTask */, null /* remoteTransition */,
                                     null /* displayChange */);
+                            nextTransition.setAllReady();
                         } else {
                             nextTransition.abort();
                         }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 703cb8a4..7b880fe 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3160,15 +3160,25 @@
                 + (mActivityRecord == null || mActivityRecord.windowsAreFocusable(fromUserTouch))
                 + " canReceiveTouchInput=" + canReceiveTouchInput()
                 + " displayIsOnTop=" + getDisplayContent().isOnTop()
-                + " displayIsTrusted=" + getDisplayContent().isTrusted();
+                + " displayIsTrusted=" + getDisplayContent().isTrusted()
+                + " transitShouldKeepFocus=" + (mActivityRecord != null
+                        && mTransitionController.shouldKeepFocus(mActivityRecord));
     }
 
     public boolean canReceiveKeys(boolean fromUserTouch) {
+        if (mActivityRecord != null && mTransitionController.shouldKeepFocus(mActivityRecord)) {
+            // During transient launch, the transient-hide windows are not visibleRequested
+            // or on-top but are kept focusable and thus can receive keys.
+            return true;
+        }
         final boolean canReceiveKeys = isVisibleRequestedOrAdding()
                 && (mViewVisibility == View.VISIBLE) && !mRemoveOnExit
                 && ((mAttrs.flags & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) == 0)
                 && (mActivityRecord == null || mActivityRecord.windowsAreFocusable(fromUserTouch))
-                && canReceiveTouchInput();
+                // can it receive touches
+                && (mActivityRecord == null || mActivityRecord.getTask() == null
+                        || !mActivityRecord.getTask().getRootTask().shouldIgnoreInput());
+
         if (!canReceiveKeys) {
             return false;
         }
@@ -3194,6 +3204,11 @@
         if (mActivityRecord == null  || mActivityRecord.getTask() == null) {
             return true;
         }
+        // During transient launch, the transient-hide windows are not visibleRequested
+        // or on-top but are kept focusable and thus can receive touch input.
+        if (mTransitionController.shouldKeepFocus(mActivityRecord)) {
+            return true;
+        }
 
         return !mActivityRecord.getTask().getRootTask().shouldIgnoreInput()
                 && mActivityRecord.isVisibleRequested();
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index ab1badb..ef5aa60 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -477,6 +477,10 @@
                     minOccurs="0" maxOccurs="1">
             <xs:annotation name="final"/>
         </xs:element>
+        <xs:element name="refreshRateZoneProfiles" type="refreshRateZoneProfiles"
+                    minOccurs="0" maxOccurs="1">
+            <xs:annotation name="final"/>
+        </xs:element>
         <xs:element name="lowerBlockingZoneConfigs" type="blockingZoneConfig"
                     minOccurs="0" maxOccurs="1">
             <xs:annotation name="final"/>
@@ -487,6 +491,22 @@
         </xs:element>
     </xs:complexType>
 
+    <xs:complexType name="refreshRateZoneProfiles">
+        <xs:sequence>
+            <xs:element name="refreshRateZoneProfile" type="refreshRateZone"
+                        minOccurs="0" maxOccurs="unbounded">
+                <xs:annotation name="final"/>
+            </xs:element>
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="refreshRateZone">
+        <xs:attribute name="id" use="required" type="xs:string" />
+        <xs:element name="refreshRateRange" type="refreshRateRange">
+            <xs:annotation name="final"/>
+        </xs:element>
+    </xs:complexType>
+
     <xs:complexType name="blockingZoneConfig">
         <xs:element name="defaultRefreshRate" type="xs:nonNegativeInteger"
                     minOccurs="1" maxOccurs="1">
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 3e1db4c..ed9f959 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -194,10 +194,12 @@
     method public final java.math.BigInteger getDefaultRefreshRate();
     method public final com.android.server.display.config.BlockingZoneConfig getHigherBlockingZoneConfigs();
     method public final com.android.server.display.config.BlockingZoneConfig getLowerBlockingZoneConfigs();
+    method public final com.android.server.display.config.RefreshRateZoneProfiles getRefreshRateZoneProfiles();
     method public final void setDefaultPeakRefreshRate(java.math.BigInteger);
     method public final void setDefaultRefreshRate(java.math.BigInteger);
     method public final void setHigherBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig);
     method public final void setLowerBlockingZoneConfigs(com.android.server.display.config.BlockingZoneConfig);
+    method public final void setRefreshRateZoneProfiles(com.android.server.display.config.RefreshRateZoneProfiles);
   }
 
   public class RefreshRateRange {
@@ -208,6 +210,19 @@
     method public final void setMinimum(java.math.BigInteger);
   }
 
+  public class RefreshRateZone {
+    ctor public RefreshRateZone();
+    method public String getId();
+    method public final com.android.server.display.config.RefreshRateRange getRefreshRateRange();
+    method public void setId(String);
+    method public final void setRefreshRateRange(com.android.server.display.config.RefreshRateRange);
+  }
+
+  public class RefreshRateZoneProfiles {
+    ctor public RefreshRateZoneProfiles();
+    method public final java.util.List<com.android.server.display.config.RefreshRateZone> getRefreshRateZoneProfile();
+  }
+
   public class SdrHdrRatioMap {
     ctor public SdrHdrRatioMap();
     method @NonNull public final java.util.List<com.android.server.display.config.SdrHdrRatioPoint> getPoint();
diff --git a/services/core/xsd/display-layout-config/display-layout-config.xsd b/services/core/xsd/display-layout-config/display-layout-config.xsd
index 78c9a54..45e10a8 100644
--- a/services/core/xsd/display-layout-config/display-layout-config.xsd
+++ b/services/core/xsd/display-layout-config/display-layout-config.xsd
@@ -55,5 +55,6 @@
         </xs:sequence>
         <xs:attribute name="enabled" type="xs:boolean" use="optional" />
         <xs:attribute name="defaultDisplay" type="xs:boolean" use="optional" />
+        <xs:attribute name="refreshRateZoneId" type="xs:string" use="optional" />
     </xs:complexType>
 </xs:schema>
diff --git a/services/core/xsd/display-layout-config/schema/current.txt b/services/core/xsd/display-layout-config/schema/current.txt
index 6a28d8a..2c16c37 100644
--- a/services/core/xsd/display-layout-config/schema/current.txt
+++ b/services/core/xsd/display-layout-config/schema/current.txt
@@ -6,6 +6,7 @@
     method public java.math.BigInteger getAddress();
     method public String getBrightnessThrottlingMapId();
     method public String getPosition();
+    method public String getRefreshRateZoneId();
     method public boolean isDefaultDisplay();
     method public boolean isEnabled();
     method public void setAddress(java.math.BigInteger);
@@ -13,6 +14,7 @@
     method public void setDefaultDisplay(boolean);
     method public void setEnabled(boolean);
     method public void setPosition(String);
+    method public void setRefreshRateZoneId(String);
   }
 
   public class Layout {
diff --git a/services/java/com/android/server/BootUserInitializer.java b/services/java/com/android/server/BootUserInitializer.java
deleted file mode 100644
index 3d71739..0000000
--- a/services/java/com/android/server/BootUserInitializer.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.server;
-
-import android.annotation.UserIdInt;
-import android.content.ContentResolver;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.provider.Settings;
-
-import com.android.server.am.ActivityManagerService;
-import com.android.server.pm.UserManagerInternal;
-import com.android.server.utils.Slogf;
-import com.android.server.utils.TimingsTraceAndSlog;
-
-/**
- * Class responsible for booting the device in the proper user on headless system user mode.
- *
- */
-// TODO(b/204091126): STOPSHIP - provide proper APIs
-final class BootUserInitializer {
-
-    private static final String TAG = BootUserInitializer.class.getSimpleName();
-
-     // TODO(b/204091126): STOPSHIP - set to false or dynamic value
-    private static final boolean DEBUG = true;
-
-    private final ActivityManagerService mAms;
-    private final ContentResolver mContentResolver;
-
-    BootUserInitializer(ActivityManagerService am, ContentResolver contentResolver) {
-        mAms = am;
-        mContentResolver = contentResolver;
-    }
-
-    public void init(TimingsTraceAndSlog t) {
-        Slogf.i(TAG, "init())");
-
-        // TODO(b/204091126): in the long term, we need to decide who's reponsible for that,
-        // this class or the setup wizard app
-        provisionHeadlessSystemUser();
-
-        unlockSystemUser(t);
-
-        try {
-            t.traceBegin("getBootUser");
-            int bootUser = LocalServices.getService(UserManagerInternal.class).getBootUser();
-            t.traceEnd();
-            t.traceBegin("switchToBootUser-" + bootUser);
-            switchToBootUser(bootUser);
-            t.traceEnd();
-        } catch (UserManager.CheckedUserOperationException e) {
-            Slogf.wtf(TAG, "Failed to created boot user", e);
-        }
-    }
-
-    /* TODO(b/261791491): STOPSHIP - SUW should be responsible for this. */
-    private void provisionHeadlessSystemUser() {
-        if (isDeviceProvisioned()) {
-            Slogf.d(TAG, "provisionHeadlessSystemUser(): already provisioned");
-            return;
-        }
-
-        Slogf.i(TAG, "Marking USER_SETUP_COMPLETE for system user");
-        Settings.Secure.putInt(mContentResolver, Settings.Secure.USER_SETUP_COMPLETE, 1);
-        Slogf.i(TAG, "Marking DEVICE_PROVISIONED for system user");
-        Settings.Global.putInt(mContentResolver, Settings.Global.DEVICE_PROVISIONED, 1);
-    }
-
-    private boolean isDeviceProvisioned() {
-        try {
-            return Settings.Global.getInt(mContentResolver,
-                    Settings.Global.DEVICE_PROVISIONED) == 1;
-        } catch (Exception e) {
-            Slogf.wtf(TAG, "DEVICE_PROVISIONED setting not found.", e);
-            return false;
-        }
-    }
-
-    // NOTE: Mostly copied from Automotive's InitialUserSetter
-    private void unlockSystemUser(TimingsTraceAndSlog t) {
-        Slogf.i(TAG, "Unlocking system user");
-        t.traceBegin("unlock-system-user");
-        try {
-            // This is for force changing state into RUNNING_LOCKED. Otherwise unlock does not
-            // update the state and USER_SYSTEM unlock happens twice.
-            t.traceBegin("am.startUser");
-            boolean started = mAms.startUserInBackgroundWithListener(UserHandle.USER_SYSTEM,
-                            /* listener= */ null);
-            t.traceEnd();
-            if (!started) {
-                Slogf.w(TAG, "could not restart system user in background; trying unlock instead");
-                t.traceBegin("am.unlockUser");
-                boolean unlocked = mAms.unlockUser(UserHandle.USER_SYSTEM, /* token= */ null,
-                        /* secret= */ null, /* listener= */ null);
-                t.traceEnd();
-                if (!unlocked) {
-                    Slogf.w(TAG, "could not unlock system user either");
-                    return;
-                }
-            }
-        } finally {
-            t.traceEnd();
-        }
-    }
-
-    private void switchToBootUser(@UserIdInt int bootUserId) {
-        Slogf.i(TAG, "Switching to boot user %d", bootUserId);
-        boolean started = mAms.startUserInForegroundWithListener(bootUserId,
-                /* unlockListener= */ null);
-        if (!started) {
-            Slogf.wtf(TAG, "Failed to start user %d in foreground", bootUserId);
-        }
-    }
-}
diff --git a/services/java/com/android/server/HsumBootUserInitializer.java b/services/java/com/android/server/HsumBootUserInitializer.java
new file mode 100644
index 0000000..cc6c36e
--- /dev/null
+++ b/services/java/com/android/server/HsumBootUserInitializer.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.ContentResolver;
+import android.content.pm.UserInfo;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+
+import com.android.server.am.ActivityManagerService;
+import com.android.server.pm.UserManagerInternal;
+import com.android.server.utils.Slogf;
+import com.android.server.utils.TimingsTraceAndSlog;
+
+/**
+ * Class responsible for booting the device in the proper user on headless system user mode.
+ *
+ */
+final class HsumBootUserInitializer {
+
+    private static final String TAG = HsumBootUserInitializer.class.getSimpleName();
+
+    private final UserManagerInternal mUmi;
+    private final ActivityManagerService mAms;
+    private final ContentResolver mContentResolver;
+
+    /** Whether this device should always have a non-removable MainUser, including at first boot. */
+    private final boolean mShouldAlwaysHaveMainUser;
+
+    /** Static factory method for creating a {@link HsumBootUserInitializer} instance. */
+    public static @Nullable HsumBootUserInitializer createInstance(ActivityManagerService am,
+            ContentResolver contentResolver, boolean shouldAlwaysHaveMainUser) {
+
+        if (!UserManager.isHeadlessSystemUserMode()) {
+            return null;
+        }
+        return new HsumBootUserInitializer(
+                LocalServices.getService(UserManagerInternal.class),
+                am, contentResolver, shouldAlwaysHaveMainUser);
+    }
+
+    private HsumBootUserInitializer(UserManagerInternal umi, ActivityManagerService am,
+            ContentResolver contentResolver, boolean shouldAlwaysHaveMainUser) {
+        mUmi = umi;
+        mAms = am;
+        mContentResolver = contentResolver;
+        this.mShouldAlwaysHaveMainUser = shouldAlwaysHaveMainUser;
+    }
+
+    /**
+     * Initialize this object, and create MainUser if needed.
+     *
+     * Should be called before PHASE_SYSTEM_SERVICES_READY as services' setups may require MainUser,
+     * but probably after PHASE_LOCK_SETTINGS_READY since that may be needed for user creation.
+     */
+    public void init(TimingsTraceAndSlog t) {
+        Slogf.i(TAG, "init())");
+
+        // TODO(b/204091126): in the long term, we need to decide who's reponsible for that,
+        // this class or the setup wizard app
+        provisionHeadlessSystemUser();
+
+        if (mShouldAlwaysHaveMainUser) {
+            t.traceBegin("createMainUserIfNeeded");
+            createMainUserIfNeeded();
+            t.traceEnd();
+        }
+    }
+
+    private void createMainUserIfNeeded() {
+        int mainUser = mUmi.getMainUserId();
+        if (mainUser != UserHandle.USER_NULL) {
+            Slogf.d(TAG, "Found existing MainUser, userId=%d", mainUser);
+            return;
+        }
+
+        Slogf.d(TAG, "Creating a new MainUser");
+        try {
+            final UserInfo newInitialUser = mUmi.createUserEvenWhenDisallowed(
+                    /* name= */ null, // null will appear as "Owner" in on-demand localisation
+                    UserManager.USER_TYPE_FULL_SECONDARY,
+                    UserInfo.FLAG_ADMIN | UserInfo.FLAG_MAIN,
+                    /* disallowedPackages= */ null,
+                    /* token= */ null);
+            if (newInitialUser == null) {
+                Slogf.wtf(TAG, "Initial bootable MainUser creation failed: returned null");
+            } else {
+                Slogf.i(TAG, "Successfully created MainUser, userId=%d", newInitialUser.id);
+            }
+        } catch (UserManager.CheckedUserOperationException e) {
+            Slogf.wtf(TAG, "Initial bootable MainUser creation failed", e);
+        }
+    }
+
+    /**
+     * Put the device into the correct user state: unlock the system and switch to the boot user.
+     *
+     * Should only call once PHASE_THIRD_PARTY_APPS_CAN_START is reached to ensure that privileged
+     * apps have had the chance to set the boot user, if applicable.
+     */
+    public void systemRunning(TimingsTraceAndSlog t) {
+        unlockSystemUser(t);
+
+        try {
+            t.traceBegin("getBootUser");
+            final int bootUser = mUmi.getBootUser();
+            t.traceEnd();
+            t.traceBegin("switchToBootUser-" + bootUser);
+            switchToBootUser(bootUser);
+            t.traceEnd();
+        } catch (UserManager.CheckedUserOperationException e) {
+            Slogf.wtf(TAG, "Failed to switch to boot user since there isn't one.");
+        }
+    }
+
+    /* TODO(b/261791491): STOPSHIP - SUW should be responsible for this. */
+    private void provisionHeadlessSystemUser() {
+        if (isDeviceProvisioned()) {
+            Slogf.d(TAG, "provisionHeadlessSystemUser(): already provisioned");
+            return;
+        }
+
+        Slogf.i(TAG, "Marking USER_SETUP_COMPLETE for system user");
+        Settings.Secure.putInt(mContentResolver, Settings.Secure.USER_SETUP_COMPLETE, 1);
+        Slogf.i(TAG, "Marking DEVICE_PROVISIONED for system user");
+        Settings.Global.putInt(mContentResolver, Settings.Global.DEVICE_PROVISIONED, 1);
+    }
+
+    private boolean isDeviceProvisioned() {
+        try {
+            return Settings.Global.getInt(mContentResolver,
+                    Settings.Global.DEVICE_PROVISIONED) == 1;
+        } catch (Exception e) {
+            Slogf.wtf(TAG, "DEVICE_PROVISIONED setting not found.", e);
+            return false;
+        }
+    }
+
+    // NOTE: Mostly copied from Automotive's InitialUserSetter
+    private void unlockSystemUser(TimingsTraceAndSlog t) {
+        Slogf.i(TAG, "Unlocking system user");
+        t.traceBegin("unlock-system-user");
+        try {
+            // This is for force changing state into RUNNING_LOCKED. Otherwise unlock does not
+            // update the state and USER_SYSTEM unlock happens twice.
+            t.traceBegin("am.startUser");
+            boolean started = mAms.startUserInBackgroundWithListener(UserHandle.USER_SYSTEM,
+                            /* listener= */ null);
+            t.traceEnd();
+            if (!started) {
+                Slogf.w(TAG, "could not restart system user in background; trying unlock instead");
+                t.traceBegin("am.unlockUser");
+                boolean unlocked = mAms.unlockUser(UserHandle.USER_SYSTEM, /* token= */ null,
+                        /* secret= */ null, /* listener= */ null);
+                t.traceEnd();
+                if (!unlocked) {
+                    Slogf.w(TAG, "could not unlock system user either");
+                    return;
+                }
+            }
+        } finally {
+            t.traceEnd();
+        }
+    }
+
+    private void switchToBootUser(@UserIdInt int bootUserId) {
+        Slogf.i(TAG, "Switching to boot user %d", bootUserId);
+        boolean started = mAms.startUserInForegroundWithListener(bootUserId,
+                /* unlockListener= */ null);
+        if (!started) {
+            Slogf.wtf(TAG, "Failed to start user %d in foreground", bootUserId);
+        }
+    }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index a15c6d2..d22be9e 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -75,7 +75,6 @@
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
-import android.os.UserManager;
 import android.os.storage.IStorageManager;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
@@ -2694,6 +2693,18 @@
         mSystemServiceManager.startBootPhase(t, SystemService.PHASE_LOCK_SETTINGS_READY);
         t.traceEnd();
 
+        // Create initial user if needed, which should be done early since some system services rely
+        // on it in their setup, but likely needs to be done after LockSettingsService is ready.
+        final HsumBootUserInitializer hsumBootUserInitializer =
+                HsumBootUserInitializer.createInstance(
+                        mActivityManagerService, mContentResolver,
+                        context.getResources().getBoolean(R.bool.config_isMainUserPermanentAdmin));
+        if (hsumBootUserInitializer != null) {
+            t.traceBegin("HsumBootUserInitializer.init");
+            hsumBootUserInitializer.init(t);
+            t.traceEnd();
+        }
+
         t.traceBegin("StartBootPhaseSystemServicesReady");
         mSystemServiceManager.startBootPhase(t, SystemService.PHASE_SYSTEM_SERVICES_READY);
         t.traceEnd();
@@ -2961,10 +2972,10 @@
             mSystemServiceManager.startBootPhase(t, SystemService.PHASE_THIRD_PARTY_APPS_CAN_START);
             t.traceEnd();
 
-            if (UserManager.isHeadlessSystemUserMode() && !isAutomotive) {
-                // TODO(b/204091126): remove isAutomotive check once the workflow is finalized
-                t.traceBegin("BootUserInitializer");
-                new BootUserInitializer(mActivityManagerService, mContentResolver).init(t);
+            if (hsumBootUserInitializer != null && !isAutomotive) {
+                // TODO(b/261924826): remove isAutomotive check once the workflow is finalized
+                t.traceBegin("HsumBootUserInitializer.systemRunning");
+                hsumBootUserInitializer.systemRunning(t);
                 t.traceEnd();
             }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
index 57e873d..5ca01ee 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java
@@ -19,13 +19,14 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
@@ -41,6 +42,7 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.PowerManager;
+import android.os.SystemProperties;
 import android.os.test.TestLooper;
 import android.util.FloatProperty;
 import android.view.Display;
@@ -55,6 +57,7 @@
 import com.android.server.am.BatteryStatsService;
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.color.ColorDisplayService;
+import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.testutils.OffsettableClock;
 
@@ -77,13 +80,18 @@
 public final class DisplayPowerController2Test {
     private static final String UNIQUE_DISPLAY_ID = "unique_id_test123";
     private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
+    private static final int FOLLOWER_DISPLAY_ID = Display.DEFAULT_DISPLAY + 1;
 
     private MockitoSession mSession;
     private OffsettableClock mClock;
     private TestLooper mTestLooper;
     private Handler mHandler;
     private DisplayPowerController2.Injector mInjector;
+    private DisplayPowerController2.Injector mFollowerInjector;
     private Context mContextSpy;
+    private DisplayPowerController2 mDpc;
+    private DisplayPowerController2 mFollowerDpc;
+    private Sensor mProxSensor;
 
     @Mock
     private DisplayPowerCallbacks mDisplayPowerCallbacksMock;
@@ -94,14 +102,22 @@
     @Mock
     private HighBrightnessModeMetadata mHighBrightnessModeMetadataMock;
     @Mock
+    private HighBrightnessModeMetadata mFollowerHighBrightnessModeMetadataMock;
+    @Mock
     private LogicalDisplay mLogicalDisplayMock;
     @Mock
+    private LogicalDisplay mFollowerLogicalDisplayMock;
+    @Mock
     private DisplayDevice mDisplayDeviceMock;
     @Mock
+    private DisplayDevice mFollowerDisplayDeviceMock;
+    @Mock
     private BrightnessTracker mBrightnessTrackerMock;
     @Mock
     private BrightnessSetting mBrightnessSettingMock;
     @Mock
+    private BrightnessSetting mFollowerBrightnessSettingMock;
+    @Mock
     private WindowManagerPolicy mWindowManagerPolicyMock;
     @Mock
     private PowerManager mPowerManagerMock;
@@ -110,10 +126,22 @@
     @Mock
     private DisplayDeviceConfig mDisplayDeviceConfigMock;
     @Mock
+    private DisplayDeviceConfig mFollowerDisplayDeviceConfigMock;
+    @Mock
     private DisplayPowerState mDisplayPowerStateMock;
     @Mock
     private DualRampAnimator<DisplayPowerState> mDualRampAnimatorMock;
     @Mock
+    private DualRampAnimator<DisplayPowerState> mFollowerDualRampAnimatorMock;
+    @Mock
+    private AutomaticBrightnessController mAutomaticBrightnessControllerMock;
+    @Mock
+    private AutomaticBrightnessController mFollowerAutomaticBrightnessControllerMock;
+    @Mock
+    private BrightnessMappingStrategy mBrightnessMapperMock;
+    @Mock
+    private HysteresisLevels mHysteresisLevelsMock;
+    @Mock
     private WakelockController mWakelockController;
     @Mock
     private ColorDisplayService.ColorDisplayServiceInternal mCdsiMock;
@@ -126,6 +154,7 @@
         mSession = ExtendedMockito.mockitoSession()
                 .initMocks(this)
                 .strictness(Strictness.LENIENT)
+                .spyStatic(SystemProperties.class)
                 .spyStatic(LocalServices.class)
                 .spyStatic(BatteryStatsService.class)
                 .startMocking();
@@ -167,6 +196,113 @@
                         displayDeviceConfig, looper, nudgeUpdatePowerState, displayId,
                         sensorManager, /* injector= */ null);
             }
+
+            @Override
+            AutomaticBrightnessController getAutomaticBrightnessController(
+                    AutomaticBrightnessController.Callbacks callbacks, Looper looper,
+                    SensorManager sensorManager, Sensor lightSensor,
+                    BrightnessMappingStrategy interactiveModeBrightnessMapper,
+                    int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+                    float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+                    long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+                    boolean resetAmbientLuxAfterWarmUpConfig,
+                    HysteresisLevels ambientBrightnessThresholds,
+                    HysteresisLevels screenBrightnessThresholds,
+                    HysteresisLevels ambientBrightnessThresholdsIdle,
+                    HysteresisLevels screenBrightnessThresholdsIdle, Context context,
+                    HighBrightnessModeController hbmController,
+                    BrightnessThrottler brightnessThrottler,
+                    BrightnessMappingStrategy idleModeBrightnessMapper,
+                    int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux,
+                    float userBrightness) {
+                return mAutomaticBrightnessControllerMock;
+            }
+
+            @Override
+            BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+                    DisplayDeviceConfig displayDeviceConfig,
+                    DisplayWhiteBalanceController displayWhiteBalanceController) {
+                return mBrightnessMapperMock;
+            }
+
+            @Override
+            HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                    float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                    float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                    float minBrighteningThreshold) {
+                return mHysteresisLevelsMock;
+            }
+
+            @Override
+            HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                    float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                    float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                    float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+                return mHysteresisLevelsMock;
+            }
+        };
+        mFollowerInjector = new DisplayPowerController2.Injector() {
+            @Override
+            DisplayPowerController2.Clock getClock() {
+                return mClock::now;
+            }
+
+            @Override
+            DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
+                    int displayId, int displayState) {
+                return mDisplayPowerStateMock;
+            }
+
+            @Override
+            DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
+                    FloatProperty<DisplayPowerState> firstProperty,
+                    FloatProperty<DisplayPowerState> secondProperty) {
+                return mFollowerDualRampAnimatorMock;
+            }
+
+            @Override
+            AutomaticBrightnessController getAutomaticBrightnessController(
+                    AutomaticBrightnessController.Callbacks callbacks, Looper looper,
+                    SensorManager sensorManager, Sensor lightSensor,
+                    BrightnessMappingStrategy interactiveModeBrightnessMapper,
+                    int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+                    float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+                    long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+                    boolean resetAmbientLuxAfterWarmUpConfig,
+                    HysteresisLevels ambientBrightnessThresholds,
+                    HysteresisLevels screenBrightnessThresholds,
+                    HysteresisLevels ambientBrightnessThresholdsIdle,
+                    HysteresisLevels screenBrightnessThresholdsIdle, Context context,
+                    HighBrightnessModeController hbmController,
+                    BrightnessThrottler brightnessThrottler,
+                    BrightnessMappingStrategy idleModeBrightnessMapper,
+                    int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux,
+                    float userBrightness) {
+                return mFollowerAutomaticBrightnessControllerMock;
+            }
+
+            @Override
+            BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+                    DisplayDeviceConfig displayDeviceConfig,
+                    DisplayWhiteBalanceController displayWhiteBalanceController) {
+                return mBrightnessMapperMock;
+            }
+
+            @Override
+            HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                    float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                    float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                    float minBrighteningThreshold) {
+                return mHysteresisLevelsMock;
+            }
+
+            @Override
+            HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                    float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                    float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                    float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+                return mHysteresisLevelsMock;
+            }
         };
 
         addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
@@ -174,11 +310,30 @@
         when(mContextSpy.getSystemService(eq(PowerManager.class))).thenReturn(mPowerManagerMock);
         when(mContextSpy.getResources()).thenReturn(mResourcesMock);
 
+        doAnswer((Answer<Void>) invocationOnMock -> null).when(() ->
+                SystemProperties.set(anyString(), any()));
         doAnswer((Answer<ColorDisplayService.ColorDisplayServiceInternal>) invocationOnMock ->
                 mCdsiMock).when(() -> LocalServices.getService(
                 ColorDisplayService.ColorDisplayServiceInternal.class));
-        doAnswer((Answer<Void>) invocationOnMock -> null).when(() ->
-                BatteryStatsService.getService());
+        doAnswer((Answer<Void>) invocationOnMock -> null).when(BatteryStatsService::getService);
+
+        setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID, mLogicalDisplayMock, mDisplayDeviceMock,
+                mDisplayDeviceConfigMock);
+        setUpDisplay(FOLLOWER_DISPLAY_ID, UNIQUE_DISPLAY_ID, mFollowerLogicalDisplayMock,
+                mFollowerDisplayDeviceMock, mFollowerDisplayDeviceConfigMock);
+
+        mProxSensor = setUpProxSensor();
+
+        mDpc = new DisplayPowerController2(
+                mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
+                mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
+                mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
+        }, mHighBrightnessModeMetadataMock);
+        mFollowerDpc = new DisplayPowerController2(
+                mContextSpy, mFollowerInjector, mDisplayPowerCallbacksMock, mHandler,
+                mSensorManagerMock, mDisplayBlankerMock, mFollowerLogicalDisplayMock,
+                mBrightnessTrackerMock, mFollowerBrightnessSettingMock, () -> {
+        }, mFollowerHighBrightnessModeMetadataMock);
     }
 
     @After
@@ -189,30 +344,20 @@
 
     @Test
     public void testReleaseProxSuspendBlockersOnExit() throws Exception {
-        setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID);
-
-        Sensor proxSensor = setUpProxSensor();
-
-        DisplayPowerController2 dpc = new DisplayPowerController2(
-                mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
-                mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
-                mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
-        }, mHighBrightnessModeMetadataMock);
-
         when(mDisplayPowerStateMock.getScreenState()).thenReturn(Display.STATE_ON);
         // send a display power request
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
         dpr.useProximitySensor = true;
-        dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
+        mDpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
 
         // Run updatePowerState to start listener for the prox sensor
         advanceTime(1);
 
-        SensorEventListener listener = getSensorEventListener(proxSensor);
+        SensorEventListener listener = getSensorEventListener(mProxSensor);
         assertNotNull(listener);
 
-        listener.onSensorChanged(TestUtils.createSensorEvent(proxSensor, 5 /* lux */));
+        listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor, 5 /* lux */));
         advanceTime(1);
 
         // two times, one for unfinished business and one for proximity
@@ -221,8 +366,7 @@
         verify(mWakelockController).acquireWakelock(
                 WakelockController.WAKE_LOCK_PROXIMITY_DEBOUNCE);
 
-
-        dpc.stop();
+        mDpc.stop();
         advanceTime(1);
         // two times, one for unfinished business and one for proximity
         verify(mWakelockController).acquireWakelock(
@@ -232,29 +376,19 @@
     }
 
     @Test
-    public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() throws Exception {
-        setUpDisplay(1, UNIQUE_DISPLAY_ID);
-
-        Sensor proxSensor = setUpProxSensor();
-
-        DisplayPowerController2 dpc = new DisplayPowerController2(
-                mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
-                mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
-                mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
-        }, mHighBrightnessModeMetadataMock);
-
+    public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() {
         when(mDisplayPowerStateMock.getScreenState()).thenReturn(Display.STATE_ON);
         // send a display power request
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
         dpr.useProximitySensor = true;
-        dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
+        mFollowerDpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
 
         // Run updatePowerState
         advanceTime(1);
 
         verify(mSensorManagerMock, never()).registerListener(any(SensorEventListener.class),
-                eq(proxSensor), anyInt(), any(Handler.class));
+                eq(mProxSensor), anyInt(), any(Handler.class));
     }
 
     /**
@@ -284,56 +418,158 @@
         return mSensorEventListenerCaptor.getValue();
     }
 
-    private void setUpDisplay(int displayId, String uniqueId) {
+    private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock,
+            DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock) {
         DisplayInfo info = new DisplayInfo();
         DisplayDeviceInfo deviceInfo = new DisplayDeviceInfo();
 
-        when(mLogicalDisplayMock.getDisplayIdLocked()).thenReturn(displayId);
-        when(mLogicalDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(mDisplayDeviceMock);
-        when(mLogicalDisplayMock.getDisplayInfoLocked()).thenReturn(info);
-        when(mLogicalDisplayMock.isEnabledLocked()).thenReturn(true);
-        when(mLogicalDisplayMock.isInTransitionLocked()).thenReturn(false);
-        when(mDisplayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo);
-        when(mDisplayDeviceMock.getUniqueId()).thenReturn(uniqueId);
-        when(mDisplayDeviceMock.getDisplayDeviceConfig()).thenReturn(mDisplayDeviceConfigMock);
-        when(mDisplayDeviceConfigMock.getProximitySensor()).thenReturn(
+        when(logicalDisplayMock.getDisplayIdLocked()).thenReturn(displayId);
+        when(logicalDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(displayDeviceMock);
+        when(logicalDisplayMock.getDisplayInfoLocked()).thenReturn(info);
+        when(logicalDisplayMock.isEnabledLocked()).thenReturn(true);
+        when(logicalDisplayMock.isInTransitionLocked()).thenReturn(false);
+        when(displayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo);
+        when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId);
+        when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock);
+        when(displayDeviceConfigMock.getProximitySensor()).thenReturn(
                 new DisplayDeviceConfig.SensorData() {
                     {
                         type = Sensor.STRING_TYPE_PROXIMITY;
                         name = null;
                     }
                 });
-        when(mDisplayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
+        when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
+        when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true);
+        when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn(
+                new DisplayDeviceConfig.SensorData());
+        when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn(
+                new DisplayDeviceConfig.SensorData());
     }
 
     @Test
-    public void testDisplayBrightnessFollowers() {
-        setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID);
+    public void testDisplayBrightnessFollowers_BothDpcsSupportNits() {
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
 
-        DisplayPowerController2 defaultDpc = new DisplayPowerController2(
-                mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
-                mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
-                mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
-        }, mHighBrightnessModeMetadataMock);
-        DisplayPowerController2 followerDpc = new DisplayPowerController2(
-                mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
-                mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
-                mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
-        }, mHighBrightnessModeMetadataMock);
+        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
 
-        defaultDpc.addDisplayBrightnessFollower(followerDpc);
+        mDpc.addDisplayBrightnessFollower(mFollowerDpc);
 
-        defaultDpc.setBrightness(0.3f);
-        assertEquals(defaultDpc.getBrightnessInfo().brightness,
-                followerDpc.getBrightnessInfo().brightness, 0);
+        // Test different float scale values
+        float leadBrightness = 0.3f;
+        float followerBrightness = 0.4f;
+        float nits = 300;
+        when(mAutomaticBrightnessControllerMock.convertToNits(leadBrightness)).thenReturn(nits);
+        when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(nits))
+                .thenReturn(followerBrightness);
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(leadBrightness);
+        listener.onBrightnessChanged(leadBrightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(leadBrightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock).animateTo(eq(followerBrightness), anyFloat(),
+                anyFloat());
 
-        defaultDpc.setBrightness(0.6f);
-        assertEquals(defaultDpc.getBrightnessInfo().brightness,
-                followerDpc.getBrightnessInfo().brightness, 0);
+        clearInvocations(mDualRampAnimatorMock, mFollowerDualRampAnimatorMock);
 
-        float brightness = 0.1f;
-        defaultDpc.clearDisplayBrightnessFollowers();
-        defaultDpc.setBrightness(brightness);
-        assertNotEquals(brightness, followerDpc.getBrightnessInfo().brightness, 0);
+        // Test the same float scale value
+        float brightness = 0.6f;
+        nits = 600;
+        when(mAutomaticBrightnessControllerMock.convertToNits(brightness)).thenReturn(nits);
+        when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(nits))
+                .thenReturn(brightness);
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+        listener.onBrightnessChanged(brightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+
+        clearInvocations(mDualRampAnimatorMock, mFollowerDualRampAnimatorMock);
+
+        // Test clear followers
+        mDpc.clearDisplayBrightnessFollowers();
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(leadBrightness);
+        listener.onBrightnessChanged(leadBrightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(leadBrightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock, never()).animateTo(eq(followerBrightness), anyFloat(),
+                anyFloat());
+    }
+
+    @Test
+    public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+        mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+        float brightness = 0.3f;
+        when(mAutomaticBrightnessControllerMock.convertToNits(brightness)).thenReturn(300f);
+        when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(anyFloat()))
+                .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+        listener.onBrightnessChanged(brightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+    }
+
+    @Test
+    public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+        mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+        float brightness = 0.3f;
+        when(mAutomaticBrightnessControllerMock.convertToNits(anyFloat())).thenReturn(-1f);
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+        listener.onBrightnessChanged(brightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+    }
+
+    @Test
+    public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+        mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+        float brightness = 0.3f;
+        when(mAutomaticBrightnessControllerMock.convertToNits(anyFloat())).thenReturn(-1f);
+        when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(anyFloat()))
+                .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+        listener.onBrightnessChanged(brightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
index 6bf5b62..996a9ab 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -19,14 +19,14 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
@@ -40,7 +40,9 @@
 import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.PowerManager;
+import android.os.SystemProperties;
 import android.os.test.TestLooper;
 import android.util.FloatProperty;
 import android.view.Display;
@@ -55,6 +57,7 @@
 import com.android.server.am.BatteryStatsService;
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.color.ColorDisplayService;
+import com.android.server.display.whitebalance.DisplayWhiteBalanceController;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.testutils.OffsettableClock;
 
@@ -77,13 +80,18 @@
 public final class DisplayPowerControllerTest {
     private static final String UNIQUE_DISPLAY_ID = "unique_id_test123";
     private static final int DISPLAY_ID = Display.DEFAULT_DISPLAY;
+    private static final int FOLLOWER_DISPLAY_ID = Display.DEFAULT_DISPLAY + 1;
 
     private MockitoSession mSession;
     private OffsettableClock mClock;
     private TestLooper mTestLooper;
     private Handler mHandler;
     private DisplayPowerController.Injector mInjector;
+    private DisplayPowerController.Injector mFollowerInjector;
     private Context mContextSpy;
+    private DisplayPowerController mDpc;
+    private DisplayPowerController mFollowerDpc;
+    private Sensor mProxSensor;
 
     @Mock
     private DisplayPowerCallbacks mDisplayPowerCallbacksMock;
@@ -94,14 +102,22 @@
     @Mock
     private LogicalDisplay mLogicalDisplayMock;
     @Mock
+    private LogicalDisplay mFollowerLogicalDisplayMock;
+    @Mock
     private DisplayDevice mDisplayDeviceMock;
     @Mock
+    private DisplayDevice mFollowerDisplayDeviceMock;
+    @Mock
     private HighBrightnessModeMetadata mHighBrightnessModeMetadataMock;
     @Mock
+    private HighBrightnessModeMetadata mFollowerHighBrightnessModeMetadataMock;
+    @Mock
     private BrightnessTracker mBrightnessTrackerMock;
     @Mock
     private BrightnessSetting mBrightnessSettingMock;
     @Mock
+    private BrightnessSetting mFollowerBrightnessSettingMock;
+    @Mock
     private WindowManagerPolicy mWindowManagerPolicyMock;
     @Mock
     private PowerManager mPowerManagerMock;
@@ -110,10 +126,22 @@
     @Mock
     private DisplayDeviceConfig mDisplayDeviceConfigMock;
     @Mock
+    private DisplayDeviceConfig mFollowerDisplayDeviceConfigMock;
+    @Mock
     private DisplayPowerState mDisplayPowerStateMock;
     @Mock
     private DualRampAnimator<DisplayPowerState> mDualRampAnimatorMock;
     @Mock
+    private DualRampAnimator<DisplayPowerState> mFollowerDualRampAnimatorMock;
+    @Mock
+    private AutomaticBrightnessController mAutomaticBrightnessControllerMock;
+    @Mock
+    private AutomaticBrightnessController mFollowerAutomaticBrightnessControllerMock;
+    @Mock
+    private BrightnessMappingStrategy mBrightnessMapperMock;
+    @Mock
+    private HysteresisLevels mHysteresisLevelsMock;
+    @Mock
     private ColorDisplayService.ColorDisplayServiceInternal mCdsiMock;
 
     @Captor
@@ -124,6 +152,7 @@
         mSession = ExtendedMockito.mockitoSession()
                 .initMocks(this)
                 .strictness(Strictness.LENIENT)
+                .spyStatic(SystemProperties.class)
                 .spyStatic(LocalServices.class)
                 .spyStatic(BatteryStatsService.class)
                 .startMocking();
@@ -149,6 +178,113 @@
                     FloatProperty<DisplayPowerState> secondProperty) {
                 return mDualRampAnimatorMock;
             }
+
+            @Override
+            AutomaticBrightnessController getAutomaticBrightnessController(
+                    AutomaticBrightnessController.Callbacks callbacks, Looper looper,
+                    SensorManager sensorManager, Sensor lightSensor,
+                    BrightnessMappingStrategy interactiveModeBrightnessMapper,
+                    int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+                    float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+                    long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+                    boolean resetAmbientLuxAfterWarmUpConfig,
+                    HysteresisLevels ambientBrightnessThresholds,
+                    HysteresisLevels screenBrightnessThresholds,
+                    HysteresisLevels ambientBrightnessThresholdsIdle,
+                    HysteresisLevels screenBrightnessThresholdsIdle, Context context,
+                    HighBrightnessModeController hbmController,
+                    BrightnessThrottler brightnessThrottler,
+                    BrightnessMappingStrategy idleModeBrightnessMapper,
+                    int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux,
+                    float userBrightness) {
+                return mAutomaticBrightnessControllerMock;
+            }
+
+            @Override
+            BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+                    DisplayDeviceConfig displayDeviceConfig,
+                    DisplayWhiteBalanceController displayWhiteBalanceController) {
+                return mBrightnessMapperMock;
+            }
+
+            @Override
+            HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                    float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                    float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                    float minBrighteningThreshold) {
+                return mHysteresisLevelsMock;
+            }
+
+            @Override
+            HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                    float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                    float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                    float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+                return mHysteresisLevelsMock;
+            }
+        };
+        mFollowerInjector = new DisplayPowerController.Injector() {
+            @Override
+            DisplayPowerController.Clock getClock() {
+                return mClock::now;
+            }
+
+            @Override
+            DisplayPowerState getDisplayPowerState(DisplayBlanker blanker, ColorFade colorFade,
+                    int displayId, int displayState) {
+                return mDisplayPowerStateMock;
+            }
+
+            @Override
+            DualRampAnimator<DisplayPowerState> getDualRampAnimator(DisplayPowerState dps,
+                    FloatProperty<DisplayPowerState> firstProperty,
+                    FloatProperty<DisplayPowerState> secondProperty) {
+                return mFollowerDualRampAnimatorMock;
+            }
+
+            @Override
+            AutomaticBrightnessController getAutomaticBrightnessController(
+                    AutomaticBrightnessController.Callbacks callbacks, Looper looper,
+                    SensorManager sensorManager, Sensor lightSensor,
+                    BrightnessMappingStrategy interactiveModeBrightnessMapper,
+                    int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+                    float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+                    long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+                    boolean resetAmbientLuxAfterWarmUpConfig,
+                    HysteresisLevels ambientBrightnessThresholds,
+                    HysteresisLevels screenBrightnessThresholds,
+                    HysteresisLevels ambientBrightnessThresholdsIdle,
+                    HysteresisLevels screenBrightnessThresholdsIdle, Context context,
+                    HighBrightnessModeController hbmController,
+                    BrightnessThrottler brightnessThrottler,
+                    BrightnessMappingStrategy idleModeBrightnessMapper,
+                    int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux,
+                    float userBrightness) {
+                return mFollowerAutomaticBrightnessControllerMock;
+            }
+
+            @Override
+            BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources,
+                    DisplayDeviceConfig displayDeviceConfig,
+                    DisplayWhiteBalanceController displayWhiteBalanceController) {
+                return mBrightnessMapperMock;
+            }
+
+            @Override
+            HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                    float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                    float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                    float minBrighteningThreshold) {
+                return mHysteresisLevelsMock;
+            }
+
+            @Override
+            HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                    float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                    float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                    float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+                return mHysteresisLevelsMock;
+            }
         };
 
         addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock);
@@ -156,11 +292,30 @@
         when(mContextSpy.getSystemService(eq(PowerManager.class))).thenReturn(mPowerManagerMock);
         when(mContextSpy.getResources()).thenReturn(mResourcesMock);
 
+        doAnswer((Answer<Void>) invocationOnMock -> null).when(() ->
+                SystemProperties.set(anyString(), any()));
         doAnswer((Answer<ColorDisplayService.ColorDisplayServiceInternal>) invocationOnMock ->
                 mCdsiMock).when(() -> LocalServices.getService(
-                        ColorDisplayService.ColorDisplayServiceInternal.class));
-        doAnswer((Answer<Void>) invocationOnMock -> null).when(() ->
-                BatteryStatsService.getService());
+                ColorDisplayService.ColorDisplayServiceInternal.class));
+        doAnswer((Answer<Void>) invocationOnMock -> null).when(BatteryStatsService::getService);
+
+        setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID, mLogicalDisplayMock, mDisplayDeviceMock,
+                mDisplayDeviceConfigMock);
+        setUpDisplay(FOLLOWER_DISPLAY_ID, UNIQUE_DISPLAY_ID, mFollowerLogicalDisplayMock,
+                mFollowerDisplayDeviceMock, mFollowerDisplayDeviceConfigMock);
+
+        mProxSensor = setUpProxSensor();
+
+        mDpc = new DisplayPowerController(
+                mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
+                mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
+                mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
+        }, mHighBrightnessModeMetadataMock);
+        mFollowerDpc = new DisplayPowerController(
+                mContextSpy, mFollowerInjector, mDisplayPowerCallbacksMock, mHandler,
+                mSensorManagerMock, mDisplayBlankerMock, mFollowerLogicalDisplayMock,
+                mBrightnessTrackerMock, mFollowerBrightnessSettingMock, () -> {
+        }, mFollowerHighBrightnessModeMetadataMock);
     }
 
     @After
@@ -171,72 +326,52 @@
 
     @Test
     public void testReleaseProxSuspendBlockersOnExit() throws Exception {
-        setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID);
-
-        Sensor proxSensor = setUpProxSensor();
-
-        DisplayPowerController dpc = new DisplayPowerController(
-                mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
-                mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
-                mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
-        }, mHighBrightnessModeMetadataMock);
-
         when(mDisplayPowerStateMock.getScreenState()).thenReturn(Display.STATE_ON);
         // send a display power request
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
         dpr.useProximitySensor = true;
-        dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
+        mDpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
 
         // Run updatePowerState to start listener for the prox sensor
         advanceTime(1);
 
-        SensorEventListener listener = getSensorEventListener(proxSensor);
+        SensorEventListener listener = getSensorEventListener(mProxSensor);
         assertNotNull(listener);
 
-        listener.onSensorChanged(TestUtils.createSensorEvent(proxSensor, 5 /* lux */));
+        listener.onSensorChanged(TestUtils.createSensorEvent(mProxSensor, 5 /* lux */));
         advanceTime(1);
 
         // two times, one for unfinished business and one for proximity
         verify(mDisplayPowerCallbacksMock).acquireSuspendBlocker(
-                dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
+                mDpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
         verify(mDisplayPowerCallbacksMock).acquireSuspendBlocker(
-                dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
+                mDpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
 
-        dpc.stop();
+        mDpc.stop();
         advanceTime(1);
 
         // two times, one for unfinished business and one for proximity
         verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker(
-                dpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
+                mDpc.getSuspendBlockerUnfinishedBusinessId(DISPLAY_ID));
         verify(mDisplayPowerCallbacksMock).releaseSuspendBlocker(
-                dpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
+                mDpc.getSuspendBlockerProxDebounceId(DISPLAY_ID));
     }
 
     @Test
-    public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() throws Exception {
-        setUpDisplay(1, UNIQUE_DISPLAY_ID);
-
-        Sensor proxSensor = setUpProxSensor();
-
-        DisplayPowerController dpc = new DisplayPowerController(
-                mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
-                mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
-                mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
-        }, mHighBrightnessModeMetadataMock);
-
+    public void testProximitySensorListenerNotRegisteredForNonDefaultDisplay() {
         when(mDisplayPowerStateMock.getScreenState()).thenReturn(Display.STATE_ON);
         // send a display power request
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
         dpr.useProximitySensor = true;
-        dpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
+        mFollowerDpc.requestPowerState(dpr, false /* waitForNegativeProximity */);
 
         // Run updatePowerState
         advanceTime(1);
 
         verify(mSensorManagerMock, never()).registerListener(any(SensorEventListener.class),
-                eq(proxSensor), anyInt(), any(Handler.class));
+                eq(mProxSensor), anyInt(), any(Handler.class));
     }
 
     /**
@@ -266,56 +401,158 @@
         return mSensorEventListenerCaptor.getValue();
     }
 
-    private void setUpDisplay(int displayId, String uniqueId) {
+    private void setUpDisplay(int displayId, String uniqueId, LogicalDisplay logicalDisplayMock,
+            DisplayDevice displayDeviceMock, DisplayDeviceConfig displayDeviceConfigMock) {
         DisplayInfo info = new DisplayInfo();
         DisplayDeviceInfo deviceInfo = new DisplayDeviceInfo();
 
-        when(mLogicalDisplayMock.getDisplayIdLocked()).thenReturn(displayId);
-        when(mLogicalDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(mDisplayDeviceMock);
-        when(mLogicalDisplayMock.getDisplayInfoLocked()).thenReturn(info);
-        when(mLogicalDisplayMock.isEnabledLocked()).thenReturn(true);
-        when(mLogicalDisplayMock.isInTransitionLocked()).thenReturn(false);
-        when(mDisplayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo);
-        when(mDisplayDeviceMock.getUniqueId()).thenReturn(uniqueId);
-        when(mDisplayDeviceMock.getDisplayDeviceConfig()).thenReturn(mDisplayDeviceConfigMock);
-        when(mDisplayDeviceConfigMock.getProximitySensor()).thenReturn(
+        when(logicalDisplayMock.getDisplayIdLocked()).thenReturn(displayId);
+        when(logicalDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(displayDeviceMock);
+        when(logicalDisplayMock.getDisplayInfoLocked()).thenReturn(info);
+        when(logicalDisplayMock.isEnabledLocked()).thenReturn(true);
+        when(logicalDisplayMock.isInTransitionLocked()).thenReturn(false);
+        when(displayDeviceMock.getDisplayDeviceInfoLocked()).thenReturn(deviceInfo);
+        when(displayDeviceMock.getUniqueId()).thenReturn(uniqueId);
+        when(displayDeviceMock.getDisplayDeviceConfig()).thenReturn(displayDeviceConfigMock);
+        when(displayDeviceConfigMock.getProximitySensor()).thenReturn(
                 new DisplayDeviceConfig.SensorData() {
                     {
                         type = Sensor.STRING_TYPE_PROXIMITY;
                         name = null;
                     }
                 });
-        when(mDisplayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
+        when(displayDeviceConfigMock.getNits()).thenReturn(new float[]{2, 500});
+        when(displayDeviceConfigMock.isAutoBrightnessAvailable()).thenReturn(true);
+        when(displayDeviceConfigMock.getAmbientLightSensor()).thenReturn(
+                new DisplayDeviceConfig.SensorData());
+        when(displayDeviceConfigMock.getScreenOffBrightnessSensor()).thenReturn(
+                new DisplayDeviceConfig.SensorData());
     }
 
     @Test
-    public void testDisplayBrightnessFollowers() {
-        setUpDisplay(DISPLAY_ID, UNIQUE_DISPLAY_ID);
+    public void testDisplayBrightnessFollowers_BothDpcsSupportNits() {
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
 
-        DisplayPowerController defaultDpc = new DisplayPowerController(
-                mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
-                mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
-                mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
-        }, mHighBrightnessModeMetadataMock);
-        DisplayPowerController followerDpc = new DisplayPowerController(
-                mContextSpy, mInjector, mDisplayPowerCallbacksMock, mHandler,
-                mSensorManagerMock, mDisplayBlankerMock, mLogicalDisplayMock,
-                mBrightnessTrackerMock, mBrightnessSettingMock, () -> {
-        }, mHighBrightnessModeMetadataMock);
+        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
 
-        defaultDpc.addDisplayBrightnessFollower(followerDpc);
+        mDpc.addDisplayBrightnessFollower(mFollowerDpc);
 
-        defaultDpc.setBrightness(0.3f);
-        assertEquals(defaultDpc.getBrightnessInfo().brightness,
-                followerDpc.getBrightnessInfo().brightness, 0);
+        // Test different float scale values
+        float leadBrightness = 0.3f;
+        float followerBrightness = 0.4f;
+        float nits = 300;
+        when(mAutomaticBrightnessControllerMock.convertToNits(leadBrightness)).thenReturn(nits);
+        when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(nits))
+                .thenReturn(followerBrightness);
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(leadBrightness);
+        listener.onBrightnessChanged(leadBrightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(leadBrightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock).animateTo(eq(followerBrightness), anyFloat(),
+                anyFloat());
 
-        defaultDpc.setBrightness(0.6f);
-        assertEquals(defaultDpc.getBrightnessInfo().brightness,
-                followerDpc.getBrightnessInfo().brightness, 0);
+        clearInvocations(mDualRampAnimatorMock, mFollowerDualRampAnimatorMock);
 
-        float brightness = 0.1f;
-        defaultDpc.clearDisplayBrightnessFollowers();
-        defaultDpc.setBrightness(brightness);
-        assertNotEquals(brightness, followerDpc.getBrightnessInfo().brightness, 0);
+        // Test the same float scale value
+        float brightness = 0.6f;
+        nits = 600;
+        when(mAutomaticBrightnessControllerMock.convertToNits(brightness)).thenReturn(nits);
+        when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(nits))
+                .thenReturn(brightness);
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+        listener.onBrightnessChanged(brightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+
+        clearInvocations(mDualRampAnimatorMock, mFollowerDualRampAnimatorMock);
+
+        // Test clear followers
+        mDpc.clearDisplayBrightnessFollowers();
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(leadBrightness);
+        listener.onBrightnessChanged(leadBrightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(leadBrightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock, never()).animateTo(eq(followerBrightness), anyFloat(),
+                anyFloat());
+    }
+
+    @Test
+    public void testDisplayBrightnessFollowers_FollowerDoesNotSupportNits() {
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+        mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+        float brightness = 0.3f;
+        when(mAutomaticBrightnessControllerMock.convertToNits(brightness)).thenReturn(300f);
+        when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(anyFloat()))
+                .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+        listener.onBrightnessChanged(brightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+    }
+
+    @Test
+    public void testDisplayBrightnessFollowers_LeadDpcDoesNotSupportNits() {
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+        mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+        float brightness = 0.3f;
+        when(mAutomaticBrightnessControllerMock.convertToNits(anyFloat())).thenReturn(-1f);
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+        listener.onBrightnessChanged(brightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+    }
+
+    @Test
+    public void testDisplayBrightnessFollowers_NeitherDpcSupportsNits() {
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        mDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        mFollowerDpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(mBrightnessSettingMock).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+
+        mDpc.addDisplayBrightnessFollower(mFollowerDpc);
+
+        float brightness = 0.3f;
+        when(mAutomaticBrightnessControllerMock.convertToNits(anyFloat())).thenReturn(-1f);
+        when(mFollowerAutomaticBrightnessControllerMock.convertToFloatScale(anyFloat()))
+                .thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        when(mBrightnessSettingMock.getBrightness()).thenReturn(brightness);
+        listener.onBrightnessChanged(brightness);
+        advanceTime(1); // Send messages, run updatePowerState
+        verify(mDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
+        verify(mFollowerDualRampAnimatorMock).animateTo(eq(brightness), anyFloat(), anyFloat());
     }
 }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index d03d196..1ed2f78 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -20,6 +20,7 @@
 
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
@@ -316,21 +317,10 @@
     }
 
     @Test
-    public void testGetBootUser_Headless_UserCreatedIfOnlySystemUserExists() throws Exception {
+    public void testGetBootUser_Headless_ThrowsIfOnlySystemUserExists() throws Exception {
         setSystemUserHeadless(true);
 
-        int bootUser = mUmi.getBootUser();
-
-        assertWithMessage("getStartingUser")
-                .that(bootUser).isNotEqualTo(UserHandle.USER_SYSTEM);
-
-        UserData newUser = mUsers.get(bootUser);
-        assertWithMessage("New boot user is a full user")
-                .that(newUser.info.isFull()).isTrue();
-        assertWithMessage("New boot user is an admin user")
-                .that(newUser.info.isAdmin()).isTrue();
-        assertWithMessage("New boot user is the main user")
-                .that(newUser.info.isMain()).isTrue();
+        assertThrows(UserManager.CheckedUserOperationException.class, () -> mUmi.getBootUser());
     }
 
     private void mockCurrentUser(@UserIdInt int userId) {
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
index d996e37..bf23d9d 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java
@@ -19,6 +19,7 @@
 import static android.accessibilityservice.MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN;
 
 import static com.android.server.accessibility.magnification.FullScreenMagnificationController.MagnificationInfoChangedCallback;
+import static com.android.server.accessibility.magnification.MockWindowMagnificationConnection.TEST_DISPLAY;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -48,6 +49,9 @@
 import android.graphics.Region;
 import android.hardware.display.DisplayManagerInternal;
 import android.os.Looper;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
 import android.view.DisplayInfo;
 import android.view.MagnificationSpec;
 import android.view.accessibility.MagnificationAnimationCallback;
@@ -55,6 +59,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.server.LocalServices;
 import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.test.MessageCapturingHandler;
@@ -71,6 +76,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 import org.mockito.stubbing.Answer;
+import org.testng.Assert;
 
 import java.util.Locale;
 
@@ -93,6 +99,7 @@
     static final int DISPLAY_1 = 1;
     static final int DISPLAY_COUNT = 2;
     static final int INVALID_DISPLAY = 2;
+    private static final int CURRENT_USER_ID = UserHandle.USER_SYSTEM;
 
     final FullScreenMagnificationController.ControllerContext mMockControllerCtx =
             mock(FullScreenMagnificationController.ControllerContext.class);
@@ -105,8 +112,8 @@
             MagnificationInfoChangedCallback.class);
     private final MessageCapturingHandler mMessageCapturingHandler = new MessageCapturingHandler(
             null);
-    private final MagnificationScaleProvider mScaleProvider = mock(
-            MagnificationScaleProvider.class);
+    private MagnificationScaleProvider mScaleProvider;
+    private MockContentResolver mResolver;
 
     private final ArgumentCaptor<MagnificationConfig> mConfigCaptor = ArgumentCaptor.forClass(
             MagnificationConfig.class);
@@ -129,6 +136,12 @@
         when(mMockControllerCtx.getWindowManager()).thenReturn(mMockWindowManager);
         when(mMockControllerCtx.getHandler()).thenReturn(mMessageCapturingHandler);
         when(mMockControllerCtx.getAnimationDuration()).thenReturn(1000L);
+        mResolver = new MockContentResolver();
+        mResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        when(mMockContext.getContentResolver()).thenReturn(mResolver);
+        Settings.Secure.putFloatForUser(mResolver,
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.0f,
+                CURRENT_USER_ID);
         initMockWindowManager();
 
         final DisplayInfo displayInfo = new DisplayInfo();
@@ -137,6 +150,7 @@
         LocalServices.removeServiceForTest(DisplayManagerInternal.class);
         LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
 
+        mScaleProvider = new MagnificationScaleProvider(mMockContext);
         mFullScreenMagnificationController = new FullScreenMagnificationController(
                 mMockControllerCtx, new Object(), mRequestObserver, mScaleProvider);
     }
@@ -1168,6 +1182,20 @@
         verify(mRequestObserver).onImeWindowVisibilityChanged(eq(DISPLAY_0), eq(true));
     }
 
+    @Test
+    public void persistScale_setValueWhenScaleIsOne_nothingChanged() {
+        final float persistedScale =
+                mFullScreenMagnificationController.getPersistedScale(TEST_DISPLAY);
+
+        PointF pivotPoint = INITIAL_BOUNDS_LOWER_RIGHT_2X_CENTER;
+        mFullScreenMagnificationController.setScale(DISPLAY_0, 1.0f, pivotPoint.x, pivotPoint.y,
+                false, SERVICE_ID_1);
+        mFullScreenMagnificationController.persistScale(TEST_DISPLAY);
+
+        Assert.assertEquals(mFullScreenMagnificationController.getPersistedScale(TEST_DISPLAY),
+                persistedScale);
+    }
+
     private void setScaleToMagnifying() {
         register(DISPLAY_0);
         float scale = 2.0f;
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
index 25ad2be..d841dfc 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java
@@ -274,6 +274,19 @@
     }
 
     @Test
+    public void persistScale_setValueWhenScaleIsOne_nothingChanged() {
+        mWindowMagnificationManager.setConnection(mMockConnection.getConnection());
+        final float persistedScale = mWindowMagnificationManager.getPersistedScale(TEST_DISPLAY);
+
+        mWindowMagnificationManager.setScale(TEST_DISPLAY, 1.0f);
+        mWindowMagnificationManager.persistScale(TEST_DISPLAY);
+
+        assertEquals(Settings.Secure.getFloatForUser(mResolver,
+                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 0f,
+                CURRENT_USER_ID), persistedScale);
+    }
+
+    @Test
     public void scaleSetterGetter_enabledOnTestDisplay_expectedValue() {
         mWindowMagnificationManager.setConnection(mMockConnection.getConnection());
         mWindowMagnificationManager.enableWindowMagnification(TEST_DISPLAY, 2.0f, NaN, NaN);
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index b146c27..18d629f 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -412,7 +412,7 @@
         mInjector.mHandler.clearAllRecordedMessages();
         // Verify that continueUserSwitch worked as expected
         continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
-        verify(mInjector, times(0)).dismissKeyguard(any(), anyString());
+        verify(mInjector, times(0)).dismissKeyguard(any());
         verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
         continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false);
         verifySystemUserVisibilityChangesNeverNotified();
@@ -433,7 +433,7 @@
         mInjector.mHandler.clearAllRecordedMessages();
         // Verify that continueUserSwitch worked as expected
         continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
-        verify(mInjector, times(1)).dismissKeyguard(any(), anyString());
+        verify(mInjector, times(1)).dismissKeyguard(any());
         verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
         continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false);
         verifySystemUserVisibilityChangesNeverNotified();
@@ -1148,7 +1148,7 @@
         }
 
         @Override
-        protected void dismissKeyguard(Runnable runnable, String reason) {
+        protected void dismissKeyguard(Runnable runnable) {
             runnable.run();
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java b/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
index a192913..8f2a1e5 100644
--- a/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DeviceStateToLayoutMapTest.java
@@ -19,6 +19,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+import android.view.Display;
 import android.view.DisplayAddress;
 
 import androidx.test.filters.SmallTest;
@@ -65,11 +66,13 @@
         testLayout.createDisplayLocked(
                 DisplayAddress.fromPhysicalDisplayId(123456L), /* isDefault= */ true,
                 /* isEnabled= */ true, mDisplayIdProducerMock,
-                /* brightnessThrottlingMapId= */ null);
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         testLayout.createDisplayLocked(
                 DisplayAddress.fromPhysicalDisplayId(78910L), /* isDefault= */ false,
                 /* isEnabled= */ false, mDisplayIdProducerMock,
-                /* brightnessThrottlingMapId= */ null);
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         assertEquals(testLayout, configLayout);
     }
 
@@ -81,11 +84,13 @@
         testLayout.createDisplayLocked(
                 DisplayAddress.fromPhysicalDisplayId(78910L), /* isDefault= */ true,
                 /* isEnabled= */ true, mDisplayIdProducerMock,
-                /* brightnessThrottlingMapId= */ null);
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         testLayout.createDisplayLocked(
                 DisplayAddress.fromPhysicalDisplayId(123456L), /* isDefault= */ false,
                 /* isEnabled= */ false, mDisplayIdProducerMock,
-                /* brightnessThrottlingMapId= */ null);
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
 
         assertEquals(testLayout, configLayout);
     }
@@ -99,13 +104,15 @@
         Layout.Display display1 = testLayout.createDisplayLocked(
                 DisplayAddress.fromPhysicalDisplayId(345L), /* isDefault= */ true,
                 /* isEnabled= */ true, mDisplayIdProducerMock,
-                /* brightnessThrottlingMapId= */ "concurrent");
+                /* brightnessThrottlingMapId= */ "concurrent",
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         display1.setPosition(Layout.Display.POSITION_FRONT);
 
         Layout.Display display2 = testLayout.createDisplayLocked(
                 DisplayAddress.fromPhysicalDisplayId(678L), /* isDefault= */ false,
                 /* isEnabled= */ true, mDisplayIdProducerMock,
-                /* brightnessThrottlingMapId= */ "concurrent");
+                /* brightnessThrottlingMapId= */ "concurrent",
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         display2.setPosition(Layout.Display.POSITION_REAR);
 
         assertEquals(testLayout, configLayout);
@@ -119,6 +126,26 @@
         assertEquals(Layout.Display.POSITION_REAR, configLayout.getAt(1).getPosition());
     }
 
+    @Test
+    public void testRefreshRateZoneId() {
+        Layout configLayout = mDeviceStateToLayoutMap.get(3);
+
+        Layout testLayout = new Layout();
+        Layout.Display display1 = testLayout.createDisplayLocked(
+                DisplayAddress.fromPhysicalDisplayId(345L), /* isDefault= */ true,
+                /* isEnabled= */ true, mDisplayIdProducerMock,
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
+        display1.setRefreshRateZoneId("test1");
+        testLayout.createDisplayLocked(
+                DisplayAddress.fromPhysicalDisplayId(678L), /* isDefault= */ false,
+                /* isEnabled= */ true, mDisplayIdProducerMock,
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
+
+        assertEquals(testLayout, configLayout);
+    }
+
     ////////////////////
     // Helper Methods //
     ////////////////////
@@ -166,6 +193,17 @@
                 +        "<brightnessThrottlingMapId>concurrent</brightnessThrottlingMapId>\n"
                 +      "</display>\n"
                 +    "</layout>\n"
+
+                +    "<layout>\n"
+                +      "<state>3</state> \n"
+                +      "<display enabled=\"true\" defaultDisplay=\"true\" "
+                +                                               "refreshRateZoneId=\"test1\">\n"
+                +        "<address>345</address>\n"
+                +      "</display>\n"
+                +      "<display enabled=\"true\">\n"
+                +        "<address>678</address>\n"
+                +      "</display>\n"
+                +    "</layout>\n"
                 +  "</layouts>\n";
     }
 }
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 42bdbec..0e9c171 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -155,6 +155,13 @@
         assertEquals(90, mDisplayDeviceConfig.getDefaultHighBlockingZoneRefreshRate());
         assertEquals(85, mDisplayDeviceConfig.getDefaultPeakRefreshRate());
         assertEquals(45, mDisplayDeviceConfig.getDefaultRefreshRate());
+
+        assertEquals(2, mDisplayDeviceConfig.getRefreshRangeProfiles().size());
+        assertEquals(60, mDisplayDeviceConfig.getRefreshRange("test1").min, SMALL_DELTA);
+        assertEquals(60, mDisplayDeviceConfig.getRefreshRange("test1").max, SMALL_DELTA);
+        assertEquals(80, mDisplayDeviceConfig.getRefreshRange("test2").min, SMALL_DELTA);
+        assertEquals(90, mDisplayDeviceConfig.getRefreshRange("test2").max, SMALL_DELTA);
+
         assertArrayEquals(new int[]{45, 55},
                 mDisplayDeviceConfig.getLowDisplayBrightnessThresholds());
         assertArrayEquals(new int[]{50, 60},
@@ -288,6 +295,9 @@
                 DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE);
         assertEquals(mDisplayDeviceConfig.getDefaultPeakRefreshRate(), DEFAULT_PEAK_REFRESH_RATE);
         assertEquals(mDisplayDeviceConfig.getDefaultRefreshRate(), DEFAULT_REFRESH_RATE);
+
+        assertEquals(0, mDisplayDeviceConfig.getRefreshRangeProfiles().size());
+
         assertArrayEquals(mDisplayDeviceConfig.getLowDisplayBrightnessThresholds(),
                 LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
         assertArrayEquals(mDisplayDeviceConfig.getLowAmbientBrightnessThresholds(),
@@ -547,6 +557,20 @@
                 +   "<refreshRate>\n"
                 +       "<defaultRefreshRate>45</defaultRefreshRate>\n"
                 +       "<defaultPeakRefreshRate>85</defaultPeakRefreshRate>\n"
+                +       "<refreshRateZoneProfiles>"
+                +           "<refreshRateZoneProfile id=\"test1\">"
+                +               "<refreshRateRange>\n"
+                +                   "<minimum>60</minimum>\n"
+                +                   "<maximum>60</maximum>\n"
+                +               "</refreshRateRange>\n"
+                +           "</refreshRateZoneProfile>\n"
+                +           "<refreshRateZoneProfile id=\"test2\">"
+                +               "<refreshRateRange>\n"
+                +                   "<minimum>80</minimum>\n"
+                +                   "<maximum>90</maximum>\n"
+                +               "</refreshRateRange>\n"
+                +           "</refreshRateZoneProfile>\n"
+                +       "</refreshRateZoneProfiles>"
                 +       "<lowerBlockingZoneConfigs>\n"
                 +           "<defaultRefreshRate>75</defaultRefreshRate>\n"
                 +           "<blockingZoneThreshold>\n"
diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 8a37ed9..bd2b5fd 100644
--- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -299,9 +299,11 @@
 
         Layout layout1 = new Layout();
         layout1.createDisplayLocked(info(device1).address, /* isDefault= */ true,
-                /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null);
+                /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         layout1.createDisplayLocked(info(device2).address, /* isDefault= */ false,
-                /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null);
+                /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT)).thenReturn(layout1);
         assertThat(layout1.size()).isEqualTo(2);
         final int logicalId2 = layout1.getByAddress(info(device2).address).getLogicalDisplayId();
@@ -335,16 +337,19 @@
 
         Layout layout1 = new Layout();
         layout1.createDisplayLocked(info(device1).address, /* isDefault= */ true,
-                /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null);
+                /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT)).thenReturn(layout1);
 
         final int layoutState2 = 2;
         Layout layout2 = new Layout();
         layout2.createDisplayLocked(info(device2).address, /* isDefault= */ false,
-                /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null);
+                /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         // Device3 is the default display.
         layout2.createDisplayLocked(info(device3).address, /* isDefault= */ true,
-                /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null);
+                /* isEnabled= */ true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         when(mDeviceStateToLayoutMapSpy.get(layoutState2)).thenReturn(layout2);
         assertThat(layout2.size()).isEqualTo(2);
         final int logicalId2 = layout2.getByAddress(info(device2).address).getLogicalDisplayId();
@@ -567,17 +572,21 @@
         Layout layout = new Layout();
         layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address,
                 true, true, mIdProducer,
-                /* brightnessThrottlingMapId= */ "concurrent");
+                /* brightnessThrottlingMapId= */ "concurrent",
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address,
                 false, true, mIdProducer,
-                /* brightnessThrottlingMapId= */ "concurrent");
+                /* brightnessThrottlingMapId= */ "concurrent",
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(layout);
 
         layout = new Layout();
         layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address,
-                false, false, mIdProducer, /* brightnessThrottlingMapId= */ null);
+                false, false, mIdProducer, /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address,
-                true, true, mIdProducer, /* brightnessThrottlingMapId= */ null);
+                true, true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(layout);
         when(mDeviceStateToLayoutMapSpy.get(2)).thenReturn(layout);
 
@@ -604,6 +613,10 @@
         assertTrue(mLogicalDisplayMapper.getDisplayLocked(device2).isEnabledLocked());
         assertFalse(mLogicalDisplayMapper.getDisplayLocked(device1).isInTransitionLocked());
         assertFalse(mLogicalDisplayMapper.getDisplayLocked(device2).isInTransitionLocked());
+        assertEquals(-1, mLogicalDisplayMapper.getDisplayLocked(device1)
+                .getLeadDisplayLocked());
+        assertEquals(0, mLogicalDisplayMapper.getDisplayLocked(device2)
+                .getLeadDisplayLocked());
         assertEquals("concurrent", mLogicalDisplayMapper.getDisplayLocked(device1)
                 .getBrightnessThrottlingDataIdLocked());
         assertEquals("concurrent", mLogicalDisplayMapper.getDisplayLocked(device2)
@@ -655,19 +668,22 @@
                 /* isDefault= */ true,
                 /* isEnabled= */ true,
                 mIdProducer,
-                /* brightnessThrottlingMapId= */ null);
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         threeDevicesEnabledLayout.createDisplayLocked(
                 displayAddressTwo,
                 /* isDefault= */ false,
                 /* isEnabled= */ true,
                 mIdProducer,
-                /* brightnessThrottlingMapId= */ null);
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         threeDevicesEnabledLayout.createDisplayLocked(
                 displayAddressThree,
                 /* isDefault= */ false,
                 /* isEnabled= */ true,
                 mIdProducer,
-                /* brightnessThrottlingMapId= */ null);
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
 
         when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT))
                 .thenReturn(threeDevicesEnabledLayout);
@@ -703,19 +719,22 @@
                 /* isDefault= */ true,
                 /* isEnabled= */ true,
                 mIdProducer,
-                /* brightnessThrottlingMapId= */ null);
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         oneDeviceEnabledLayout.createDisplayLocked(
                 displayAddressTwo,
                 /* isDefault= */ false,
                 /* isEnabled= */ false,
                 mIdProducer,
-                /* brightnessThrottlingMapId= */ null);
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         oneDeviceEnabledLayout.createDisplayLocked(
                 displayAddressThree,
                 /* isDefault= */ false,
                 /* isEnabled= */ false,
                 mIdProducer,
-                /* brightnessThrottlingMapId= */ null);
+                /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
 
         when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(oneDeviceEnabledLayout);
         when(mDeviceStateToLayoutMapSpy.get(1)).thenReturn(threeDevicesEnabledLayout);
@@ -790,10 +809,11 @@
 
         Layout layout = new Layout();
         layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address,
-                true, true, mIdProducer, /* brightnessThrottlingMapId= */ null);
+                true, true, mIdProducer, /* brightnessThrottlingMapId= */ null,
+                /* leadDisplayId= */ Display.DEFAULT_DISPLAY);
         layout.createDisplayLocked(device2.getDisplayDeviceInfoLocked().address,
                 false, true, mIdProducer, /* brightnessThrottlingMapId= */ null,
-                POSITION_REAR);
+                POSITION_REAR, Display.DEFAULT_DISPLAY);
         when(mDeviceStateToLayoutMapSpy.get(0)).thenReturn(layout);
 
         when(mDeviceStateToLayoutMapSpy.size()).thenReturn(1);
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index 5ca695b..c9612cd 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -49,8 +49,8 @@
 import android.os.UserManager;
 import android.os.storage.IStorageManager;
 import android.os.storage.StorageManager;
-import android.provider.Settings;
 import android.provider.DeviceConfig;
+import android.provider.Settings;
 import android.security.KeyStore;
 
 import androidx.test.InstrumentationRegistry;
@@ -83,16 +83,15 @@
     protected static final int MANAGED_PROFILE_USER_ID = 12;
     protected static final int TURNED_OFF_PROFILE_USER_ID = 17;
     protected static final int SECONDARY_USER_ID = 20;
+    protected static final int TERTIARY_USER_ID = 21;
 
-    private static final UserInfo PRIMARY_USER_INFO = new UserInfo(PRIMARY_USER_ID, null, null,
-            UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY
-                    | UserInfo.FLAG_MAIN);
-    private static final UserInfo SECONDARY_USER_INFO = new UserInfo(SECONDARY_USER_ID, null, null,
-            UserInfo.FLAG_INITIALIZED);
+    protected UserInfo mPrimaryUserInfo;
+    protected UserInfo mSecondaryUserInfo;
+    protected UserInfo mTertiaryUserInfo;
 
     private ArrayList<UserInfo> mPrimaryUserProfiles = new ArrayList<>();
 
-    LockSettingsService mService;
+    LockSettingsServiceTestable mService;
     LockSettingsInternal mLocalService;
 
     MockLockSettingsContext mContext;
@@ -117,6 +116,7 @@
     FingerprintManager mFingerprintManager;
     FaceManager mFaceManager;
     PackageManager mPackageManager;
+    LockSettingsServiceTestable.MockInjector mInjector;
     @Rule
     public FakeSettingsProviderRule mSettingsRule = FakeSettingsProvider.rule();
 
@@ -162,22 +162,61 @@
         mSpManager = new MockSyntheticPasswordManager(mContext, mStorage, mGateKeeperService,
                 mUserManager, mPasswordSlotManager);
         mAuthSecretService = mock(IAuthSecret.class);
-        mService = new LockSettingsServiceTestable(mContext, mStorage,
-                mGateKeeperService, mKeyStore, setUpStorageManagerMock(), mActivityManager,
-                mSpManager, mAuthSecretService, mGsiService, mRecoverableKeyStoreManager,
-                mUserManagerInternal, mDeviceStateCache);
+        mInjector =
+                new LockSettingsServiceTestable.MockInjector(
+                        mContext,
+                        mStorage,
+                        mKeyStore,
+                        mActivityManager,
+                        setUpStorageManagerMock(),
+                        mSpManager,
+                        mGsiService,
+                        mRecoverableKeyStoreManager,
+                        mUserManagerInternal,
+                        mDeviceStateCache);
+        mService =
+                new LockSettingsServiceTestable(mInjector, mGateKeeperService, mAuthSecretService);
         mService.mHasSecureLockScreen = true;
-        when(mUserManager.getUserInfo(eq(PRIMARY_USER_ID))).thenReturn(PRIMARY_USER_INFO);
-        mPrimaryUserProfiles.add(PRIMARY_USER_INFO);
+        mPrimaryUserInfo =
+                new UserInfo(
+                        PRIMARY_USER_ID,
+                        null,
+                        null,
+                        UserInfo.FLAG_INITIALIZED
+                                | UserInfo.FLAG_ADMIN
+                                | UserInfo.FLAG_PRIMARY
+                                | UserInfo.FLAG_MAIN
+                                | UserInfo.FLAG_FULL);
+        mSecondaryUserInfo =
+                new UserInfo(
+                        SECONDARY_USER_ID,
+                        null,
+                        null,
+                        UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_FULL);
+        mTertiaryUserInfo =
+                new UserInfo(
+                        TERTIARY_USER_ID,
+                        null,
+                        null,
+                        UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_FULL);
+
+        when(mUserManager.getUserInfo(eq(PRIMARY_USER_ID))).thenReturn(mPrimaryUserInfo);
+        when(mUserManagerInternal.getUserInfo(eq(PRIMARY_USER_ID))).thenReturn(mPrimaryUserInfo);
+        mPrimaryUserProfiles.add(mPrimaryUserInfo);
         installChildProfile(MANAGED_PROFILE_USER_ID);
         installAndTurnOffChildProfile(TURNED_OFF_PROFILE_USER_ID);
         for (UserInfo profile : mPrimaryUserProfiles) {
             when(mUserManager.getProfiles(eq(profile.id))).thenReturn(mPrimaryUserProfiles);
         }
-        when(mUserManager.getUserInfo(eq(SECONDARY_USER_ID))).thenReturn(SECONDARY_USER_INFO);
+        when(mUserManager.getUserInfo(eq(SECONDARY_USER_ID))).thenReturn(mSecondaryUserInfo);
+        when(mUserManagerInternal.getUserInfo(eq(SECONDARY_USER_ID)))
+                .thenReturn(mSecondaryUserInfo);
+        when(mUserManager.getUserInfo(eq(TERTIARY_USER_ID))).thenReturn(mTertiaryUserInfo);
+        when(mUserManagerInternal.getUserInfo(eq(TERTIARY_USER_ID))).thenReturn(mTertiaryUserInfo);
 
         final ArrayList<UserInfo> allUsers = new ArrayList<>(mPrimaryUserProfiles);
-        allUsers.add(SECONDARY_USER_INFO);
+        allUsers.add(mSecondaryUserInfo);
+        allUsers.add(mTertiaryUserInfo);
         when(mUserManager.getUsers()).thenReturn(allUsers);
 
         when(mActivityManager.unlockUser2(anyInt(), any())).thenAnswer(
@@ -227,9 +266,10 @@
         userInfo.profileGroupId = PRIMARY_USER_ID;
         mPrimaryUserProfiles.add(userInfo);
         when(mUserManager.getUserInfo(eq(profileId))).thenReturn(userInfo);
-        when(mUserManager.getProfileParent(eq(profileId))).thenReturn(PRIMARY_USER_INFO);
+        when(mUserManager.getProfileParent(eq(profileId))).thenReturn(mPrimaryUserInfo);
         when(mUserManager.isUserRunning(eq(profileId))).thenReturn(true);
         when(mUserManager.isUserUnlocked(eq(profileId))).thenReturn(true);
+        when(mUserManagerInternal.getUserInfo(eq(profileId))).thenReturn(userInfo);
         // TODO(b/258213147): Remove
         when(mUserManagerInternal.isUserManaged(eq(profileId))).thenReturn(true);
         when(mDeviceStateCache.isUserOrganizationManaged(eq(profileId)))
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
index f0f0632..9686c38 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTestable.java
@@ -30,6 +30,7 @@
 import android.os.storage.IStorageManager;
 import android.security.KeyStore;
 import android.security.keystore.KeyPermanentlyInvalidatedException;
+import android.service.gatekeeper.IGateKeeperService;
 
 import com.android.internal.widget.LockscreenCredential;
 import com.android.server.ServiceThread;
@@ -40,7 +41,7 @@
 
 public class LockSettingsServiceTestable extends LockSettingsService {
 
-    private static class MockInjector extends LockSettingsService.Injector {
+    public static class MockInjector extends LockSettingsService.Injector {
 
         private LockSettingsStorage mLockSettingsStorage;
         private KeyStore mKeyStore;
@@ -52,6 +53,9 @@
         private UserManagerInternal mUserManagerInternal;
         private DeviceStateCache mDeviceStateCache;
 
+        public boolean mIsHeadlessSystemUserMode = false;
+        public boolean mIsMainUserPermanentAdmin = false;
+
         public MockInjector(Context context, LockSettingsStorage storage, KeyStore keyStore,
                 IActivityManager activityManager,
                 IStorageManager storageManager, SyntheticPasswordManager spManager,
@@ -140,19 +144,22 @@
             return mock(ManagedProfilePasswordCache.class);
         }
 
+        @Override
+        public boolean isHeadlessSystemUserMode() {
+            return mIsHeadlessSystemUserMode;
+        }
+
+        @Override
+        public boolean isMainUserPermanentAdmin() {
+            return mIsMainUserPermanentAdmin;
+        }
     }
 
-    public MockInjector mInjector;
-
-    protected LockSettingsServiceTestable(Context context,
-            LockSettingsStorage storage, FakeGateKeeperService gatekeeper, KeyStore keystore,
-            IStorageManager storageManager, IActivityManager mActivityManager,
-            SyntheticPasswordManager spManager, IAuthSecret authSecretService,
-            FakeGsiService gsiService, RecoverableKeyStoreManager recoverableKeyStoreManager,
-            UserManagerInternal userManagerInternal, DeviceStateCache deviceStateCache) {
-        super(new MockInjector(context, storage, keystore, mActivityManager,
-                storageManager, spManager, gsiService, recoverableKeyStoreManager,
-                userManagerInternal, deviceStateCache));
+    protected LockSettingsServiceTestable(
+            LockSettingsService.Injector injector,
+            IGateKeeperService gatekeeper,
+            IAuthSecret authSecretService) {
+        super(injector);
         mGateKeeperService = gatekeeper;
         mAuthSecretService = authSecretService;
     }
@@ -199,4 +206,10 @@
         UserInfo userInfo = mUserManager.getUserInfo(userId);
         return userInfo.isCloneProfile() || userInfo.isManagedProfile();
     }
+
+    void clearAuthSecret() {
+        synchronized (mHeadlessAuthSecretLock) {
+            mAuthSecret = null;
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
index 57593cf..62d8a76 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java
@@ -16,6 +16,10 @@
 
 package com.android.server.locksettings;
 
+import static android.content.pm.UserInfo.FLAG_FULL;
+import static android.content.pm.UserInfo.FLAG_MAIN;
+import static android.content.pm.UserInfo.FLAG_PRIMARY;
+
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN;
@@ -27,9 +31,9 @@
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -247,6 +251,15 @@
 
     @Test
     public void testUnlockUserKeyIfUnsecuredPassesPrimaryUserAuthSecret() throws RemoteException {
+        initSpAndSetCredential(PRIMARY_USER_ID, newPassword(null));
+        reset(mAuthSecretService);
+        mLocalService.unlockUserKeyIfUnsecured(PRIMARY_USER_ID);
+        verify(mAuthSecretService).setPrimaryUserCredential(any(byte[].class));
+    }
+
+    @Test
+    public void testUnlockUserKeyIfUnsecuredPassesPrimaryUserAuthSecretIfPasswordIsCleared()
+            throws RemoteException {
         LockscreenCredential password = newPassword("password");
         initSpAndSetCredential(PRIMARY_USER_ID, password);
         mService.setLockCredential(nonePassword(), password, PRIMARY_USER_ID);
@@ -256,6 +269,56 @@
         verify(mAuthSecretService).setPrimaryUserCredential(any(byte[].class));
     }
 
+    private void setupHeadlessTest() {
+        mInjector.mIsHeadlessSystemUserMode = true;
+        mInjector.mIsMainUserPermanentAdmin = true;
+        mPrimaryUserInfo.flags &= ~(FLAG_FULL | FLAG_PRIMARY);
+        mSecondaryUserInfo.flags |= FLAG_MAIN;
+        mService.initializeSyntheticPassword(PRIMARY_USER_ID);
+        mService.initializeSyntheticPassword(SECONDARY_USER_ID);
+        mService.initializeSyntheticPassword(TERTIARY_USER_ID);
+        reset(mAuthSecretService);
+    }
+
+    @Test
+    public void testHeadlessSystemUserDoesNotPassAuthSecret() throws RemoteException {
+        setupHeadlessTest();
+        mLocalService.unlockUserKeyIfUnsecured(PRIMARY_USER_ID);
+        verify(mAuthSecretService, never()).setPrimaryUserCredential(any(byte[].class));
+    }
+
+    @Test
+    public void testHeadlessSecondaryUserPassesAuthSecret() throws RemoteException {
+        setupHeadlessTest();
+        mLocalService.unlockUserKeyIfUnsecured(SECONDARY_USER_ID);
+        verify(mAuthSecretService).setPrimaryUserCredential(any(byte[].class));
+    }
+
+    @Test
+    public void testHeadlessTertiaryUserPassesSameAuthSecret() throws RemoteException {
+        setupHeadlessTest();
+        mLocalService.unlockUserKeyIfUnsecured(SECONDARY_USER_ID);
+        var captor = ArgumentCaptor.forClass(byte[].class);
+        verify(mAuthSecretService).setPrimaryUserCredential(captor.capture());
+        var value = captor.getValue();
+        reset(mAuthSecretService);
+        mLocalService.unlockUserKeyIfUnsecured(TERTIARY_USER_ID);
+        verify(mAuthSecretService).setPrimaryUserCredential(eq(value));
+    }
+
+    @Test
+    public void testHeadlessTertiaryUserPassesSameAuthSecretAfterReset() throws RemoteException {
+        setupHeadlessTest();
+        mLocalService.unlockUserKeyIfUnsecured(SECONDARY_USER_ID);
+        var captor = ArgumentCaptor.forClass(byte[].class);
+        verify(mAuthSecretService).setPrimaryUserCredential(captor.capture());
+        var value = captor.getValue();
+        mService.clearAuthSecret();
+        reset(mAuthSecretService);
+        mLocalService.unlockUserKeyIfUnsecured(TERTIARY_USER_ID);
+        verify(mAuthSecretService).setPrimaryUserCredential(eq(value));
+    }
+
     @Test
     public void testTokenBasedResetPassword() throws RemoteException {
         LockscreenCredential password = newPassword("password");
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
index 966c047..50f3a88 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java
@@ -8,6 +8,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.server.locksettings.LockSettingsStorage.PersistentData;
+
 import com.google.android.collect.Sets;
 
 import org.junit.Before;
@@ -29,7 +30,7 @@
     // sequentially, starting at slot 0.
     @Test
     public void testFrpWeaverSlotNotReused() {
-        final int userId = 10;
+        final int userId = SECONDARY_USER_ID;
         final int frpWeaverSlot = 0;
 
         setDeviceProvisioned(false);
@@ -45,7 +46,7 @@
     // it's here as a control for testFrpWeaverSlotNotReused().
     @Test
     public void testFrpWeaverSlotReused() {
-        final int userId = 10;
+        final int userId = SECONDARY_USER_ID;
         final int frpWeaverSlot = 0;
 
         setDeviceProvisioned(true);
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index c760efd..8a292cb 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -302,7 +302,7 @@
     @MediumTest
     @Test
     public void testRemoveUserByHandle_ThrowsException() {
-        assertThrows(IllegalArgumentException.class, () -> removeUser(null));
+        assertThrows(IllegalArgumentException.class, () -> mUserManager.removeUser(null));
     }
 
     @MediumTest
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 23587c9..5c69c84 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -78,6 +78,7 @@
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNotSame;
 import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.Assert.fail;
@@ -10083,4 +10084,211 @@
         mInternalService.sendReviewPermissionsNotification();
         verify(mMockNm, never()).notify(anyString(), anyInt(), any(Notification.class));
     }
+
+    @Test
+    public void fixSystemNotification_withOnGoingFlag_shouldBeNonDismissible()
+            throws Exception {
+        // Given: a notification from an app on the system partition has the flag
+        // FLAG_ONGOING_EVENT set
+        // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+        mService.setOngoingDismissal(true);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setOngoing(true)
+                .build();
+
+        final ApplicationInfo systemAppInfo = new ApplicationInfo();
+        systemAppInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
+        when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenReturn(systemAppInfo);
+
+        // When: fix the notification with NotificationManagerService
+        mService.fixNotification(n, PKG, "tag", 9, 0);
+
+        // Then: the notification's flag FLAG_NO_DISMISS should be set
+        assertNotSame(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+        // Avoid affecting other tests
+        mService.setOngoingDismissal(false);
+    }
+
+    @Test
+    public void fixMediaNotification_withOnGoingFlag_shouldBeNonDismissible()
+            throws Exception {
+        // Given: a media notification has the flag FLAG_ONGOING_EVENT set
+        // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+        mService.setOngoingDismissal(true);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setOngoing(true)
+                .setStyle(new Notification.MediaStyle()
+                        .setMediaSession(mock(MediaSession.Token.class)))
+                .build();
+
+        // When: fix the notification with NotificationManagerService
+        mService.fixNotification(n, PKG, "tag", 9, 0);
+
+        // Then: the notification's flag FLAG_NO_DISMISS should be set
+        assertNotSame(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+        // Avoid affecting other tests
+        mService.setOngoingDismissal(false);
+    }
+
+    @Test
+    public void fixNonExemptNotification_withOnGoingFlag_shouldBeDismissible() throws Exception {
+        // Given: a non-exempt notification has the flag FLAG_ONGOING_EVENT set
+        // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+        mService.setOngoingDismissal(true);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setOngoing(true)
+                .build();
+
+        // When: fix the notification with NotificationManagerService
+        mService.fixNotification(n, PKG, "tag", 9, 0);
+
+        // Then: the notification's flag FLAG_NO_DISMISS should not be set
+        assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+        // Avoid affecting other tests
+        mService.setOngoingDismissal(false);
+    }
+
+    @Test
+    public void fixNonExemptNotification_withNoDismissFlag_shouldBeDismissible()
+            throws Exception {
+        // Given: a non-exempt notification has the flag FLAG_NO_DISMISS set (even though this is
+        // not allowed)
+        // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+        mService.setOngoingDismissal(true);
+        Notification n = new Notification.Builder(mContext, "test")
+                .build();
+        n.flags |= Notification.FLAG_NO_DISMISS;
+
+        // When: fix the notification with NotificationManagerService
+        mService.fixNotification(n, PKG, "tag", 9, 0);
+
+        // Then: the notification's flag FLAG_NO_DISMISS should be cleared
+        assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+        // Avoid affecting other tests
+        mService.setOngoingDismissal(false);
+    }
+
+    @Test
+    public void fixSystemNotification_withoutOnGoingFlag_shouldBeDismissible() throws Exception {
+        // Given: a notification from an app on the system partition doesn't have the flag
+        // FLAG_ONGOING_EVENT set
+        // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+        mService.setOngoingDismissal(true);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setOngoing(false)
+                .build();
+
+        final ApplicationInfo systemAppInfo = new ApplicationInfo();
+        systemAppInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
+        when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenReturn(systemAppInfo);
+
+        // When: fix the notification with NotificationManagerService
+        mService.fixNotification(n, PKG, "tag", 9, 0);
+
+        // Then: the notification's flag FLAG_NO_DISMISS should not be set
+        assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+        // Avoid affecting other tests
+        mService.setOngoingDismissal(false);
+    }
+
+    @Test
+    public void fixSystemNotification_withoutOnGoingFlag_withNoDismissFlag_shouldBeDismissible()
+            throws Exception {
+        // Given: a notification from an app on the system partition doesn't have the flag
+        // FLAG_ONGOING_EVENT set, but has the flag FLAG_NO_DISMISS set
+        // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+        mService.setOngoingDismissal(true);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setOngoing(false)
+                .build();
+        n.flags |= Notification.FLAG_NO_DISMISS;
+
+        final ApplicationInfo systemAppInfo = new ApplicationInfo();
+        systemAppInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
+        when(mPackageManagerClient.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+                .thenReturn(systemAppInfo);
+
+        // When: fix the notification with NotificationManagerService
+        mService.fixNotification(n, PKG, "tag", 9, 0);
+
+        // Then: the notification's flag FLAG_NO_DISMISS should be cleared
+        assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+        // Avoid affecting other tests
+        mService.setOngoingDismissal(false);
+    }
+
+    @Test
+    public void fixMediaNotification_withoutOnGoingFlag_shouldBeDismissible() throws Exception {
+        // Given: a media notification doesn't have the flag FLAG_ONGOING_EVENT set
+        // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+        mService.setOngoingDismissal(true);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setOngoing(false)
+                .setStyle(new Notification.MediaStyle()
+                        .setMediaSession(mock(MediaSession.Token.class)))
+                .build();
+
+        // When: fix the notification with NotificationManagerService
+        mService.fixNotification(n, PKG, "tag", 9, 0);
+
+        // Then: the notification's flag FLAG_NO_DISMISS should not be set
+        assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+        // Avoid affecting other tests
+        mService.setOngoingDismissal(false);
+    }
+
+    @Test
+    public void fixMediaNotification_withoutOnGoingFlag_withNoDismissFlag_shouldBeDismissible()
+            throws Exception {
+        // Given: a media notification doesn't have the flag FLAG_ONGOING_EVENT set,
+        // but has the flag FLAG_NO_DISMISS set
+        // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+        mService.setOngoingDismissal(true);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setOngoing(false)
+                .setStyle(new Notification.MediaStyle()
+                        .setMediaSession(mock(MediaSession.Token.class)))
+                .build();
+        n.flags |= Notification.FLAG_NO_DISMISS;
+
+        // When: fix the notification with NotificationManagerService
+        mService.fixNotification(n, PKG, "tag", 9, 0);
+
+        // Then: the notification's flag FLAG_NO_DISMISS should be cleared
+        assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+        // Avoid affecting other tests
+        mService.setOngoingDismissal(false);
+    }
+
+    @Test
+    public void fixNonExempt_Notification_withoutOnGoingFlag_shouldBeDismissible()
+            throws Exception {
+        // Given: a non-exempt notification has the flag FLAG_ONGOING_EVENT set
+        // feature flag: NOTIFICATION_ONGOING_DISMISSAL is on
+        mService.setOngoingDismissal(true);
+        mService.setOngoingDismissal(true);
+        Notification n = new Notification.Builder(mContext, "test")
+                .setOngoing(false)
+                .build();
+
+        // When: fix the notification with NotificationManagerService
+        mService.fixNotification(n, PKG, "tag", 9, 0);
+
+        // Then: the notification's flag FLAG_NO_DISMISS should not be set
+        assertEquals(0, n.flags & Notification.FLAG_NO_DISMISS);
+
+        // Avoid affecting other tests
+        mService.setOngoingDismissal(false);
+    }
+
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
index 61a6985..b8c94da 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java
@@ -36,6 +36,8 @@
     int countLogSmartSuggestionsVisible = 0;
     Set<Integer> mChannelToastsSent = new HashSet<>();
 
+    public boolean ONGOING_DISMISSAL = false;
+
     String stringArrayResourceValue;
     @Nullable
     NotificationAssistantAccessGrantedCallback mNotificationAssistantAccessGrantedCallback;
@@ -159,4 +161,9 @@
             return mGetStrongAuthForUserReturnValue;
         }
     }
+
+    // Mock SystemProperties
+    protected void setOngoingDismissal(boolean ongoingDismissal) {
+        ONGOING_DISMISSAL = ongoingDismissal;
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index abc0c14..1ce8c61 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -105,6 +105,7 @@
 import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.when;
 
+import android.annotation.NonNull;
 import android.app.ActivityTaskManager;
 import android.app.WindowConfiguration;
 import android.content.res.Configuration;
@@ -123,6 +124,7 @@
 import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.view.ContentRecordingSession;
+import android.view.Display;
 import android.view.DisplayCutout;
 import android.view.DisplayInfo;
 import android.view.Gravity;
@@ -1831,6 +1833,111 @@
     }
 
     @Test
+    public void testSecondaryInternalDisplayRotationFollowsDefaultDisplay() {
+        // Skip freezing so the unrelated conditions in updateRotationUnchecked won't disturb.
+        doNothing().when(mWm).startFreezingDisplay(anyInt(), anyInt(), any(), anyInt());
+
+        final DisplayRotationCoordinator coordinator =
+                mRootWindowContainer.getDisplayRotationCoordinator();
+        final DisplayContent defaultDisplayContent = mDisplayContent;
+        final DisplayRotation defaultDisplayRotation = defaultDisplayContent.getDisplayRotation();
+        coordinator.removeDefaultDisplayRotationChangedCallback();
+
+        DeviceStateController deviceStateController = mock(DeviceStateController.class);
+        when(deviceStateController.shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay())
+                .thenReturn(true);
+
+        // Create secondary display
+        final DisplayContent secondaryDisplayContent =
+                createSecondaryDisplayContent(Display.TYPE_INTERNAL, deviceStateController);
+        final DisplayRotation secondaryDisplayRotation =
+                secondaryDisplayContent.getDisplayRotation();
+        try {
+            // TestDisplayContent bypasses this method but we need it for this test
+            doCallRealMethod().when(secondaryDisplayRotation).updateRotationUnchecked(anyBoolean());
+
+            // TestDisplayContent creates this as a mock. Lets set it up to test our use case.
+            when(secondaryDisplayContent.mDeviceStateController
+                    .shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay()).thenReturn(
+                    true);
+
+            // Check that secondary display registered callback
+            assertEquals(secondaryDisplayRotation.mDefaultDisplayRotationChangedCallback,
+                    coordinator.mDefaultDisplayRotationChangedCallback);
+
+            // Set the default display to a known orientation. This may be a zero or non-zero
+            // rotation since mDisplayInfo.logicalWidth/Height depends on the DUT's default display
+            defaultDisplayRotation.updateOrientation(SCREEN_ORIENTATION_PORTRAIT, false);
+            assertEquals(defaultDisplayRotation.mPortraitRotation,
+                    defaultDisplayRotation.getRotation());
+            assertEquals(defaultDisplayRotation.mPortraitRotation,
+                    coordinator.getDefaultDisplayCurrentRotation());
+
+            // Check that in the initial state, the secondary display is in the right rotation
+            assertRotationsAreCorrectlyReversed(defaultDisplayRotation.getRotation(),
+                    secondaryDisplayRotation.getRotation());
+
+            // Update primary display rotation, check display coordinator rotation is the default
+            // display's landscape rotation, and that the secondary display rotation is correct.
+            defaultDisplayRotation.updateOrientation(SCREEN_ORIENTATION_LANDSCAPE, false);
+            assertEquals(defaultDisplayRotation.mLandscapeRotation,
+                    defaultDisplayRotation.getRotation());
+            assertEquals(defaultDisplayRotation.mLandscapeRotation,
+                    coordinator.getDefaultDisplayCurrentRotation());
+            assertRotationsAreCorrectlyReversed(defaultDisplayRotation.getRotation(),
+                    secondaryDisplayRotation.getRotation());
+        } finally {
+            secondaryDisplayRotation.removeDefaultDisplayRotationChangedCallback();
+        }
+    }
+
+    @Test
+    public void testSecondaryNonInternalDisplayDoesNotFollowDefaultDisplay() {
+        // Skip freezing so the unrelated conditions in updateRotationUnchecked won't disturb.
+        doNothing().when(mWm).startFreezingDisplay(anyInt(), anyInt(), any(), anyInt());
+
+        final DisplayRotationCoordinator coordinator =
+                mRootWindowContainer.getDisplayRotationCoordinator();
+        coordinator.removeDefaultDisplayRotationChangedCallback();
+
+        DeviceStateController deviceStateController = mock(DeviceStateController.class);
+        when(deviceStateController.shouldMatchBuiltInDisplayOrientationToReverseDefaultDisplay())
+                .thenReturn(true);
+
+        // Create secondary non-internal displays
+        createSecondaryDisplayContent(Display.TYPE_EXTERNAL, deviceStateController);
+        assertNull(coordinator.mDefaultDisplayRotationChangedCallback);
+        createSecondaryDisplayContent(Display.TYPE_VIRTUAL, deviceStateController);
+        assertNull(coordinator.mDefaultDisplayRotationChangedCallback);
+    }
+
+    private DisplayContent createSecondaryDisplayContent(int displayType,
+            @NonNull DeviceStateController deviceStateController) {
+        final DisplayInfo secondaryDisplayInfo = new DisplayInfo();
+        secondaryDisplayInfo.copyFrom(mDisplayInfo);
+        secondaryDisplayInfo.type = displayType;
+
+        return new TestDisplayContent.Builder(mAtm, secondaryDisplayInfo)
+                .setDeviceStateController(deviceStateController)
+                .build();
+    }
+
+    private static void assertRotationsAreCorrectlyReversed(@Surface.Rotation int rotation1,
+            @Surface.Rotation int rotation2) {
+        if (rotation1 == ROTATION_0) {
+            assertEquals(rotation1, rotation2);
+        } else if (rotation1 == ROTATION_180) {
+            assertEquals(rotation1, rotation2);
+        } else if (rotation1 == ROTATION_90) {
+            assertEquals(ROTATION_270, rotation2);
+        } else if (rotation1 == ROTATION_270) {
+            assertEquals(ROTATION_90, rotation2);
+        } else {
+            throw new IllegalArgumentException("Unknown rotation: " + rotation1 + ", " + rotation2);
+        }
+    }
+
+    @Test
     public void testRemoteRotation() {
         final DisplayContent dc = mDisplayContent;
         final DisplayRotation dr = dc.getDisplayRotation();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCoordinatorTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCoordinatorTests.java
new file mode 100644
index 0000000..4557df0
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCoordinatorTests.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.junit.Assert.assertEquals;
+
+import android.annotation.NonNull;
+import android.platform.test.annotations.Presubmit;
+import android.view.Surface;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+/**
+ * Test class for {@link DisplayRotationCoordinator}
+ *
+ * Build/Install/Run:
+ *  atest DisplayRotationCoordinatorTests
+ */
+@SmallTest
+@Presubmit
+public class DisplayRotationCoordinatorTests {
+
+    @NonNull
+    private final DisplayRotationCoordinator mCoordinator = new DisplayRotationCoordinator();
+
+    @Test
+    public void testDefaultDisplayRotationChangedWhenNoCallbackRegistered() {
+        // Does not cause NPE
+        mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90);
+    }
+
+    @Test (expected = UnsupportedOperationException.class)
+    public void testSecondRegistrationWithoutRemovingFirst() {
+        Runnable callback1 = mock(Runnable.class);
+        Runnable callback2 = mock(Runnable.class);
+        mCoordinator.setDefaultDisplayRotationChangedCallback(callback1);
+        mCoordinator.setDefaultDisplayRotationChangedCallback(callback2);
+        assertEquals(callback1, mCoordinator.mDefaultDisplayRotationChangedCallback);
+    }
+
+    @Test
+    public void testSecondRegistrationAfterRemovingFirst() {
+        Runnable callback1 = mock(Runnable.class);
+        mCoordinator.setDefaultDisplayRotationChangedCallback(callback1);
+        mCoordinator.removeDefaultDisplayRotationChangedCallback();
+
+        Runnable callback2 = mock(Runnable.class);
+        mCoordinator.setDefaultDisplayRotationChangedCallback(callback2);
+
+        mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90);
+        verify(callback2).run();
+        verify(callback1, never()).run();
+    }
+
+    @Test
+    public void testRegisterThenDefaultDisplayRotationChanged() {
+        Runnable callback = mock(Runnable.class);
+        mCoordinator.setDefaultDisplayRotationChangedCallback(callback);
+        assertEquals(Surface.ROTATION_0, mCoordinator.getDefaultDisplayCurrentRotation());
+        verify(callback, never()).run();
+
+        mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90);
+        verify(callback).run();
+        assertEquals(Surface.ROTATION_90, mCoordinator.getDefaultDisplayCurrentRotation());
+    }
+
+    @Test
+    public void testDefaultDisplayRotationChangedThenRegister() {
+        mCoordinator.onDefaultDisplayRotationChanged(Surface.ROTATION_90);
+        Runnable callback = mock(Runnable.class);
+        mCoordinator.setDefaultDisplayRotationChangedCallback(callback);
+        verify(callback).run();
+        assertEquals(Surface.ROTATION_90, mCoordinator.getDefaultDisplayCurrentRotation());
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
index ed2b0a3..21e8ec4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java
@@ -1135,7 +1135,7 @@
             mDeviceStateController = mock(DeviceStateController.class);
             mTarget = new DisplayRotation(sMockWm, mMockDisplayContent, mMockDisplayAddress,
                     mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object(),
-                    mDeviceStateController) {
+                    mDeviceStateController, mock(DisplayRotationCoordinator.class)) {
                 @Override
                 DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy(
                         WindowManagerService service, DisplayContent displayContent) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
index 4eaae9f..d1a41ae 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DualDisplayAreaGroupPolicyTest.java
@@ -552,7 +552,7 @@
         /** Please use the {@link Builder} to create. */
         DualDisplayContent(RootWindowContainer rootWindowContainer,
                 Display display) {
-            super(rootWindowContainer, display);
+            super(rootWindowContainer, display, mock(DeviceStateController.class));
 
             mFirstRoot = getGroupRoot(FEATURE_FIRST_ROOT);
             mSecondRoot = getGroupRoot(FEATURE_SECOND_ROOT);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index bcaf886..0037e57 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -24,7 +24,9 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import android.hardware.display.DisplayManager;
@@ -289,6 +291,14 @@
         assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
         assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+
+        // If there will be display size change when switching from preferred mode to default mode,
+        // then keep the current preferred mode during animating.
+        mDisplayInfo = spy(mDisplayInfo);
+        final Mode defaultMode = new Mode(4321 /* width */, 1234 /* height */, LOW_REFRESH_RATE);
+        doReturn(defaultMode).when(mDisplayInfo).getDefaultMode();
+        mPolicy = new RefreshRatePolicy(mWm, mDisplayInfo, mDenylist);
+        assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 7ad5442..6db0f27 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -1895,6 +1895,43 @@
     }
 
     @Test
+    @EnableCompatChanges({ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION})
+    public void testOverrideRespectRequestedOrientationIsEnabled_orientationIsRespected() {
+        // Set up a display in landscape
+        setUpDisplaySizeWithApp(2800, 1400);
+
+        final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */ false,
+                RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
+        activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+
+        // Display should be rotated.
+        assertEquals(SCREEN_ORIENTATION_PORTRAIT, activity.mDisplayContent.getOrientation());
+
+        // No size compat mode
+        assertFalse(activity.inSizeCompatMode());
+    }
+
+    @Test
+    @EnableCompatChanges({ActivityInfo.OVERRIDE_RESPECT_REQUESTED_ORIENTATION})
+    public void testOverrideRespectRequestedOrientationIsEnabled_multiWindow_orientationIgnored() {
+        // Set up a display in landscape
+        setUpDisplaySizeWithApp(2800, 1400);
+
+        final ActivityRecord activity = buildActivityRecord(/* supportsSizeChanges= */ false,
+                RESIZE_MODE_UNRESIZEABLE, SCREEN_ORIENTATION_PORTRAIT);
+        TaskFragment taskFragment = activity.getTaskFragment();
+        spyOn(taskFragment);
+        activity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        doReturn(WINDOWING_MODE_MULTI_WINDOW).when(taskFragment).getWindowingMode();
+
+        // Display should not be rotated.
+        assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, activity.mDisplayContent.getOrientation());
+
+        // No size compat mode
+        assertFalse(activity.inSizeCompatMode());
+    }
+
+    @Test
     public void testSplitAspectRatioForUnresizableLandscapeApps() {
         // Set up a display in portrait and ignoring orientation request.
         int screenWidth = 1400;
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 5099869..dab842c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -45,11 +45,13 @@
 import static org.mockito.Mockito.clearInvocations;
 
 import android.content.res.Configuration;
+import android.graphics.Color;
 import android.graphics.Rect;
 import android.os.Binder;
 import android.platform.test.annotations.Presubmit;
 import android.view.SurfaceControl;
 import android.window.ITaskFragmentOrganizer;
+import android.window.TaskFragmentAnimationParams;
 import android.window.TaskFragmentInfo;
 import android.window.TaskFragmentOrganizer;
 
@@ -299,35 +301,44 @@
 
     @Test
     public void testEmbeddedTaskFragmentEnterPip_singleActivity_resetOrganizerOverrideConfig() {
-        final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm)
-                .setOrganizer(mOrganizer)
-                .setFragmentToken(new Binder())
-                .setCreateParentTask()
-                .createActivityCount(1)
+        final Task task = createTask(mDisplayContent);
+        final TaskFragment taskFragment0 = createTaskFragmentWithEmbeddedActivity(task, mOrganizer);
+        final TaskFragment taskFragment1 = new TaskFragmentBuilder(mAtm)
+                .setParentTask(task)
                 .build();
-        final Task task = taskFragment.getTask();
-        final ActivityRecord activity = taskFragment.getTopMostActivity();
+        final ActivityRecord activity = taskFragment0.getTopMostActivity();
         final Rect taskFragmentBounds = new Rect(0, 0, 300, 1000);
         task.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
-        taskFragment.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
-        taskFragment.setBounds(taskFragmentBounds);
+        taskFragment0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+        taskFragment0.setBounds(taskFragmentBounds);
+        taskFragment0.setAdjacentTaskFragment(taskFragment1);
+        taskFragment0.setCompanionTaskFragment(taskFragment1);
+        taskFragment0.setAnimationParams(new TaskFragmentAnimationParams.Builder()
+                .setAnimationBackgroundColor(Color.GREEN)
+                .build());
 
         assertEquals(taskFragmentBounds, activity.getBounds());
         assertEquals(WINDOWING_MODE_MULTI_WINDOW, activity.getWindowingMode());
+        assertEquals(taskFragment1, taskFragment0.getAdjacentTaskFragment());
+        assertEquals(taskFragment1, taskFragment0.getCompanionTaskFragment());
+        assertNotEquals(TaskFragmentAnimationParams.DEFAULT, taskFragment0.getAnimationParams());
 
         // Move activity to pinned root task.
         mRootWindowContainer.moveActivityToPinnedRootTask(activity,
                 null /* launchIntoPipHostActivity */, "test");
 
         // Ensure taskFragment requested config is reset.
-        assertEquals(taskFragment, activity.getOrganizedTaskFragment());
+        assertEquals(taskFragment0, activity.getOrganizedTaskFragment());
         assertEquals(task, activity.getTask());
         assertTrue(task.inPinnedWindowingMode());
-        assertTrue(taskFragment.inPinnedWindowingMode());
+        assertTrue(taskFragment0.inPinnedWindowingMode());
         final Rect taskBounds = task.getBounds();
-        assertEquals(taskBounds, taskFragment.getBounds());
+        assertEquals(taskBounds, taskFragment0.getBounds());
         assertEquals(taskBounds, activity.getBounds());
-        assertEquals(Configuration.EMPTY, taskFragment.getRequestedOverrideConfiguration());
+        assertEquals(Configuration.EMPTY, taskFragment0.getRequestedOverrideConfiguration());
+        assertNull(taskFragment0.getAdjacentTaskFragment());
+        assertNull(taskFragment0.getCompanionTaskFragment());
+        assertEquals(TaskFragmentAnimationParams.DEFAULT, taskFragment0.getAnimationParams());
         // Because the whole Task is entering PiP, no need to record for future reparent.
         assertNull(activity.mLastTaskFragmentOrganizerBeforePip);
     }
@@ -335,18 +346,8 @@
     @Test
     public void testEmbeddedTaskFragmentEnterPip_multiActivities_notifyOrganizer() {
         final Task task = createTask(mDisplayContent);
-        final TaskFragment taskFragment0 = new TaskFragmentBuilder(mAtm)
-                .setParentTask(task)
-                .setOrganizer(mOrganizer)
-                .setFragmentToken(new Binder())
-                .createActivityCount(1)
-                .build();
-        final TaskFragment taskFragment1 = new TaskFragmentBuilder(mAtm)
-                .setParentTask(task)
-                .setOrganizer(mOrganizer)
-                .setFragmentToken(new Binder())
-                .createActivityCount(1)
-                .build();
+        final TaskFragment taskFragment0 = createTaskFragmentWithEmbeddedActivity(task, mOrganizer);
+        final TaskFragment taskFragment1 = createTaskFragmentWithEmbeddedActivity(task, mOrganizer);
         final ActivityRecord activity0 = taskFragment0.getTopMostActivity();
         final ActivityRecord activity1 = taskFragment1.getTopMostActivity();
         activity0.setVisibility(true /* visible */, false /* deferHidingClient */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
index 83be4f0..fec079b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
@@ -27,8 +27,10 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -51,8 +53,9 @@
     public static final int DEFAULT_LOGICAL_DISPLAY_DENSITY = 300;
 
     /** Please use the {@link Builder} to create, visible for use in test builder overrides only. */
-    TestDisplayContent(RootWindowContainer rootWindowContainer, Display display) {
-        super(display, rootWindowContainer);
+    TestDisplayContent(RootWindowContainer rootWindowContainer, Display display,
+            @NonNull DeviceStateController deviceStateController) {
+        super(display, rootWindowContainer, deviceStateController);
         // Normally this comes from display-properties as exposed by WM. Without that, just
         // hard-code to FULLSCREEN for tests.
         setWindowingMode(WINDOWING_MODE_FULLSCREEN);
@@ -97,6 +100,8 @@
         private int mStatusBarHeight = 0;
         private SettingsEntry mOverrideSettings;
         private DisplayMetrics mDisplayMetrics;
+        @NonNull
+        private DeviceStateController mDeviceStateController = mock(DeviceStateController.class);
         @Mock
         Context mMockContext;
         @Mock
@@ -198,8 +203,13 @@
                         com.android.internal.R.dimen.default_minimal_size_resizable_task);
             return this;
         }
+        Builder setDeviceStateController(@NonNull DeviceStateController deviceStateController) {
+            mDeviceStateController = deviceStateController;
+            return this;
+        }
         TestDisplayContent createInternal(Display display) {
-            return new TestDisplayContent(mService.mRootWindowContainer, display);
+            return new TestDisplayContent(mService.mRootWindowContainer, display,
+                    mDeviceStateController);
         }
         TestDisplayContent build() {
             SystemServicesTestRule.checkHoldsLock(mService.mGlobalLock);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index e5efe05..3e8d259 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -79,6 +79,7 @@
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
 import android.hardware.display.DisplayManager;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -750,11 +751,11 @@
     }
 
     /**
-     * Creates a {@link TaskFragment} with {@link ActivityRecord} and attach it to the
+     * Creates a {@link TaskFragment} with {@link ActivityRecord}, and attaches it to the
      * {@code parentTask}.
      *
-     * @param parentTask the {@link Task} this TaskFragment is going to be attached
-     * @return the created TaskFragment
+     * @param parentTask the {@link Task} this {@link TaskFragment} is going to be attached.
+     * @return the created {@link TaskFragment}
      */
     static TaskFragment createTaskFragmentWithActivity(@NonNull Task parentTask) {
         return new TaskFragmentBuilder(parentTask.mAtmService)
@@ -763,13 +764,27 @@
                 .build();
     }
 
+    /**
+     * Creates an embedded {@link TaskFragment} organized by {@code organizer} with
+     * {@link ActivityRecord}, and attaches it to the {@code parentTask}.
+     *
+     * @param parentTask the {@link Task} this {@link TaskFragment} is going to be attached.
+     * @param organizer  the {@link TaskFragmentOrganizer} this {@link TaskFragment} is going to be
+     *                   organized by.
+     * @return the created {@link TaskFragment}
+     */
     static TaskFragment createTaskFragmentWithEmbeddedActivity(@NonNull Task parentTask,
-            TaskFragmentOrganizer organizer) {
-        return new TaskFragmentBuilder(parentTask.mAtmService)
+            @NonNull TaskFragmentOrganizer organizer) {
+        final IBinder fragmentToken = new Binder();
+        final TaskFragment taskFragment = new TaskFragmentBuilder(parentTask.mAtmService)
                 .setParentTask(parentTask)
                 .createActivityCount(1)
                 .setOrganizer(organizer)
+                .setFragmentToken(fragmentToken)
                 .build();
+        parentTask.mAtmService.mWindowOrganizerController.mLaunchTaskFragments
+                .put(fragmentToken, taskFragment);
+        return taskFragment;
     }
 
     /** Creates a {@link DisplayContent} that supports IME and adds it to the system. */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
index 64ed453..73b5afe 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
@@ -184,12 +184,12 @@
     }
 
     open fun cujCompleted() {
-        entireScreenCovered()
-        statusBarLayerIsVisibleAtStartAndEnd()
-        statusBarLayerPositionAtStartAndEnd()
-        statusBarWindowIsAlwaysVisible()
-        visibleLayersShownMoreThanOneConsecutiveEntry()
-        visibleWindowsShownMoreThanOneConsecutiveEntry()
+        runAndIgnoreAssumptionViolation { entireScreenCovered() }
+        runAndIgnoreAssumptionViolation { statusBarLayerIsVisibleAtStartAndEnd() }
+        runAndIgnoreAssumptionViolation { statusBarLayerPositionAtStartAndEnd() }
+        runAndIgnoreAssumptionViolation { statusBarWindowIsAlwaysVisible() }
+        runAndIgnoreAssumptionViolation { visibleLayersShownMoreThanOneConsecutiveEntry() }
+        runAndIgnoreAssumptionViolation { visibleWindowsShownMoreThanOneConsecutiveEntry() }
         runAndIgnoreAssumptionViolation { taskBarLayerIsVisibleAtStartAndEnd() }
         runAndIgnoreAssumptionViolation { taskBarWindowIsAlwaysVisible() }
         runAndIgnoreAssumptionViolation { navBarLayerIsVisibleAtStartAndEnd() }
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplitTest.kt
similarity index 63%
rename from tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplit.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplitTest.kt
index ea67729..7f2e057f 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingPlaceholderSplitTest.kt
@@ -33,13 +33,13 @@
  * Test opening an activity that will launch another activity as ActivityEmbedding placeholder in
  * split.
  *
- * To run this test: `atest FlickerTests:OpenActivityEmbeddingPlaceholderSplit`
+ * To run this test: `atest FlickerTests:OpenActivityEmbeddingPlaceholderSplitTest`
  */
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class OpenActivityEmbeddingPlaceholderSplit(flicker: FlickerTest) :
+class OpenActivityEmbeddingPlaceholderSplitTest(flicker: FlickerTest) :
     ActivityEmbeddingTestBase(flicker) {
 
     /** {@inheritDoc} */
@@ -55,9 +55,21 @@
         }
     }
 
+    /** Main activity should become invisible after launching the placeholder primary activity. */
     @Presubmit
     @Test
-    fun mainActivityBecomesInvisible() {
+    fun mainActivityWindowBecomesInvisible() {
+        flicker.assertWm {
+            isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                .then()
+                .isAppWindowInvisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+        }
+    }
+
+    /** Main activity should become invisible after launching the placeholder primary activity. */
+    @Presubmit
+    @Test
+    fun mainActivityLayerBecomesInvisible() {
         flicker.assertLayers {
             isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
                 .then()
@@ -65,76 +77,41 @@
         }
     }
 
+    /**
+     * Placeholder primary and secondary should become visible after launch. The windows are not
+     * necessarily to become visible at the same time.
+     */
     @Presubmit
     @Test
-    fun placeholderSplitBecomesVisible() {
-        flicker.assertLayers {
-            isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+    fun placeholderSplitWindowsBecomeVisible() {
+        flicker.assertWm {
+            notContains(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
                 .then()
-                .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+                .isAppWindowInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+                .then()
+                .isAppWindowVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
         }
-        flicker.assertLayers {
-            isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+        flicker.assertWm {
+            notContains(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
                 .then()
-                .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+                .isAppWindowInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+                .then()
+                .isAppWindowVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
         }
     }
 
-    /** {@inheritDoc} */
-    @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered()
-
-    /** {@inheritDoc} */
+    /** Placeholder primary and secondary should become visible together after launch. */
     @Presubmit
     @Test
-    override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun statusBarLayerIsVisibleAtStartAndEnd() =
-        super.statusBarLayerIsVisibleAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() =
-        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
-
-    /** {@inheritDoc} */
-    @Presubmit
-    @Test
-    override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
-        super.visibleLayersShownMoreThanOneConsecutiveEntry()
+    fun placeholderSplitLayersBecomeVisible() {
+        flicker.assertLayers {
+            isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+            isInvisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+                .then()
+                .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_PRIMARY_COMPONENT)
+                .isVisible(ActivityEmbeddingAppHelper.PLACEHOLDER_SECONDARY_COMPONENT)
+        }
+    }
 
     companion object {
         /**
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingSecondaryToSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingSecondaryToSplitTest.kt
new file mode 100644
index 0000000..5fff15c
--- /dev/null
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/OpenActivityEmbeddingSecondaryToSplitTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.activityembedding
+
+import android.platform.test.annotations.Presubmit
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.FlickerBuilder
+import com.android.server.wm.flicker.FlickerTest
+import com.android.server.wm.flicker.FlickerTestFactory
+import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
+import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
+import com.android.server.wm.traces.common.ComponentNameMatcher
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test opening a secondary activity that will split with the main activity.
+ *
+ * To run this test: `atest FlickerTests:OpenActivityEmbeddingSecondaryToSplitTest`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenActivityEmbeddingSecondaryToSplitTest(flicker: FlickerTest) :
+    ActivityEmbeddingTestBase(flicker) {
+
+    /** {@inheritDoc} */
+    override val transition: FlickerBuilder.() -> Unit = {
+        setup {
+            tapl.setExpectedRotationCheckEnabled(false)
+            testApp.launchViaIntent(wmHelper)
+        }
+        transitions { testApp.launchSecondaryActivity(wmHelper) }
+        teardown {
+            tapl.goHome()
+            testApp.exit(wmHelper)
+        }
+    }
+
+    /** Main activity should remain visible when enter split from fullscreen. */
+    @Presubmit
+    @Test
+    fun mainActivityWindowIsAlwaysVisible() {
+        flicker.assertWm {
+            isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+        }
+    }
+
+    /**
+     * Main activity surface is animated from fullscreen to ActivityEmbedding split.
+     * During the transition, there is a period of time that it is covered by a snapshot of itself.
+     */
+    @Presubmit
+    @Test
+    fun mainActivityLayerIsAlwaysVisible() {
+        flicker.assertLayers {
+            isVisible(
+                ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT.or(
+                    ComponentNameMatcher.TRANSITION_SNAPSHOT
+                )
+            )
+        }
+        flicker.assertLayersEnd {
+            isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT)
+                .isInvisible(ComponentNameMatcher.TRANSITION_SNAPSHOT)
+        }
+    }
+
+    /** Secondary activity should become visible after launching into split. */
+    @Presubmit
+    @Test
+    fun secondaryActivityWindowBecomesVisible() {
+        flicker.assertWm {
+            notContains(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .then()
+                .isAppWindowInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .then()
+                .isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+    }
+
+    /** Secondary activity should become visible after launching into split. */
+    @Presubmit
+    @Test
+    fun secondaryActivityLayerBecomesVisible() {
+        flicker.assertLayers {
+            isInvisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+                .then()
+                .isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT)
+        }
+    }
+
+    companion object {
+        /**
+         * Creates the test configurations.
+         *
+         * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and
+         * navigation modes.
+         */
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTest> {
+            return FlickerTestFactory.nonRotationTests()
+        }
+    }
+}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
index 368cc56..a2db587 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt
@@ -39,6 +39,25 @@
 ) : StandardAppHelper(instr, launcherName, component) {
 
     /**
+     * Clicks the button to launch the secondary activity, which should split with the main activity
+     * based on the split pair rule.
+     */
+    fun launchSecondaryActivity(wmHelper: WindowManagerStateHelper) {
+        val launchButton =
+            uiDevice.wait(
+                Until.findObject(By.res(getPackage(), "launch_secondary_activity_button")),
+                FIND_TIMEOUT
+            )
+        require(launchButton != null) { "Can't find launch secondary activity button on screen." }
+        launchButton.click()
+        wmHelper
+            .StateSyncBuilder()
+            .withActivityState(SECONDARY_ACTIVITY_COMPONENT, STATE_RESUMED)
+            .withActivityState(MAIN_ACTIVITY_COMPONENT, STATE_RESUMED)
+            .waitForAndVerify()
+    }
+
+    /**
      * Clicks the button to launch the placeholder primary activity, which should launch the
      * placeholder secondary activity based on the placeholder rule.
      */
@@ -63,6 +82,9 @@
         val MAIN_ACTIVITY_COMPONENT =
             ActivityOptions.ActivityEmbedding.MainActivity.COMPONENT.toFlickerComponent()
 
+        val SECONDARY_ACTIVITY_COMPONENT =
+            ActivityOptions.ActivityEmbedding.SecondaryActivity.COMPONENT.toFlickerComponent()
+
         val PLACEHOLDER_PRIMARY_COMPONENT =
             ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT
                 .toFlickerComponent()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
index b40720b..4890bba 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnGoHomeTest.kt
@@ -100,6 +100,7 @@
         flicker.assertLayers { this.isVisible(testApp).then().isInvisible(testApp) }
     }
 
+    @Presubmit
     @Test
     @IwTest(focusArea = "ime")
     override fun cujCompleted() {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
index f6838f4..63f7200 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToAppOnPressBackTest.kt
@@ -102,6 +102,7 @@
         flicker.assertWm { this.isAppWindowOnTop(testApp) }
     }
 
+    @Presubmit
     @Test
     @IwTest(focusArea = "ime")
     override fun cujCompleted() {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
index a744cd7..0b7b165 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
@@ -79,6 +79,7 @@
         super.visibleLayersShownMoreThanOneConsecutiveEntry()
     }
 
+    @Presubmit
     @Test
     @IwTest(focusArea = "ime")
     override fun cujCompleted() {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
index 36747cc7..851651e 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/ShowImeWhenFocusingOnInputFieldTest.kt
@@ -50,6 +50,7 @@
         }
     }
 
+    @Presubmit
     @Test
     @IwTest(focusArea = "ime")
     override fun cujCompleted() {
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index cd47f60..5361d73f 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -179,6 +179,13 @@
             </intent-filter>
         </activity>
         <activity
+            android:name=".ActivityEmbeddingSecondaryActivity"
+            android:label="ActivityEmbedding Secondary"
+            android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding"
+            android:theme="@style/CutoutShortEdges"
+            android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
+            android:exported="false"/>
+        <activity
             android:name=".ActivityEmbeddingPlaceholderPrimaryActivity"
             android:label="ActivityEmbedding Placeholder Primary"
             android:taskAffinity="com.android.server.wm.flicker.testapp.ActivityEmbedding"
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
index 19c81a8..d78b9a8 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml
@@ -22,6 +22,14 @@
     android:background="@android:color/holo_orange_light">
 
     <Button
+        android:id="@+id/launch_secondary_activity_button"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:layout_centerHorizontal="true"
+        android:onClick="launchSecondaryActivity"
+        android:text="Launch Secondary Activity" />
+
+    <Button
         android:id="@+id/launch_placeholder_split_button"
         android:layout_width="wrap_content"
         android:layout_height="48dp"
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
index 04a590d..6a7a2cc 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java
@@ -25,6 +25,7 @@
 
 import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
 import androidx.window.extensions.embedding.EmbeddingRule;
+import androidx.window.extensions.embedding.SplitPairRule;
 import androidx.window.extensions.embedding.SplitPlaceholderRule;
 
 import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper;
@@ -40,20 +41,23 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_embedding_main_layout);
+    }
 
-        initializeSplitRules();
+    /** R.id.launch_secondary_activity_button onClick */
+    public void launchSecondaryActivity(View view) {
+        initializeSplitRules(createSplitPairRules());
+        startActivity(new Intent().setComponent(
+                ActivityOptions.ActivityEmbedding.SecondaryActivity.COMPONENT));
     }
 
     /** R.id.launch_placeholder_split_button onClick */
     public void launchPlaceholderSplit(View view) {
-        startActivity(
-                new Intent().setComponent(
-                        ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT
-                )
-        );
+        initializeSplitRules(createSplitPlaceholderRules());
+        startActivity(new Intent().setComponent(
+                ActivityOptions.ActivityEmbedding.PlaceholderPrimaryActivity.COMPONENT));
     }
 
-    private void initializeSplitRules() {
+    private void initializeSplitRules(Set<EmbeddingRule> rules) {
         ActivityEmbeddingComponent embeddingComponent =
                 ActivityEmbeddingAppHelper.getActivityEmbeddingComponent();
         if (embeddingComponent == null) {
@@ -62,14 +66,28 @@
             finish();
             return;
         }
-
-        embeddingComponent.setEmbeddingRules(getSplitRules());
+        embeddingComponent.setEmbeddingRules(rules);
     }
 
-    private Set<EmbeddingRule> getSplitRules() {
+    private Set<EmbeddingRule> createSplitPairRules() {
         final Set<EmbeddingRule> rules = new ArraySet<>();
+        final SplitPairRule rule = new SplitPairRule.Builder(
+                activitiesPair -> activitiesPair.first instanceof ActivityEmbeddingMainActivity
+                        && activitiesPair.second instanceof ActivityEmbeddingSecondaryActivity,
+                activityIntentPair ->
+                        activityIntentPair.first instanceof ActivityEmbeddingMainActivity
+                                && activityIntentPair.second.getComponent().equals(ActivityOptions
+                                .ActivityEmbedding.SecondaryActivity.COMPONENT),
+                windowMetrics -> true)
+                .setSplitRatio(DEFAULT_SPLIT_RATIO)
+                .build();
+        rules.add(rule);
+        return rules;
+    }
 
-        final SplitPlaceholderRule placeholderRule = new SplitPlaceholderRule.Builder(
+    private Set<EmbeddingRule> createSplitPlaceholderRules() {
+        final Set<EmbeddingRule> rules = new ArraySet<>();
+        final SplitPlaceholderRule rule = new SplitPlaceholderRule.Builder(
                 new Intent().setComponent(
                         ActivityOptions.ActivityEmbedding.PlaceholderSecondaryActivity.COMPONENT),
                 activity -> activity instanceof ActivityEmbeddingPlaceholderPrimaryActivity,
@@ -78,7 +96,7 @@
                 windowMetrics -> true)
                 .setSplitRatio(DEFAULT_SPLIT_RATIO)
                 .build();
-        rules.add(placeholderRule);
+        rules.add(rule);
         return rules;
     }
 }
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
new file mode 100644
index 0000000..00f4c25
--- /dev/null
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.testapp;
+
+import android.graphics.Color;
+
+/**
+ * Activity to be used as the secondary activity to split with
+ * {@link ActivityEmbeddingMainActivity}.
+ */
+public class ActivityEmbeddingSecondaryActivity extends ActivityEmbeddingBaseActivity {
+    @Override
+    int getBackgroundColor() {
+        return Color.YELLOW;
+    }
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index 00684de..b61bc0c 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -87,6 +87,12 @@
                     FLICKER_APP_PACKAGE + ".ActivityEmbeddingMainActivity");
         }
 
+        public static class SecondaryActivity {
+            public static final String LABEL = "ActivityEmbeddingSecondaryActivity";
+            public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
+                    FLICKER_APP_PACKAGE + ".ActivityEmbeddingSecondaryActivity");
+        }
+
         public static class PlaceholderPrimaryActivity {
             public static final String LABEL = "ActivityEmbeddingPlaceholderPrimaryActivity";
             public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/OWNERS b/wifi/java/src/android/net/wifi/sharedconnectivity/OWNERS
new file mode 100644
index 0000000..2a4acc1
--- /dev/null
+++ b/wifi/java/src/android/net/wifi/sharedconnectivity/OWNERS
@@ -0,0 +1,4 @@
+# Bug component: 1216021
+
+asapperstein@google.com
+etancohen@google.com
diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/OWNERS b/wifi/tests/src/android/net/wifi/sharedconnectivity/OWNERS
new file mode 100644
index 0000000..8873d07
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/OWNERS
@@ -0,0 +1 @@
+file:/wifi/java/src/android/net/wifi/sharedconnectivity/OWNERS