Merge "Continue to remove code around legacy captions" into main
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 412f2b7..11da20a 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -284,6 +284,8 @@
         "100-framework-minus-apex.ravenwood",
         "200-kxml2-android",
 
+        "ravenwood-runtime-common-ravenwood",
+
         "android.test.mock.ravenwood",
         "ravenwood-helper-runtime",
         "hoststubgen-helper-runtime.ravenwood",
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java b/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
index 515ddc8..459c286 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
@@ -19,13 +19,16 @@
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.perftests.utils.ShellHelper;
+import android.util.Log;
 
 import java.util.ArrayList;
 
 // Based on //platform/frameworks/base/apct-tests/perftests/utils/BenchmarkState.java
 public class BenchmarkRunner {
-
+    private static final String TAG = BenchmarkRunner.class.getSimpleName();
     private static final long COOL_OFF_PERIOD_MS = 1000;
+    private static final int CPU_IDLE_TIMEOUT_MS = 60 * 1000;
+    private static final int CPU_IDLE_THRESHOLD_PERCENTAGE = 90;
 
     private static final int NUM_ITERATIONS = 4;
 
@@ -80,7 +83,7 @@
 
     private void prepareForNextRun() {
         SystemClock.sleep(COOL_OFF_PERIOD_MS);
-        ShellHelper.runShellCommand("am wait-for-broadcast-idle --flush-broadcast-loopers");
+        waitCoolDownPeriod();
         mStartTimeNs = System.nanoTime();
         mPausedDurationNs = 0;
     }
@@ -102,7 +105,7 @@
      * to avoid unnecessary waiting.
      */
     public void resumeTiming() {
-        ShellHelper.runShellCommand("am wait-for-broadcast-idle --flush-broadcast-loopers");
+        waitCoolDownPeriod();
         resumeTimer();
     }
 
@@ -162,4 +165,56 @@
         }
         return null;
     }
+
+    /** Waits for the broadcast queue and the CPU cores to be idle. */
+    public void waitCoolDownPeriod() {
+        waitForBroadcastIdle();
+        waitForCpuIdle();
+    }
+
+    private void waitForBroadcastIdle() {
+        Log.d(TAG, "starting to waitForBroadcastIdle");
+        final long startedAt = System.currentTimeMillis();
+        ShellHelper.runShellCommand("am wait-for-broadcast-idle --flush-broadcast-loopers");
+        final long elapsed = System.currentTimeMillis() - startedAt;
+        Log.d(TAG, "waitForBroadcastIdle is complete in " + elapsed + " ms");
+    }
+    private void waitForCpuIdle() {
+        Log.d(TAG, "starting to waitForCpuIdle");
+        final long startedAt = System.currentTimeMillis();
+        while (true) {
+            final int idleCpuPercentage = getIdleCpuPercentage();
+            final long elapsed = System.currentTimeMillis() - startedAt;
+            Log.d(TAG, "waitForCpuIdle " + idleCpuPercentage + "% (" + elapsed + "ms elapsed)");
+            if (idleCpuPercentage >= CPU_IDLE_THRESHOLD_PERCENTAGE) {
+                Log.d(TAG, "waitForCpuIdle is complete in " + elapsed + " ms");
+                return;
+            }
+            if (elapsed >= CPU_IDLE_TIMEOUT_MS) {
+                Log.e(TAG, "Ending waitForCpuIdle because it didn't finish in "
+                        + CPU_IDLE_TIMEOUT_MS + " ms");
+                return;
+            }
+            SystemClock.sleep(1000);
+        }
+    }
+
+    private int getIdleCpuPercentage() {
+        String output = ShellHelper.runShellCommand("top -m 1 -n 1");
+        String[] tokens = output.split("\\s+");
+        float totalCpu = -1;
+        float idleCpu = -1;
+        for (String token : tokens) {
+            if (token.contains("%cpu")) {
+                totalCpu = Float.parseFloat(token.split("%")[0]);
+            } else if (token.contains("%idle")) {
+                idleCpu = Float.parseFloat(token.split("%")[0]);
+            }
+        }
+        if (totalCpu < 0 || idleCpu < 0) {
+            Log.e(TAG, "Could not get idle cpu percentage, output=" + output);
+            return -1;
+        }
+        return (int) (100 * idleCpu / totalCpu);
+    }
 }
\ No newline at end of file
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
index 762e2af..98ab0c2 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
@@ -188,21 +188,6 @@
         }
     }
 
-    /** Tests creating a new user, with wait times between iterations. */
-    @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
-    public void createUser_realistic() throws RemoteException {
-        while (mRunner.keepRunning()) {
-            Log.i(TAG, "Starting timer");
-            final int userId = createUserNoFlags();
-
-            mRunner.pauseTiming();
-            Log.i(TAG, "Stopping timer");
-            removeUser(userId);
-            waitCoolDownPeriod();
-            mRunner.resumeTimingForNextIteration();
-        }
-    }
-
     /** Tests creating and starting a new user. */
     @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
     public void createAndStartUser() throws RemoteException {
@@ -239,7 +224,6 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
-            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -254,7 +238,6 @@
             mRunner.pauseTiming();
             final int userId = createUserNoFlags();
 
-            waitForBroadcastIdle();
             runThenWaitForBroadcasts(userId, () -> {
                 mRunner.resumeTiming();
                 Log.i(TAG, "Starting timer");
@@ -309,9 +292,6 @@
 
             preStartUser(userId, numberOfIterationsToSkip);
 
-            waitForBroadcastIdle();
-            waitCoolDownPeriod();
-
             runThenWaitForBroadcasts(userId, () -> {
                 mRunner.resumeTiming();
                 Log.i(TAG, "Starting timer");
@@ -353,9 +333,6 @@
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
 
-            waitForBroadcastIdle();
-            waitCoolDownPeriod();
-
             runThenWaitForBroadcasts(userId, () -> {
                 mRunner.resumeTiming();
                 Log.i(TAG, "Starting timer");
@@ -420,7 +397,6 @@
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
 
-            waitCoolDownPeriod();
             mRunner.resumeTiming();
             Log.i(TAG, "Starting timer");
 
@@ -454,7 +430,6 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
-            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -466,6 +441,7 @@
             mRunner.pauseTiming();
             final int startUser = mAm.getCurrentUser();
             final int userId = createUserNoFlags();
+
             mRunner.resumeTiming();
             Log.i(TAG, "Starting timer");
 
@@ -479,27 +455,6 @@
         }
     }
 
-    /** Tests switching to an uninitialized user with wait times between iterations. */
-    @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
-    public void switchUser_realistic() throws Exception {
-        while (mRunner.keepRunning()) {
-            mRunner.pauseTiming();
-            final int startUser = ActivityManager.getCurrentUser();
-            final int userId = createUserNoFlags();
-            waitCoolDownPeriod();
-            Log.d(TAG, "Starting timer");
-            mRunner.resumeTiming();
-
-            switchUser(userId);
-
-            mRunner.pauseTiming();
-            Log.d(TAG, "Stopping timer");
-            switchUserNoCheck(startUser);
-            removeUser(userId);
-            mRunner.resumeTimingForNextIteration();
-        }
-    }
-
     /** Tests switching to a previously-started, but no-longer-running, user. */
     @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
     public void switchUser_stopped() throws RemoteException {
@@ -507,6 +462,7 @@
             mRunner.pauseTiming();
             final int startUser = mAm.getCurrentUser();
             final int testUser = initializeNewUserAndSwitchBack(/* stopNewUser */ true);
+
             mRunner.resumeTiming();
             Log.i(TAG, "Starting timer");
 
@@ -536,7 +492,6 @@
 
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
-            waitCoolDownPeriod();
             Log.d(TAG, "Starting timer");
             mRunner.resumeTiming();
 
@@ -562,7 +517,6 @@
                 /* useStaticWallpaper */true);
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
-            waitCoolDownPeriod();
             Log.d(TAG, "Starting timer");
             mRunner.resumeTiming();
 
@@ -606,7 +560,6 @@
         final int testUser = initializeNewUserAndSwitchBack(/* stopNewUser */ false);
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
-            waitCoolDownPeriod();
             Log.d(TAG, "Starting timer");
             mRunner.resumeTiming();
 
@@ -614,7 +567,6 @@
 
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
-            waitForBroadcastIdle();
             switchUserNoCheck(startUser);
             mRunner.resumeTimingForNextIteration();
         }
@@ -631,7 +583,6 @@
                 /* useStaticWallpaper */ true);
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
-            waitCoolDownPeriod();
             Log.d(TAG, "Starting timer");
             mRunner.resumeTiming();
 
@@ -639,7 +590,6 @@
 
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
-            waitForBroadcastIdle();
             switchUserNoCheck(startUser);
             mRunner.resumeTimingForNextIteration();
         }
@@ -675,13 +625,11 @@
     @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS)
     public void stopUser_realistic() throws RemoteException {
         final int userId = createUserNoFlags();
-        waitCoolDownPeriod();
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
             runThenWaitForBroadcasts(userId, ()-> {
                 mIam.startUserInBackground(userId);
             }, Intent.ACTION_USER_STARTED, Intent.ACTION_MEDIA_MOUNTED);
-            waitCoolDownPeriod();
             Log.d(TAG, "Starting timer");
             mRunner.resumeTiming();
 
@@ -703,7 +651,7 @@
             final int startUser = mAm.getCurrentUser();
             final int userId = createUserNoFlags();
 
-            waitForBroadcastIdle();
+            mRunner.waitCoolDownPeriod();
             mUserSwitchWaiter.runThenWaitUntilBootCompleted(userId, () -> {
                 mRunner.resumeTiming();
                 Log.i(TAG, "Starting timer");
@@ -726,7 +674,7 @@
             final int startUser = ActivityManager.getCurrentUser();
             final int userId = createUserNoFlags();
 
-            waitCoolDownPeriod();
+            mRunner.waitCoolDownPeriod();
             mUserSwitchWaiter.runThenWaitUntilBootCompleted(userId, () -> {
                 mRunner.resumeTiming();
                 Log.d(TAG, "Starting timer");
@@ -752,7 +700,7 @@
                 switchUser(userId);
             }, Intent.ACTION_MEDIA_MOUNTED);
 
-            waitForBroadcastIdle();
+            mRunner.waitCoolDownPeriod();
             mUserSwitchWaiter.runThenWaitUntilSwitchCompleted(startUser, () -> {
                 runThenWaitForBroadcasts(userId, () -> {
                     mRunner.resumeTiming();
@@ -781,7 +729,7 @@
                 switchUser(userId);
             }, Intent.ACTION_MEDIA_MOUNTED);
 
-            waitCoolDownPeriod();
+            mRunner.waitCoolDownPeriod();
             mUserSwitchWaiter.runThenWaitUntilSwitchCompleted(startUser, () -> {
                 runThenWaitForBroadcasts(userId, () -> {
                     mRunner.resumeTiming();
@@ -827,7 +775,6 @@
             Log.d(TAG, "Stopping timer");
             attestTrue("Failed creating profile " + userId, mUm.isManagedProfile(userId));
             removeUser(userId);
-            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -868,7 +815,6 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
-            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -913,7 +859,6 @@
 
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
-            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
         removeUser(userId);
@@ -965,7 +910,6 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
-            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -1030,7 +974,6 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
-            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -1071,7 +1014,6 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
-            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -1124,7 +1066,6 @@
             mRunner.pauseTiming();
             Log.d(TAG, "Stopping timer");
             removeUser(userId);
-            waitCoolDownPeriod();
             mRunner.resumeTimingForNextIteration();
         }
     }
@@ -1164,7 +1105,6 @@
             runThenWaitForBroadcasts(userId, () -> {
                 startUserInBackgroundAndWaitForUnlock(userId);
             }, Intent.ACTION_MEDIA_MOUNTED);
-            waitCoolDownPeriod();
             mRunner.resumeTiming();
             Log.d(TAG, "Starting timer");
 
@@ -1280,6 +1220,7 @@
      * If lack of success should fail the test, use {@link #switchUser(int)} instead.
      */
     private boolean switchUserNoCheck(int userId) throws RemoteException {
+        mRunner.waitCoolDownPeriod();
         final boolean[] success = {true};
         mUserSwitchWaiter.runThenWaitUntilSwitchCompleted(userId, () -> {
             mAm.switchUser(userId);
@@ -1296,7 +1237,7 @@
      */
     private void stopUserAfterWaitingForBroadcastIdle(int userId)
             throws RemoteException {
-        waitForBroadcastIdle();
+        mRunner.waitCoolDownPeriod();
         stopUser(userId);
     }
 
@@ -1438,6 +1379,8 @@
      */
     private void runThenWaitForBroadcasts(int userId, FunctionalUtils.ThrowingRunnable runnable,
             String... actions) {
+        mRunner.waitCoolDownPeriod();
+
         final String unreceivedAction =
                 mBroadcastWaiter.runThenWaitForBroadcasts(userId, runnable, actions);
 
@@ -1538,28 +1481,4 @@
         assertEquals("", ShellHelper.runShellCommand("setprop " + name + " " + value));
         return TextUtils.firstNotEmpty(oldValue, "invalid");
     }
-
-    private void waitForBroadcastIdle() {
-        try {
-            ShellHelper.runShellCommandWithTimeout(
-                    "am wait-for-broadcast-idle --flush-broadcast-loopers", TIMEOUT_IN_SECOND);
-        } catch (TimeoutException e) {
-            Log.e(TAG, "Ending waitForBroadcastIdle because it is taking too long", e);
-        }
-    }
-
-    private void sleep(long ms) {
-        try {
-            Thread.sleep(ms);
-        } catch (InterruptedException e) {
-            // Ignore
-        }
-    }
-
-    private void waitCoolDownPeriod() {
-        // Heuristic value based on local tests. Stability increased compared to no waiting.
-        final int tenSeconds = 1000 * 10;
-        waitForBroadcastIdle();
-        sleep(tenSeconds);
-    }
 }
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 1e824a1..2887d22 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -1401,17 +1401,9 @@
     public static final int RESTRICTION_SUBREASON_MAX_LENGTH = 16;
 
     /**
-     * Restriction reason unknown - do not use directly.
-     *
-     * For use with noteAppRestrictionEnabled()
-     * @hide
-     */
-    public static final int RESTRICTION_REASON_UNKNOWN = 0;
-
-    /**
      * Restriction reason to be used when this is normal behavior for the state.
      *
-     * For use with noteAppRestrictionEnabled()
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
      * @hide
      */
     public static final int RESTRICTION_REASON_DEFAULT = 1;
@@ -1420,7 +1412,7 @@
      * Restriction reason is some kind of timeout that moves the app to a more restricted state.
      * The threshold should specify how long the app was dormant, in milliseconds.
      *
-     * For use with noteAppRestrictionEnabled()
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
      * @hide
      */
     public static final int RESTRICTION_REASON_DORMANT = 2;
@@ -1429,7 +1421,7 @@
      * Restriction reason to be used when removing a restriction due to direct or indirect usage
      * of the app, especially to undo any automatic restrictions.
      *
-     * For use with noteAppRestrictionEnabled()
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
      * @hide
      */
     public static final int RESTRICTION_REASON_USAGE = 3;
@@ -1438,63 +1430,102 @@
      * Restriction reason to be used when the user chooses to manually restrict the app, through
      * UI or command line interface.
      *
-     * For use with noteAppRestrictionEnabled()
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
      * @hide
      */
     public static final int RESTRICTION_REASON_USER = 4;
 
     /**
-     * Restriction reason to be used when the user chooses to manually restrict the app on being
-     * prompted by the OS or some anomaly detection algorithm. For example, if the app is causing
-     * high battery drain or affecting system performance and the OS recommends that the user
-     * restrict the app.
-     *
-     * For use with noteAppRestrictionEnabled()
-     * @hide
-     */
-    public static final int RESTRICTION_REASON_USER_NUDGED = 5;
-
-    /**
      * Restriction reason to be used when the OS automatically detects that the app is causing
      * system health issues such as performance degradation, battery drain, high memory usage, etc.
      *
-     * For use with noteAppRestrictionEnabled()
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
      * @hide
      */
-    public static final int RESTRICTION_REASON_SYSTEM_HEALTH = 6;
+    public static final int RESTRICTION_REASON_SYSTEM_HEALTH = 5;
 
     /**
-     * Restriction reason to be used when there is a server-side decision made to restrict an app
-     * that is showing widespread problems on user devices, or violating policy in some way.
+     * Restriction reason to be used when app is doing something that is against policy, such as
+     * spamming the user or being deceptive about its intentions.
      *
-     * For use with noteAppRestrictionEnabled()
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
      * @hide
      */
-    public static final int RESTRICTION_REASON_REMOTE_TRIGGER = 7;
+    public static final int RESTRICTION_REASON_POLICY = 6;
 
     /**
      * Restriction reason to be used when some other problem requires restricting the app.
      *
-     * For use with noteAppRestrictionEnabled()
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
      * @hide
      */
-    public static final int RESTRICTION_REASON_OTHER = 8;
+    public static final int RESTRICTION_REASON_OTHER = 7;
 
     /** @hide */
     @IntDef(prefix = { "RESTRICTION_REASON_" }, value = {
-            RESTRICTION_REASON_UNKNOWN,
             RESTRICTION_REASON_DEFAULT,
             RESTRICTION_REASON_DORMANT,
             RESTRICTION_REASON_USAGE,
             RESTRICTION_REASON_USER,
-            RESTRICTION_REASON_USER_NUDGED,
             RESTRICTION_REASON_SYSTEM_HEALTH,
-            RESTRICTION_REASON_REMOTE_TRIGGER,
+            RESTRICTION_REASON_POLICY,
             RESTRICTION_REASON_OTHER
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface RestrictionReason{}
 
+    /**
+     * The source of restriction is the user manually choosing to do so.
+     *
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
+     * @hide
+     */
+    public static final int RESTRICTION_SOURCE_USER = 1;
+
+    /**
+     * The source of restriction is the user, on being prompted by the system for the specified
+     * reason.
+     *
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
+     * @hide
+     */
+    public static final int RESTRICTION_SOURCE_USER_NUDGED = 2;
+
+    /**
+     * The source of restriction is the system.
+     *
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
+     * @hide
+     */
+    public static final int RESTRICTION_SOURCE_SYSTEM = 3;
+
+    /**
+     * The source of restriction is the command line interface through the shell or a test.
+     *
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
+     * @hide
+     */
+    public static final int RESTRICTION_SOURCE_COMMAND_LINE = 4;
+
+    /**
+     * The source of restriction is a configuration pushed from a server.
+     *
+     * @see #noteAppRestrictionEnabled(String, int, int, boolean, int, String, int, long)
+     * @hide
+     */
+    public static final int RESTRICTION_SOURCE_REMOTE_TRIGGER = 5;
+
+    /** @hide */
+    @IntDef(prefix = { "RESTRICTION_SOURCE_" }, value = {
+            RESTRICTION_SOURCE_USER,
+            RESTRICTION_SOURCE_USER_NUDGED,
+            RESTRICTION_SOURCE_SYSTEM,
+            RESTRICTION_SOURCE_COMMAND_LINE,
+            RESTRICTION_SOURCE_REMOTE_TRIGGER,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RestrictionSource{}
+
     /** @hide */
     @android.ravenwood.annotation.RavenwoodKeep
     public static String restrictionLevelToName(@RestrictionLevel int level) {
@@ -6254,7 +6285,7 @@
      * <p>
      * The {@code enabled} value determines whether the state is being applied or removed.
      * Not all restrictions are actual restrictions. For example,
-     * {@link #RESTRICTION_LEVEL_ADAPTIVE} is a normal state, where there is default lifecycle
+     * {@link #RESTRICTION_LEVEL_ADAPTIVE_BUCKET} is a normal state, where there is default lifecycle
      * management applied to the app. Also, {@link #RESTRICTION_LEVEL_EXEMPTED} is used when the
      * app is being put in a power-save allowlist.
      * <p>
@@ -6267,6 +6298,7 @@
      *     true,
      *     RESTRICTION_REASON_USER,
      *     "settings",
+     *     RESTRICTION_SOURCE_USER,
      *     0);
      * </pre>
      * Example arguments when app is put in restricted standby bucket for exceeding X hours of jobs:
@@ -6278,6 +6310,7 @@
      *     true,
      *     RESTRICTION_REASON_SYSTEM_HEALTH,
      *     "job_duration",
+     *     RESTRICTION_SOURCE_SYSTEM,
      *     X * 3600 * 1000L);
      * </pre>
      *
@@ -6295,7 +6328,7 @@
      *                  Examples of system resource usage: wakelock, wakeups, mobile_data,
      *                  binder_calls, memory, excessive_threads, excessive_cpu, gps_scans, etc.
      *                  Examples of user actions: settings, notification, command_line, launch, etc.
-     *
+     * @param source the source of the action, from {@code RestrictionSource}
      * @param threshold for reasons that are due to exceeding some threshold, the threshold value
      *                  must be specified. The unit of the threshold depends on the reason and/or
      *                  subReason. For time, use milliseconds. For memory, use KB. For count, use
@@ -6308,10 +6341,10 @@
     public void noteAppRestrictionEnabled(@NonNull String packageName, int uid,
             @RestrictionLevel int restrictionLevel, boolean enabled,
             @RestrictionReason int reason,
-            @Nullable String subReason, long threshold) {
+            @Nullable String subReason, @RestrictionSource int source, long threshold) {
         try {
             getService().noteAppRestrictionEnabled(packageName, uid, restrictionLevel, enabled,
-                    reason, subReason, threshold);
+                    reason, subReason, source, threshold);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 76c1ed6..7dbf270 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -355,7 +355,7 @@
 
     private static final String DEFAULT_FULL_BACKUP_AGENT = "android.app.backup.FullBackupAgent";
 
-    private static final long BINDER_CALLBACK_THROTTLE = 10_100L;
+    private static final long BINDER_CALLBACK_THROTTLE_MS = 10_100L;
     private long mBinderCallbackLast = -1;
 
     /**
@@ -7551,12 +7551,13 @@
             @Override
             public void onTransactionError(int pid, int code, int flags, int err) {
                 final long now = SystemClock.uptimeMillis();
-                if (now < mBinderCallbackLast + BINDER_CALLBACK_THROTTLE) {
+                if (now < mBinderCallbackLast + BINDER_CALLBACK_THROTTLE_MS) {
                     Slog.d(TAG, "Too many transaction errors, throttling freezer binder callback.");
                     return;
                 }
                 mBinderCallbackLast = now;
                 try {
+                    Log.wtfStack(TAG, "Binder Transaction Error");
                     mgr.frozenBinderTransactionDetected(pid, code, flags, err);
                 } catch (RemoteException ex) {
                     throw ex.rethrowFromSystemServer();
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index e8b57f2..15b13dc 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -1016,5 +1016,5 @@
      */
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.DEVICE_POWER)")
     void noteAppRestrictionEnabled(in String packageName, int uid, int restrictionType,
-            boolean enabled, int reason, in String subReason, long threshold);
+            boolean enabled, int reason, in String subReason, int source, long threshold);
 }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index dfae48b..4c839f1 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -5637,6 +5637,10 @@
                     contentView.setDrawableTint(R.id.profile_badge, false,
                             getPrimaryTextColor(p), PorterDuff.Mode.SRC_ATOP);
                 }
+                contentView.setContentDescription(
+                        R.id.profile_badge,
+                        mContext.getSystemService(UserManager.class)
+                                .getProfileAccessibilityString(mContext.getUserId()));
             }
         }
 
@@ -8924,10 +8928,9 @@
                 sender = stripStyling(sender);
             }
 
-            final boolean showOnlySenderName = showOnlySenderName();
-
             final CharSequence title;
-            boolean isConversationTitleAvailable = !showOnlySenderName && conversationTitle != null;
+            final boolean isConversationTitleAvailable = showConversationTitle()
+                    && conversationTitle != null;
             if (isConversationTitleAvailable) {
                 title = conversationTitle;
             } else {
@@ -8947,10 +8950,10 @@
             }
         }
 
-        /** developer settings to always show sender name */
-        private boolean showOnlySenderName() {
+        /** (b/342370742) Developer settings to show conversation title. */
+        private boolean showConversationTitle() {
             return SystemProperties.getBoolean(
-                    "persist.compact_heads_up_notification.show_only_sender_name",
+                    "persist.compact_heads_up_notification.show_conversation_title_for_group",
                     false);
         }
 
diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java
index 193c524..3f6c81b 100644
--- a/core/java/android/app/NotificationChannel.java
+++ b/core/java/android/app/NotificationChannel.java
@@ -695,8 +695,8 @@
      * {@link NotificationManager#createNotificationChannel(NotificationChannel)}.
      *
      * @see #getVibrationEffect()
-     * @see Vibrator#areEffectsSupported(int...)
-     * @see Vibrator#arePrimitivesSupported(int...)
+     * @see android.os.Vibrator#areEffectsSupported(int...)
+     * @see android.os.Vibrator#arePrimitivesSupported(int...)
      */
     @FlaggedApi(Flags.FLAG_NOTIFICATION_CHANNEL_VIBRATION_EFFECT_API)
     public void setVibrationEffect(@Nullable VibrationEffect effect) {
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index c529f7d..44444b5 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -5842,6 +5842,24 @@
      * with {@link #PASSWORD_QUALITY_UNSPECIFIED} on that instance prior to setting complexity
      * requirement for the managed profile.
      *
+     * Starting from {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}, after the password
+     * requirement has been set, {@link PolicyUpdateReceiver#onPolicySetResult(Context, String,
+     * Bundle, TargetUser, PolicyUpdateResult)} will notify the admin on whether the policy was
+     * successfully set or not. This callback will contain:
+     * <ul>
+     * <li> The policy identifier {@link DevicePolicyIdentifiers#PASSWORD_COMPLEXITY_POLICY}
+     * <li> The {@link TargetUser} that this policy relates to
+     * <li> The {@link PolicyUpdateResult}, which will be
+     * {@link PolicyUpdateResult#RESULT_POLICY_SET} if the policy was successfully set or the
+     * reason the policy failed to be set
+     * e.g. {@link PolicyUpdateResult#RESULT_FAILURE_CONFLICTING_ADMIN_POLICY})
+     * </ul>
+     * If there has been a change to the policy,
+     * {@link PolicyUpdateReceiver#onPolicyChanged(Context, String, Bundle, TargetUser,
+     * PolicyUpdateResult)} will notify the admin of this change. This callback will contain the
+     * same parameters as PolicyUpdateReceiver#onPolicySetResult and the {@link PolicyUpdateResult}
+     * will contain the reason why the policy changed.
+     *
      * @throws SecurityException if the calling application is not a device owner or a profile
      * owner.
      * @throws IllegalArgumentException if the complexity level is not one of the four above.
@@ -5849,6 +5867,7 @@
      * are password requirements specified using {@link #setPasswordQuality(ComponentName, int)}
      * on the parent {@code DevicePolicyManager} instance.
      */
+    @SupportsCoexistence
     @RequiresPermission(value = MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS, conditional = true)
     public void setRequiredPasswordComplexity(@PasswordComplexity int passwordComplexity) {
         if (mService == null) {
@@ -5880,6 +5899,7 @@
      * owner.
      */
     @PasswordComplexity
+    @SupportsCoexistence
     @RequiresPermission(value = MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS, conditional = true)
     public int getRequiredPasswordComplexity() {
         if (mService == null) {
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 86b4180..7d5806a 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -328,6 +328,16 @@
 }
 
 flag {
+    name: "unmanaged_mode_migration"
+    namespace: "enterprise"
+    description: "Migrate APIs for unmanaged mode"
+    bug: "335624297"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "headless_single_user_fixes"
     namespace: "enterprise"
     description: "Various fixes for headless single user mode"
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 6ceae17..55c3bb60 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -166,4 +166,11 @@
   namespace: "systemui"
   description: "[Minimal HUN] Enables the compact heads up notification reply capability for Conversation Notifications"
   bug: "336229954"
-}
\ No newline at end of file
+}
+
+flag {
+  name: "remove_remote_views"
+  namespace: "systemui"
+  description: "Removes all custom views"
+  bug: "342602960"
+}
diff --git a/core/java/android/app/trust/ITrustManager.aidl b/core/java/android/app/trust/ITrustManager.aidl
index 70c25ea..740f593 100644
--- a/core/java/android/app/trust/ITrustManager.aidl
+++ b/core/java/android/app/trust/ITrustManager.aidl
@@ -41,4 +41,5 @@
     void unlockedByBiometricForUser(int userId, in BiometricSourceType source);
     void clearAllBiometricRecognized(in BiometricSourceType target, int unlockedUser);
     boolean isActiveUnlockRunning(int userId);
+    boolean isInSignificantPlace();
 }
diff --git a/core/java/android/app/trust/TrustManager.java b/core/java/android/app/trust/TrustManager.java
index 23b2ea4..88d4d69 100644
--- a/core/java/android/app/trust/TrustManager.java
+++ b/core/java/android/app/trust/TrustManager.java
@@ -299,6 +299,20 @@
         }
     }
 
+    /**
+     * Returns true if the device is currently in a significant place, and false in all other
+     * circumstances.
+     *
+     * @hide
+     */
+    public boolean isInSignificantPlace() {
+        try {
+            return mService.isInSignificantPlace();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
         @Override
         public void handleMessage(Message msg) {
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index 1eb466c..e3780ed 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -683,7 +683,9 @@
      * <p>
      * Read-only transactions may run concurrently with other read-only transactions, and if the
      * database is in WAL mode, they may also run concurrently with IMMEDIATE or EXCLUSIVE
-     * transactions.
+     * transactions. The {@code temp} schema may be modified during a read-only transaction;
+     * if the transaction is {@link #setTransactionSuccessful}, modifications to temp tables may
+     * be visible to some subsequent transactions.
      * <p>
      * Transactions can be nested.  However, the behavior of the transaction is not altered by
      * nested transactions.  A nested transaction may be any of the three transaction types but if
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index ec67212..b2dcf90 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -739,6 +739,9 @@
          *                  on is done.
          */
         void onBlockingScreenOn(Runnable unblocker);
+
+        /** Whether auto brightness update in doze is allowed */
+        boolean allowAutoBrightnessInDoze();
     }
 
     /** A session token that associates a internal display with a {@link DisplayOffloader}. */
@@ -749,6 +752,9 @@
         /** Whether the session is active. */
         boolean isActive();
 
+        /** Whether auto brightness update in doze is allowed */
+        boolean allowAutoBrightnessInDoze();
+
         /**
          * Update the brightness from the offload chip.
          * @param brightness The brightness value between {@link PowerManager.BRIGHTNESS_MIN} and
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index c611cb9..3e6223a 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -1948,6 +1948,10 @@
 
         public static final int SETTLE_TO_ZERO_STATES = 0xffff0000 & ~MOST_INTERESTING_STATES;
 
+        // STATES bits that are used for Power Stats tracking
+        public static final int IMPORTANT_FOR_POWER_STATS_STATES =
+                STATE_GPS_ON_FLAG | STATE_SENSOR_ON_FLAG | STATE_AUDIO_ON_FLAG;
+
         @UnsupportedAppUsage
         public int states;
 
@@ -1988,6 +1992,10 @@
 
         public static final int SETTLE_TO_ZERO_STATES2 = 0xffff0000 & ~MOST_INTERESTING_STATES2;
 
+        // STATES2 bits that are used for Power Stats tracking
+        public static final int IMPORTANT_FOR_POWER_STATS_STATES2 =
+                STATE2_VIDEO_ON_FLAG | STATE2_FLASHLIGHT_FLAG | STATE2_CAMERA_FLAG;
+
         @UnsupportedAppUsage
         public int states2;
 
@@ -2053,6 +2061,8 @@
         public static final int EVENT_WAKEUP_AP = 0x0013;
         // Event for reporting that a specific partial wake lock has been held for a long duration.
         public static final int EVENT_LONG_WAKE_LOCK = 0x0014;
+        // Event for reporting change of some device states, triggered by a specific UID
+        public static final int EVENT_STATE_CHANGE = 0x0015;
 
         // Number of event types.
         public static final int EVENT_COUNT = 0x0016;
@@ -3066,13 +3076,13 @@
     public static final String[] HISTORY_EVENT_NAMES = new String[] {
             "null", "proc", "fg", "top", "sync", "wake_lock_in", "job", "user", "userfg", "conn",
             "active", "pkginst", "pkgunin", "alarm", "stats", "pkginactive", "pkgactive",
-            "tmpwhitelist", "screenwake", "wakeupap", "longwake", "est_capacity"
+            "tmpwhitelist", "screenwake", "wakeupap", "longwake", "est_capacity", "state"
     };
 
     public static final String[] HISTORY_EVENT_CHECKIN_NAMES = new String[] {
             "Enl", "Epr", "Efg", "Etp", "Esy", "Ewl", "Ejb", "Eur", "Euf", "Ecn",
             "Eac", "Epi", "Epu", "Eal", "Est", "Eai", "Eaa", "Etw",
-            "Esw", "Ewa", "Elw", "Eec"
+            "Esw", "Ewa", "Elw", "Eec", "Esc"
     };
 
     @FunctionalInterface
@@ -3087,7 +3097,7 @@
             sUidToString, sUidToString, sUidToString, sUidToString, sUidToString, sUidToString,
             sUidToString, sUidToString, sUidToString, sUidToString, sUidToString, sIntToString,
             sUidToString, sUidToString, sUidToString, sUidToString, sUidToString, sUidToString,
-            sUidToString, sUidToString, sUidToString, sIntToString
+            sUidToString, sUidToString, sUidToString, sIntToString, sUidToString
     };
 
     /**
diff --git a/core/java/android/os/DeadObjectException.java b/core/java/android/os/DeadObjectException.java
index fc3870e..998e295 100644
--- a/core/java/android/os/DeadObjectException.java
+++ b/core/java/android/os/DeadObjectException.java
@@ -26,7 +26,7 @@
  * receive this error from an app, at a minimum, you should
  * recover by resetting the connection. For instance, you should
  * drop the binder, clean up associated state, and reset your
- * connection to the service which through this error. In order
+ * connection to the service which threw this error. In order
  * to simplify your error recovery paths, you may also want to
  * "simply" restart your process. However, this may not be an
  * option if the service you are talking to is unreliable or
@@ -34,9 +34,11 @@
  *
  * If this isn't from a service death and is instead from a
  * low-level binder error, it will be from:
- * - a oneway call queue filling up (too many oneway calls)
- * - from the binder buffer being filled up, so that the transaction
- *   is rejected.
+ * <ul>
+ * <li> a one-way call queue filling up (too many one-way calls)
+ * <li> from the binder buffer being filled up, so that the transaction
+ *      is rejected.
+ * </ul>
  *
  * In these cases, more information about the error will be
  * logged. However, there isn't a good way to differentiate
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 17dfdda..71957ee 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -52,6 +52,8 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.ravenwood.RavenwoodEnvironment;
+
 import dalvik.system.VMRuntime;
 
 import libcore.io.IoUtils;
@@ -388,7 +390,6 @@
      * new file descriptor shared state such as file position with the
      * original file descriptor.
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
     public static ParcelFileDescriptor dup(FileDescriptor orig) throws IOException {
         try {
             final FileDescriptor fd = new FileDescriptor();
@@ -406,7 +407,6 @@
      * new file descriptor shared state such as file position with the
      * original file descriptor.
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
     public ParcelFileDescriptor dup() throws IOException {
         if (mWrapped != null) {
             return mWrapped.dup();
@@ -425,7 +425,6 @@
      * @return Returns a new ParcelFileDescriptor holding a FileDescriptor
      * for a dup of the given fd.
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
     public static ParcelFileDescriptor fromFd(int fd) throws IOException {
         final FileDescriptor original = new FileDescriptor();
         setFdInt(original, fd);
@@ -485,7 +484,7 @@
      *
      * @throws UncheckedIOException if {@link #dup(FileDescriptor)} throws IOException.
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
+    @RavenwoodThrow(reason = "Socket.getFileDescriptor$()")
     public static ParcelFileDescriptor fromSocket(Socket socket) {
         FileDescriptor fd = socket.getFileDescriptor$();
         try {
@@ -519,7 +518,7 @@
      *
      * @throws UncheckedIOException if {@link #dup(FileDescriptor)} throws IOException.
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
+    @RavenwoodThrow(reason = "DatagramSocket.getFileDescriptor$()")
     public static ParcelFileDescriptor fromDatagramSocket(DatagramSocket datagramSocket) {
         FileDescriptor fd = datagramSocket.getFileDescriptor$();
         try {
@@ -534,7 +533,6 @@
      * ParcelFileDescriptor in the returned array is the read side; the second
      * is the write side.
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
     public static ParcelFileDescriptor[] createPipe() throws IOException {
         try {
             final FileDescriptor[] fds = Os.pipe2(ifAtLeastQ(O_CLOEXEC));
@@ -556,7 +554,6 @@
      * calling {@link #checkError()}, usually after detecting an EOF.
      * This can also be used to detect remote crashes.
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
     public static ParcelFileDescriptor[] createReliablePipe() throws IOException {
         try {
             final FileDescriptor[] comm = createCommSocketPair();
@@ -573,7 +570,7 @@
      * Create two ParcelFileDescriptors structured as a pair of sockets
      * connected to each other. The two sockets are indistinguishable.
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
+    @RavenwoodThrow(reason = "Os.socketpair()")
     public static ParcelFileDescriptor[] createSocketPair() throws IOException {
         return createSocketPair(SOCK_STREAM);
     }
@@ -581,7 +578,7 @@
     /**
      * @hide
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
+    @RavenwoodThrow(reason = "Os.socketpair()")
     public static ParcelFileDescriptor[] createSocketPair(int type) throws IOException {
         try {
             final FileDescriptor fd0 = new FileDescriptor();
@@ -604,7 +601,7 @@
      * calling {@link #checkError()}, usually after detecting an EOF.
      * This can also be used to detect remote crashes.
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
+    @RavenwoodThrow(reason = "Os.socketpair()")
     public static ParcelFileDescriptor[] createReliableSocketPair() throws IOException {
         return createReliableSocketPair(SOCK_STREAM);
     }
@@ -612,7 +609,7 @@
     /**
      * @hide
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
+    @RavenwoodThrow(reason = "Os.socketpair()")
     public static ParcelFileDescriptor[] createReliableSocketPair(int type) throws IOException {
         try {
             final FileDescriptor[] comm = createCommSocketPair();
@@ -627,7 +624,7 @@
         }
     }
 
-    @RavenwoodThrow(reason = "Requires JNI support")
+    @RavenwoodThrow(reason = "Os.socketpair()")
     private static FileDescriptor[] createCommSocketPair() throws IOException {
         try {
             // Use SOCK_SEQPACKET so that we have a guarantee that the status
@@ -656,7 +653,7 @@
      */
     @UnsupportedAppUsage
     @Deprecated
-    @RavenwoodThrow(reason = "Requires JNI support")
+    @RavenwoodThrow(blockedBy = MemoryFile.class)
     public static ParcelFileDescriptor fromData(byte[] data, String name) throws IOException {
         if (data == null) return null;
         MemoryFile file = new MemoryFile(name, data.length);
@@ -712,7 +709,7 @@
      * @hide
      */
     @TestApi
-    @RavenwoodThrow(reason = "Requires kernel support")
+    @RavenwoodThrow(reason = "Os.readlink() and Os.stat()")
     public static File getFile(FileDescriptor fd) throws IOException {
         try {
             final String path = Os.readlink("/proc/self/fd/" + getFdInt(fd));
@@ -744,7 +741,7 @@
      * Return the total size of the file representing this fd, as determined by
      * {@code stat()}. Returns -1 if the fd is not a file.
      */
-    @RavenwoodThrow(reason = "Requires JNI support")
+    @RavenwoodThrow(reason = "Os.readlink() and Os.stat()")
     public long getStatSize() {
         if (mWrapped != null) {
             return mWrapped.getStatSize();
@@ -769,7 +766,6 @@
      * @hide
      */
     @UnsupportedAppUsage
-    @RavenwoodThrow(reason = "Requires JNI support")
     public long seekTo(long pos) throws IOException {
         if (mWrapped != null) {
             return mWrapped.seekTo(pos);
@@ -1037,7 +1033,6 @@
      * take care of calling {@link ParcelFileDescriptor#close
      * ParcelFileDescriptor.close()} for you when the stream is closed.
      */
-    @RavenwoodKeepWholeClass
     public static class AutoCloseInputStream extends FileInputStream {
         private final ParcelFileDescriptor mPfd;
 
@@ -1326,12 +1321,15 @@
 
     }
 
-    @RavenwoodThrow
+    @RavenwoodReplace
     private static boolean isAtLeastQ() {
         return (VMRuntime.getRuntime().getTargetSdkVersion() >= Build.VERSION_CODES.Q);
     }
 
-    @RavenwoodThrow
+    private static boolean isAtLeastQ$ravenwood() {
+        return RavenwoodEnvironment.workaround().isTargetSdkAtLeastQ();
+    }
+
     private static int ifAtLeastQ(int value) {
         return isAtLeastQ() ? value : 0;
     }
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 80d3566..fdce476 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -5958,19 +5958,22 @@
     /**
      * Returns the string used to represent the profile associated with the given userId. This
      * string typically includes the profile name used by accessibility services like TalkBack.
-     * @hide
      *
      * @return String representing the accessibility label for the given profile user.
      *
      * @throws android.content.res.Resources.NotFoundException if the user does not have a label
      * defined.
+     *
+     * @see #getBadgedLabelForUser(CharSequence, UserHandle)
+     *
+     * @hide
      */
     @UserHandleAware(
             requiresAnyOfPermissionsIfNotCallerProfileGroup = {
                     Manifest.permission.MANAGE_USERS,
                     Manifest.permission.QUERY_USERS,
                     Manifest.permission.INTERACT_ACROSS_USERS})
-    public String getProfileAccessibilityString(int userId) {
+    public String getProfileAccessibilityString(@UserIdInt int userId) {
         if (isManagedProfile(mUserId)) {
             DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
             dpm.getResources().getString(
@@ -5980,7 +5983,7 @@
         return getProfileAccessibilityLabel(userId);
     }
 
-    private String getProfileAccessibilityLabel(int userId) {
+    private String getProfileAccessibilityLabel(@UserIdInt int userId) {
         try {
             final int resourceId = mService.getProfileAccessibilityLabelResId(userId);
             return Resources.getSystem().getString(resourceId);
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 25389e5..34fb963 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -193,4 +193,12 @@
     namespace: "permissions"
     description: "Enable getDeviceId API in OpEventProxyInfo"
     bug: "337340961"
+ }
+
+flag {
+    name: "device_aware_app_op_new_schema_enabled"
+    is_fixed_read_only: true
+    namespace: "permissions"
+    description: "Persist device attributed AppOp accesses on the disk"
+    bug: "308201969"
 }
\ No newline at end of file
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index ee5e533..fe7eab7 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -81,6 +81,13 @@
 }
 
 flag {
+    name: "significant_places"
+    namespace: "biometrics"
+    description: "Enabled significant place monitoring"
+    bug: "337870680"
+}
+
+flag {
     name: "report_primary_auth_attempts"
     namespace: "biometrics"
     description: "Report primary auth attempts from LockSettingsService"
diff --git a/core/java/android/view/ContentRecordingSession.java b/core/java/android/view/ContentRecordingSession.java
index dc41b70..952c63b 100644
--- a/core/java/android/view/ContentRecordingSession.java
+++ b/core/java/android/view/ContentRecordingSession.java
@@ -58,6 +58,15 @@
     /** Can't report (e.g. side loaded app). */
     public static final int TARGET_UID_UNKNOWN = -2;
 
+    /** Task id is not set either because full screen capture or launching a new app */
+    public static final int TASK_ID_UNKNOWN = -1;
+
+    /**
+     * Id of Task that is launched to be captured for a single app capture session. The value may be
+     * {@link #TASK_ID_UNKNOWN} if the session is not for a single app capture.
+     */
+    private int mTaskId = TASK_ID_UNKNOWN;
+
     /**
      * Unique logical identifier of the {@link android.hardware.display.VirtualDisplay} that has
      * recorded content rendered to its surface.
@@ -115,16 +124,16 @@
     /** Returns an instance initialized for task recording. */
     public static ContentRecordingSession createTaskSession(
             @NonNull IBinder taskWindowContainerToken) {
-        return createTaskSession(taskWindowContainerToken, TARGET_UID_UNKNOWN);
+        return createTaskSession(taskWindowContainerToken, TASK_ID_UNKNOWN);
     }
 
     /** Returns an instance initialized for task recording. */
     public static ContentRecordingSession createTaskSession(
-            @NonNull IBinder taskWindowContainerToken, int targetUid) {
+            @NonNull IBinder taskWindowContainerToken, int taskId) {
         return new ContentRecordingSession()
                 .setContentToRecord(RECORD_CONTENT_TASK)
                 .setTokenToRecord(taskWindowContainerToken)
-                .setTargetUid(targetUid);
+                .setTaskId(taskId);
     }
 
     /**
@@ -211,12 +220,14 @@
 
     @DataClass.Generated.Member
     /* package-private */ ContentRecordingSession(
+            int taskId,
             int virtualDisplayId,
             @RecordContent int contentToRecord,
             int displayToRecord,
             @Nullable IBinder tokenToRecord,
             boolean waitingForConsent,
             int targetUid) {
+        this.mTaskId = taskId;
         this.mVirtualDisplayId = virtualDisplayId;
         this.mContentToRecord = contentToRecord;
 
@@ -237,6 +248,15 @@
     }
 
     /**
+     * Id of Task that is launched to be captured for a single app capture session. The value may be
+     * {@link #TASK_ID_UNKNOWN} if the session is not for a single app capture.
+     */
+    @DataClass.Generated.Member
+    public int getTaskId() {
+        return mTaskId;
+    }
+
+    /**
      * Unique logical identifier of the {@link android.hardware.display.VirtualDisplay} that has
      * recorded content rendered to its surface.
      */
@@ -295,6 +315,16 @@
     }
 
     /**
+     * Id of Task that is launched to be captured for a single app capture session. The value may be
+     * {@link #TASK_ID_UNKNOWN} if the session is not for a single app capture.
+     */
+    @DataClass.Generated.Member
+    public @NonNull ContentRecordingSession setTaskId( int value) {
+        mTaskId = value;
+        return this;
+    }
+
+    /**
      * Unique logical identifier of the {@link android.hardware.display.VirtualDisplay} that has
      * recorded content rendered to its surface.
      */
@@ -374,6 +404,7 @@
         // String fieldNameToString() { ... }
 
         return "ContentRecordingSession { " +
+                "taskId = " + mTaskId + ", " +
                 "virtualDisplayId = " + mVirtualDisplayId + ", " +
                 "contentToRecord = " + recordContentToString(mContentToRecord) + ", " +
                 "displayToRecord = " + mDisplayToRecord + ", " +
@@ -396,6 +427,7 @@
         ContentRecordingSession that = (ContentRecordingSession) o;
         //noinspection PointlessBooleanExpression
         return true
+                && mTaskId == that.mTaskId
                 && mVirtualDisplayId == that.mVirtualDisplayId
                 && mContentToRecord == that.mContentToRecord
                 && mDisplayToRecord == that.mDisplayToRecord
@@ -411,6 +443,7 @@
         // int fieldNameHashCode() { ... }
 
         int _hash = 1;
+        _hash = 31 * _hash + mTaskId;
         _hash = 31 * _hash + mVirtualDisplayId;
         _hash = 31 * _hash + mContentToRecord;
         _hash = 31 * _hash + mDisplayToRecord;
@@ -427,9 +460,10 @@
         // void parcelFieldName(Parcel dest, int flags) { ... }
 
         byte flg = 0;
-        if (mWaitingForConsent) flg |= 0x10;
-        if (mTokenToRecord != null) flg |= 0x8;
+        if (mWaitingForConsent) flg |= 0x20;
+        if (mTokenToRecord != null) flg |= 0x10;
         dest.writeByte(flg);
+        dest.writeInt(mTaskId);
         dest.writeInt(mVirtualDisplayId);
         dest.writeInt(mContentToRecord);
         dest.writeInt(mDisplayToRecord);
@@ -449,13 +483,15 @@
         // static FieldType unparcelFieldName(Parcel in) { ... }
 
         byte flg = in.readByte();
-        boolean waitingForConsent = (flg & 0x10) != 0;
+        boolean waitingForConsent = (flg & 0x20) != 0;
+        int taskId = in.readInt();
         int virtualDisplayId = in.readInt();
         int contentToRecord = in.readInt();
         int displayToRecord = in.readInt();
-        IBinder tokenToRecord = (flg & 0x8) == 0 ? null : (IBinder) in.readStrongBinder();
+        IBinder tokenToRecord = (flg & 0x10) == 0 ? null : (IBinder) in.readStrongBinder();
         int targetUid = in.readInt();
 
+        this.mTaskId = taskId;
         this.mVirtualDisplayId = virtualDisplayId;
         this.mContentToRecord = contentToRecord;
 
@@ -496,6 +532,7 @@
     @DataClass.Generated.Member
     public static final class Builder {
 
+        private int mTaskId;
         private int mVirtualDisplayId;
         private @RecordContent int mContentToRecord;
         private int mDisplayToRecord;
@@ -509,13 +546,25 @@
         }
 
         /**
+         * Id of Task that is launched to be captured for a single app capture session. The value may be
+         * {@link #TASK_ID_UNKNOWN} if the session is not for a single app capture.
+         */
+        @DataClass.Generated.Member
+        public @NonNull Builder setTaskId(int value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x1;
+            mTaskId = value;
+            return this;
+        }
+
+        /**
          * Unique logical identifier of the {@link android.hardware.display.VirtualDisplay} that has
          * recorded content rendered to its surface.
          */
         @DataClass.Generated.Member
         public @NonNull Builder setVirtualDisplayId(int value) {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x1;
+            mBuilderFieldsSet |= 0x2;
             mVirtualDisplayId = value;
             return this;
         }
@@ -526,7 +575,7 @@
         @DataClass.Generated.Member
         public @NonNull Builder setContentToRecord(@RecordContent int value) {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x2;
+            mBuilderFieldsSet |= 0x4;
             mContentToRecord = value;
             return this;
         }
@@ -540,7 +589,7 @@
         @DataClass.Generated.Member
         public @NonNull Builder setDisplayToRecord(int value) {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x4;
+            mBuilderFieldsSet |= 0x8;
             mDisplayToRecord = value;
             return this;
         }
@@ -554,7 +603,7 @@
         @DataClass.Generated.Member
         public @NonNull Builder setTokenToRecord(@NonNull IBinder value) {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x8;
+            mBuilderFieldsSet |= 0x10;
             mTokenToRecord = value;
             return this;
         }
@@ -568,7 +617,7 @@
         @DataClass.Generated.Member
         public @NonNull Builder setWaitingForConsent(boolean value) {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x10;
+            mBuilderFieldsSet |= 0x20;
             mWaitingForConsent = value;
             return this;
         }
@@ -579,7 +628,7 @@
         @DataClass.Generated.Member
         public @NonNull Builder setTargetUid(int value) {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x20;
+            mBuilderFieldsSet |= 0x40;
             mTargetUid = value;
             return this;
         }
@@ -587,27 +636,31 @@
         /** Builds the instance. This builder should not be touched after calling this! */
         public @NonNull ContentRecordingSession build() {
             checkNotUsed();
-            mBuilderFieldsSet |= 0x40; // Mark builder used
+            mBuilderFieldsSet |= 0x80; // Mark builder used
 
             if ((mBuilderFieldsSet & 0x1) == 0) {
-                mVirtualDisplayId = INVALID_DISPLAY;
+                mTaskId = TASK_ID_UNKNOWN;
             }
             if ((mBuilderFieldsSet & 0x2) == 0) {
-                mContentToRecord = RECORD_CONTENT_DISPLAY;
+                mVirtualDisplayId = INVALID_DISPLAY;
             }
             if ((mBuilderFieldsSet & 0x4) == 0) {
-                mDisplayToRecord = INVALID_DISPLAY;
+                mContentToRecord = RECORD_CONTENT_DISPLAY;
             }
             if ((mBuilderFieldsSet & 0x8) == 0) {
-                mTokenToRecord = null;
+                mDisplayToRecord = INVALID_DISPLAY;
             }
             if ((mBuilderFieldsSet & 0x10) == 0) {
-                mWaitingForConsent = false;
+                mTokenToRecord = null;
             }
             if ((mBuilderFieldsSet & 0x20) == 0) {
+                mWaitingForConsent = false;
+            }
+            if ((mBuilderFieldsSet & 0x40) == 0) {
                 mTargetUid = TARGET_UID_UNKNOWN;
             }
             ContentRecordingSession o = new ContentRecordingSession(
+                    mTaskId,
                     mVirtualDisplayId,
                     mContentToRecord,
                     mDisplayToRecord,
@@ -618,7 +671,7 @@
         }
 
         private void checkNotUsed() {
-            if ((mBuilderFieldsSet & 0x40) != 0) {
+            if ((mBuilderFieldsSet & 0x80) != 0) {
                 throw new IllegalStateException(
                         "This Builder should not be reused. Use a new Builder instance instead");
             }
@@ -626,10 +679,10 @@
     }
 
     @DataClass.Generated(
-            time = 1697456140720L,
+            time = 1716481148184L,
             codegenVersion = "1.0.23",
             sourceFile = "frameworks/base/core/java/android/view/ContentRecordingSession.java",
-            inputSignatures = "public static final  int RECORD_CONTENT_DISPLAY\npublic static final  int RECORD_CONTENT_TASK\npublic static final  int TARGET_UID_FULL_SCREEN\npublic static final  int TARGET_UID_UNKNOWN\nprivate  int mVirtualDisplayId\nprivate @android.view.ContentRecordingSession.RecordContent int mContentToRecord\nprivate  int mDisplayToRecord\nprivate @android.annotation.Nullable android.os.IBinder mTokenToRecord\nprivate  boolean mWaitingForConsent\nprivate  int mTargetUid\npublic static  android.view.ContentRecordingSession createDisplaySession(int)\npublic static  android.view.ContentRecordingSession createTaskSession(android.os.IBinder)\npublic static  android.view.ContentRecordingSession createTaskSession(android.os.IBinder,int)\npublic static  boolean isValid(android.view.ContentRecordingSession)\npublic static  boolean isProjectionOnSameDisplay(android.view.ContentRecordingSession,android.view.ContentRecordingSession)\nclass ContentRecordingSession extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genSetters=true, genEqualsHashCode=true)")
+            inputSignatures = "public static final  int RECORD_CONTENT_DISPLAY\npublic static final  int RECORD_CONTENT_TASK\npublic static final  int TARGET_UID_FULL_SCREEN\npublic static final  int TARGET_UID_UNKNOWN\npublic static final  int TASK_ID_UNKNOWN\nprivate  int mTaskId\nprivate  int mVirtualDisplayId\nprivate @android.view.ContentRecordingSession.RecordContent int mContentToRecord\nprivate  int mDisplayToRecord\nprivate @android.annotation.Nullable android.os.IBinder mTokenToRecord\nprivate  boolean mWaitingForConsent\nprivate  int mTargetUid\npublic static  android.view.ContentRecordingSession createDisplaySession(int)\npublic static  android.view.ContentRecordingSession createTaskSession(android.os.IBinder)\npublic static  android.view.ContentRecordingSession createTaskSession(android.os.IBinder,int)\npublic static  boolean isValid(android.view.ContentRecordingSession)\npublic static  boolean isProjectionOnSameDisplay(android.view.ContentRecordingSession,android.view.ContentRecordingSession)\nclass ContentRecordingSession extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genSetters=true, genEqualsHashCode=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 1fc98cf..a9846fb 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -1262,15 +1262,11 @@
                     mHost.getInputMethodManager(), null /* icProto */);
         }
 
-        final var statsToken = (types & ime()) == 0 ? null
-                : ImeTracker.forLogging().onStart(ImeTracker.TYPE_USER,
-                        ImeTracker.ORIGIN_CLIENT,
-                        SoftInputShowHideReason.CONTROL_WINDOW_INSETS_ANIMATION,
-                        mHost.isHandlingPointerEvent() /* fromUser */);
+        // TODO(b/342111149): Create statsToken here once ImeTracker#onStart becomes async.
         controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs,
                 interpolator, animationType,
                 getLayoutInsetsDuringAnimationMode(types, fromPredictiveBack),
-                false /* useInsetsAnimationThread */, statsToken);
+                false /* useInsetsAnimationThread */, null);
     }
 
     private void controlAnimationUnchecked(@InsetsType int types,
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 1cd7d34..0bdb4ad 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -47,6 +47,7 @@
 import android.graphics.Region;
 import android.gui.DropInputMode;
 import android.gui.StalledTransactionInfo;
+import android.gui.TrustedOverlay;
 import android.hardware.DataSpace;
 import android.hardware.HardwareBuffer;
 import android.hardware.OverlayProperties;
@@ -165,7 +166,7 @@
             float maxStretchAmountX, float maxStretchAmountY, float childRelativeLeft,
             float childRelativeTop, float childRelativeRight, float childRelativeBottom);
     private static native void nativeSetTrustedOverlay(long transactionObj, long nativeObject,
-            boolean isTrustedOverlay);
+            int isTrustedOverlay);
     private static native void nativeSetDropInputMode(
             long transactionObj, long nativeObject, int flags);
     private static native void nativeSetCanOccludePresentation(long transactionObj,
@@ -302,6 +303,7 @@
                                                                 long desiredPresentTimeNanos);
     private static native void nativeSetFrameTimeline(long transactionObj,
                                                            long vsyncId);
+    private static native void nativeNotifyShutdown();
 
     /**
      * Transforms that can be applied to buffers as they are displayed to a window.
@@ -4303,13 +4305,37 @@
         }
 
         /**
-         * Sets the trusted overlay state on this SurfaceControl and it is inherited to all the
-         * children. The caller must hold the ACCESS_SURFACE_FLINGER permission.
+         * @see Transaction#setTrustedOverlay(SurfaceControl, int)
          * @hide
          */
         public Transaction setTrustedOverlay(SurfaceControl sc, boolean isTrustedOverlay) {
+            return setTrustedOverlay(sc,
+                    isTrustedOverlay ? TrustedOverlay.ENABLED : TrustedOverlay.UNSET);
+        }
+
+        /**
+         * Trusted overlay state prevents SurfaceControl from being considered as obscuring for
+         * input occlusion detection purposes. The caller must hold the
+         * ACCESS_SURFACE_FLINGER permission. See {@code TrustedOverlay}.
+         * <p>
+         * Arguments:
+         * {@code TrustedOverlay.UNSET} - The default value, SurfaceControl will inherit the state
+         * from its parents. If the parent state is also {@code TrustedOverlay.UNSET}, the layer
+         * will be considered as untrusted.
+         * <p>
+         * {@code TrustedOverlay.DISABLED} - Treats this SurfaceControl and all its children as an
+         * untrusted overlay. This will override any state set by its parent SurfaceControl.
+         * <p>
+         * {@code TrustedOverlay.ENABLED} - Treats this SurfaceControl and all its children as a
+         * trusted overlay unless the child SurfaceControl explicitly disables its trusted state
+         * via {@code TrustedOverlay.DISABLED}.
+         * <p>
+         * @hide
+         */
+        public Transaction setTrustedOverlay(SurfaceControl sc,
+                                             @TrustedOverlay int trustedOverlay) {
             checkPreconditions(sc);
-            nativeSetTrustedOverlay(mNativeObject, sc.mNativeObject, isTrustedOverlay);
+            nativeSetTrustedOverlay(mNativeObject, sc.mNativeObject, trustedOverlay);
             return this;
         }
 
@@ -4765,4 +4791,11 @@
         return nativeGetStalledTransactionInfo(pid);
     }
 
+    /**
+     * Notify the SurfaceFlinger to capture transaction traces when shutdown.
+     * @hide
+     */
+    public static void notifyShutdown() {
+        nativeNotifyShutdown();
+    }
 }
diff --git a/core/java/android/window/IWindowOrganizerController.aidl b/core/java/android/window/IWindowOrganizerController.aidl
index 5ba2f6c..6f4dd4e 100644
--- a/core/java/android/window/IWindowOrganizerController.aidl
+++ b/core/java/android/window/IWindowOrganizerController.aidl
@@ -93,11 +93,17 @@
     ITaskFragmentOrganizerController getTaskFragmentOrganizerController();
 
     /**
-     * Registers a transition player with Core. There is only one of these at a time and calling
-     * this will replace the existing one if set.
+     * Registers a transition player with Core. There is only one of these active at a time so
+     * calling this will replace the existing one (if set) until it is unregistered.
      */
     void registerTransitionPlayer(in ITransitionPlayer player);
 
+    /**
+     * Un-registers a transition player from Core. This will restore whichever player was active
+     * prior to registering this one.
+     */
+    void unregisterTransitionPlayer(in ITransitionPlayer player);
+
     /** @return An interface enabling the transition players to report its metrics. */
     ITransitionMetricsReporter getTransitionMetricsReporter();
 
diff --git a/core/java/android/window/WindowOrganizer.java b/core/java/android/window/WindowOrganizer.java
index 2dc2cbc..5c5da49 100644
--- a/core/java/android/window/WindowOrganizer.java
+++ b/core/java/android/window/WindowOrganizer.java
@@ -155,7 +155,7 @@
      * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
-    public void registerTransitionPlayer(@Nullable ITransitionPlayer player) {
+    public void registerTransitionPlayer(@NonNull ITransitionPlayer player) {
         try {
             getWindowOrganizerController().registerTransitionPlayer(player);
         } catch (RemoteException e) {
@@ -164,6 +164,19 @@
     }
 
     /**
+     * Unregister a previously-registered ITransitionPlayer.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS)
+    public void unregisterTransitionPlayer(@NonNull ITransitionPlayer player) {
+        try {
+            getWindowOrganizerController().unregisterTransitionPlayer(player);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * @see TransitionMetrics
      * @hide
      */
diff --git a/core/java/com/android/internal/app/UnlaunchableAppActivity.java b/core/java/com/android/internal/app/UnlaunchableAppActivity.java
index 97f8084..9029685 100644
--- a/core/java/com/android/internal/app/UnlaunchableAppActivity.java
+++ b/core/java/com/android/internal/app/UnlaunchableAppActivity.java
@@ -68,6 +68,7 @@
         mTarget = intent.getParcelableExtra(Intent.EXTRA_INTENT,
                 android.content.IntentSender.class);
         String targetPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+        Log.i(TAG, "Unlaunchable activity for target package: " + targetPackageName);
         final UserManager userManager = UserManager.get(this);
 
         if (mUserId == UserHandle.USER_NULL) {
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index e6af64a..244165f 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -16,6 +16,10 @@
 
 package com.android.internal.os;
 
+import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_FINISH;
+import static android.os.BatteryStats.HistoryItem.EVENT_FLAG_START;
+import static android.os.BatteryStats.HistoryItem.EVENT_STATE_CHANGE;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.BatteryConsumer;
@@ -1449,6 +1453,21 @@
     }
 
     /**
+     * Records an event when some state flag changes to true.
+     */
+    public void recordStateStartEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags,
+            int uid, String name) {
+        synchronized (this) {
+            mHistoryCur.states |= stateFlags;
+            mHistoryCur.eventCode = EVENT_STATE_CHANGE | EVENT_FLAG_START;
+            mHistoryCur.eventTag = mHistoryCur.localEventTag;
+            mHistoryCur.eventTag.uid = uid;
+            mHistoryCur.eventTag.string = name;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
+    }
+
+    /**
      * Records an event when some state flag changes to false.
      */
     public void recordStateStopEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) {
@@ -1459,6 +1478,21 @@
     }
 
     /**
+     * Records an event when some state flag changes to false.
+     */
+    public void recordStateStopEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags,
+            int uid, String name) {
+        synchronized (this) {
+            mHistoryCur.states &= ~stateFlags;
+            mHistoryCur.eventCode = EVENT_STATE_CHANGE | EVENT_FLAG_FINISH;
+            mHistoryCur.eventTag = mHistoryCur.localEventTag;
+            mHistoryCur.eventTag.uid = uid;
+            mHistoryCur.eventTag.string = name;
+            writeHistoryItem(elapsedRealtimeMs, uptimeMs);
+        }
+    }
+
+    /**
      * Records an event when some state flags change to true and some to false.
      */
     public void recordStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int stateStartFlags,
diff --git a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
index 1494425..8fe1813 100644
--- a/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
+++ b/core/java/com/android/internal/ravenwood/RavenwoodEnvironment.java
@@ -27,6 +27,7 @@
     public static final String TAG = "RavenwoodEnvironment";
 
     private static RavenwoodEnvironment sInstance = new RavenwoodEnvironment();
+    private static Workaround sWorkaround = new Workaround();
 
     private RavenwoodEnvironment() {
         if (isRunningOnRavenwood()) {
@@ -76,4 +77,30 @@
     private boolean isRunningOnRavenwood$ravenwood() {
         return true;
     }
+
+    /**
+     * See {@link Workaround}. It's only usablke on Ravenwood.
+     */
+    public static Workaround workaround() {
+        if (getInstance().isRunningOnRavenwood()) {
+            return sWorkaround;
+        }
+        throw new IllegalStateException("Workaround can only be used on Ravenwood");
+    }
+
+    /**
+     * A set of APIs used to work around missing features on Ravenwood. Ideally, this class should
+     * be empty, and all its APIs should be able to be implemented properly.
+     */
+    public static class Workaround {
+        Workaround() {
+        }
+
+        /**
+         * @return whether the app's target SDK level is at least Q.
+         */
+        public boolean isTargetSdkAtLeastQ() {
+            return true;
+        }
+    }
 }
diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp
index 6f1c763..8f70268 100644
--- a/core/jni/android_database_SQLiteConnection.cpp
+++ b/core/jni/android_database_SQLiteConnection.cpp
@@ -436,7 +436,7 @@
     int result = SQLITE_OK;
     if (connection->tableQuery == nullptr) {
         static char const* sql =
-                "SELECT COUNT(*) FROM tables_used(?) WHERE schema != 'temp' AND wr != 0";
+                "SELECT NULL FROM tables_used(?) WHERE schema != 'temp' AND wr != 0";
         result = sqlite3_prepare_v2(connection->db, sql, -1, &connection->tableQuery, nullptr);
         if (result != SQLITE_OK) {
             ALOGE("failed to compile query table: %s",
@@ -447,25 +447,20 @@
 
     // A temporary, to simplify the code.
     sqlite3_stmt* query = connection->tableQuery;
-    sqlite3_reset(query);
-    sqlite3_clear_bindings(query);
     result = sqlite3_bind_text(query, 1, sqlite3_sql(statement), -1, SQLITE_STATIC);
     if (result != SQLITE_OK) {
         ALOGE("tables bind pointer returns %s", sqlite3_errstr(result));
-        return false;
     }
     result = sqlite3_step(query);
-    if (result != SQLITE_ROW) {
-        ALOGE("tables query error: %d/%s", result, sqlite3_errstr(result));
-        // Make sure the query is no longer bound to the statement.
-        sqlite3_clear_bindings(query);
-        return false;
-    }
-
-    int count = sqlite3_column_int(query, 0);
-    // Make sure the query is no longer bound to the statement SQL string.
+    // Make sure the query is no longer bound to the statement SQL string and
+    // that is no longer holding any table locks.
+    sqlite3_reset(query);
     sqlite3_clear_bindings(query);
-    return count == 0;
+
+    if (result != SQLITE_ROW && result != SQLITE_DONE) {
+        ALOGE("tables query error: %d/%s", result, sqlite3_errstr(result));
+    }
+    return result == SQLITE_DONE;
 }
 
 static jint nativeGetColumnCount(JNIEnv* env, jclass clazz, jlong connectionPtr,
diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp
index 2068bd7..3006e20 100644
--- a/core/jni/android_util_Binder.cpp
+++ b/core/jni/android_util_Binder.cpp
@@ -1411,8 +1411,10 @@
         return JNI_TRUE;
     }
 
-    env->CallStaticVoidMethod(gBinderOffsets.mClass, gBinderOffsets.mTransactionCallback, getpid(),
-                              code, flags, err);
+    if (err == FAILED_TRANSACTION) {
+        env->CallStaticVoidMethod(gBinderOffsets.mClass, gBinderOffsets.mTransactionCallback,
+                                  getpid(), code, flags, err);
+    }
 
     if (err == UNKNOWN_TRANSACTION) {
         return JNI_FALSE;
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 1aa635c..e831a7d 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -1008,11 +1008,11 @@
 }
 
 static void nativeSetTrustedOverlay(JNIEnv* env, jclass clazz, jlong transactionObj,
-                                    jlong nativeObject, jboolean isTrustedOverlay) {
+                                    jlong nativeObject, jint trustedOverlay) {
     auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj);
 
     SurfaceControl* const ctrl = reinterpret_cast<SurfaceControl *>(nativeObject);
-    transaction->setTrustedOverlay(ctrl, isTrustedOverlay);
+    transaction->setTrustedOverlay(ctrl, static_cast<gui::TrustedOverlay>(trustedOverlay));
 }
 
 static void nativeSetFrameRate(JNIEnv* env, jclass clazz, jlong transactionObj, jlong nativeObject,
@@ -2201,6 +2201,10 @@
     return jStalledTransactionInfo;
 }
 
+static void nativeNotifyShutdown() {
+    SurfaceComposerClient::notifyShutdown();
+}
+
 // ----------------------------------------------------------------------------
 
 SurfaceControl* android_view_SurfaceControl_getNativeSurfaceControl(JNIEnv* env,
@@ -2443,7 +2447,7 @@
             (void*)nativeSetTransformHint },
     {"nativeGetTransformHint", "(J)I",
             (void*)nativeGetTransformHint },
-    {"nativeSetTrustedOverlay", "(JJZ)V",
+    {"nativeSetTrustedOverlay", "(JJI)V",
             (void*)nativeSetTrustedOverlay },
     {"nativeGetLayerId", "(J)I",
             (void*)nativeGetLayerId },
@@ -2476,6 +2480,8 @@
             (void*) nativeGetStalledTransactionInfo },
     {"nativeSetDesiredPresentTimeNanos", "(JJ)V",
             (void*) nativeSetDesiredPresentTimeNanos },
+    {"nativeNotifyShutdown", "()V",
+            (void*)nativeNotifyShutdown },
         // clang-format on
 };
 
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 8541704..7b9235cd 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2291,6 +2291,11 @@
     <permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"
                 android:protectionLevel="signature|privileged" />
 
+    <!-- @hide Allows access to Thread network APIs or shell commands ("cmd thread_network") which
+        are only for testing. -->
+    <permission android:name="android.permission.THREAD_NETWORK_TESTING"
+                android:protectionLevel="signature" />
+
     <!-- #SystemApi @hide Allows an app to bypass Private DNS.
          <p>Not for use by third-party applications.
          TODO: publish as system API in next API release. -->
diff --git a/core/res/res/values-watch/themes_device_defaults.xml b/core/res/res/values-watch/themes_device_defaults.xml
index 9ad577a..85d34e2 100644
--- a/core/res/res/values-watch/themes_device_defaults.xml
+++ b/core/res/res/values-watch/themes_device_defaults.xml
@@ -133,6 +133,8 @@
         <item name="colorControlActivated">?attr/colorControlHighlight</item>
         <item name="listPreferredItemPaddingStart">?attr/dialogPreferredPadding</item>
         <item name="listPreferredItemPaddingEnd">?attr/dialogPreferredPadding</item>
+        <item name="iconfactoryIconSize">@dimen/resolver_icon_size</item>
+        <item name="iconfactoryBadgeSize">@dimen/resolver_badge_size</item>
     </style>
 
     <!-- Use a dark theme for watches. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 28cd210..0706b32 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1193,13 +1193,14 @@
     <!-- Allows activities to be launched on a long press on power during device setup. -->
     <bool name="config_allowStartActivityForLongPressOnPowerInSetup">false</bool>
 
-    <!-- Control the behavior when the user short presses the settings button.
-            0 - Nothing
+    <!-- Control the behavior when the user presses the settings button.
+            0 - Launch Settings activity
             1 - Launch notification panel
+            2 - Nothing
          This needs to match the constants in
          com/android/server/policy/PhoneWindowManager.java
     -->
-    <integer name="config_shortPressOnSettingsBehavior">0</integer>
+    <integer name="config_settingsKeyBehavior">0</integer>
 
     <!-- Control the behavior when the user short presses the power button.
             0 - Nothing
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index e420ffe..04f6f52 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -309,7 +309,7 @@
          fresh, it will be used as the current location by Telephony to decide whether satellite
          services should be allowed.
          -->
-    <integer name="config_oem_enabled_satellite_location_fresh_duration">600</integer>
+    <integer name="config_oem_enabled_satellite_location_fresh_duration">300</integer>
     <java-symbol type="integer" name="config_oem_enabled_satellite_location_fresh_duration" />
 
     <!-- Whether enhanced IWLAN handover check is enabled. If enabled, telephony frameworks
@@ -384,4 +384,23 @@
     <bool name="config_wait_for_device_alignment_in_demo_datagram">false</bool>
     <java-symbol type="bool" name="config_wait_for_device_alignment_in_demo_datagram" />
 
+    <!-- The time duration in millis after which Telephony will abort the last message datagram
+     sending requests. Telephony starts a timer when receiving a last message datagram sending
+     request in either OFF, IDLE, or NOT_CONNECTED state. In NOT_CONNECTED, the duration of the
+     timer is given by this config.
+     In OFF or IDLE state, the duration of the timer is the sum of this config and the
+     config_satellite_modem_image_switching_duration_millis.
+     -->
+    <integer name="config_datagram_wait_for_connected_state_for_last_message_timeout_millis">60000</integer>
+    <java-symbol type="integer" name="config_datagram_wait_for_connected_state_for_last_message_timeout_millis" />
+
+    <!-- The time duration in millis after which Telephony will abort the last message datagram
+     sending requests and send failure response to the client that has requested sending the
+     datagrams. Telephony starts a timer after pushing down the last message datagram sending
+     request to modem. Before expiry, the timer will be stopped when Telephony receives the response
+     for the sending request from modem.
+     -->
+    <integer name="config_wait_for_datagram_sending_response_for_last_message_timeout_millis">60000</integer>
+    <java-symbol type="integer" name="config_wait_for_datagram_sending_response_for_last_message_timeout_millis" />
+
 </resources>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index b885e03..a0807ca 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -72,6 +72,9 @@
     <!-- The default margin used in immersive mode to capture the start of a swipe gesture from the
          edge of the screen to show the system bars. -->
     <dimen name="system_gestures_start_threshold">24dp</dimen>
+    <!-- The minimum swipe gesture distance for showing the system bars when in immersive mode. This
+         swipe must be within the specified system_gestures_start_threshold area. -->
+    <dimen name="system_gestures_distance_threshold">24dp</dimen>
 
     <!-- Height of the bottom navigation bar frame; this is different than navigation_bar_height
          where that is the height reported to all the other windows to resize themselves around the
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 9f1b7ae..bb73934 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1802,6 +1802,7 @@
   <java-symbol type="dimen" name="taskbar_frame_height" />
   <java-symbol type="dimen" name="status_bar_height" />
   <java-symbol type="dimen" name="display_cutout_touchable_region_size" />
+  <java-symbol type="dimen" name="system_gestures_distance_threshold" />
   <java-symbol type="dimen" name="system_gestures_start_threshold" />
   <java-symbol type="dimen" name="quick_qs_offset_height" />
   <java-symbol type="drawable" name="ic_jog_dial_sound_off" />
@@ -1858,7 +1859,7 @@
   <java-symbol type="integer" name="config_lidNavigationAccessibility" />
   <java-symbol type="integer" name="config_lidOpenRotation" />
   <java-symbol type="integer" name="config_longPressOnHomeBehavior" />
-  <java-symbol type="integer" name="config_shortPressOnSettingsBehavior" />
+  <java-symbol type="integer" name="config_settingsKeyBehavior" />
   <java-symbol type="layout" name="global_actions" />
   <java-symbol type="layout" name="global_actions_item" />
   <java-symbol type="layout" name="global_actions_silent_mode" />
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
index e118c98d..c4695d9 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
@@ -375,6 +375,8 @@
                 assertEquals(3, s.getColumnInt(0));
             }
 
+            mDatabase.execSQL("DROP TABLE t1");
+
         } catch (SQLiteException e) {
             allowed = false;
         } finally {
diff --git a/core/tests/coretests/src/android/view/ContentRecordingSessionTest.java b/core/tests/coretests/src/android/view/ContentRecordingSessionTest.java
index 17980ac..f8800cb 100644
--- a/core/tests/coretests/src/android/view/ContentRecordingSessionTest.java
+++ b/core/tests/coretests/src/android/view/ContentRecordingSessionTest.java
@@ -18,6 +18,7 @@
 
 import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
 import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
+import static android.view.ContentRecordingSession.TASK_ID_UNKNOWN;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 
@@ -45,6 +46,7 @@
 @Presubmit
 public class ContentRecordingSessionTest {
     private static final int DISPLAY_ID = 1;
+    private static final int TASK_ID = 123;
     private static final IBinder WINDOW_TOKEN = new Binder("DisplayContentWindowToken");
 
     @Test
@@ -65,6 +67,16 @@
         ContentRecordingSession session = ContentRecordingSession.createTaskSession(WINDOW_TOKEN);
         assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_TASK);
         assertThat(session.getTokenToRecord()).isEqualTo(WINDOW_TOKEN);
+        assertThat(session.getTaskId()).isEqualTo(TASK_ID_UNKNOWN);
+    }
+
+    @Test
+    public void testSecondaryTaskConstructor() {
+        ContentRecordingSession session =
+                ContentRecordingSession.createTaskSession(WINDOW_TOKEN, TASK_ID);
+        assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_TASK);
+        assertThat(session.getTokenToRecord()).isEqualTo(WINDOW_TOKEN);
+        assertThat(session.getTaskId()).isEqualTo(TASK_ID);
     }
 
     @Test
@@ -73,6 +85,7 @@
                 DEFAULT_DISPLAY);
         assertThat(session.getContentToRecord()).isEqualTo(RECORD_CONTENT_DISPLAY);
         assertThat(session.getTokenToRecord()).isNull();
+        assertThat(session.getTaskId()).isEqualTo(TASK_ID_UNKNOWN);
     }
 
     @Test
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyAndException/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyAndException/AndroidManifest.xml
index 78c8881..297c490 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyAndException/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyAndException/AndroidManifest.xml
@@ -5,7 +5,7 @@
      android:versionCode="1"
      android:versionName="1.0">
 
-    <uses-sdk android:minSdkVersion="8"
+    <uses-sdk android:minSdkVersion="21"
          android:targetSdkVersion="18"/>
 
     <application android:name="com.android.multidexlegacyandexception.TestApplication"
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestApp/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyTestApp/AndroidManifest.xml
index 1a60c1e..a208268 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestApp/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestApp/AndroidManifest.xml
@@ -5,7 +5,7 @@
      android:versionCode="1"
      android:versionName="1.0">
 
-    <uses-sdk android:minSdkVersion="8"
+    <uses-sdk android:minSdkVersion="21"
          android:targetSdkVersion="18"/>
 
     <application android:name=".TestApplication"
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestAppTests/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyTestAppTests/AndroidManifest.xml
index 35369c7..bb2a201 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestAppTests/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestAppTests/AndroidManifest.xml
@@ -4,7 +4,7 @@
     android:versionCode="1"
     android:versionName="1.0" >
 
-    <uses-sdk android:minSdkVersion="8" />
+    <uses-sdk android:minSdkVersion="21" />
     <instrumentation
         android:name="com.android.test.runner.MultiDexTestRunner"
         android:targetPackage="com.android.multidexlegacytestapp" />
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestAppTests2/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyTestAppTests2/AndroidManifest.xml
index 1cadfcd..b96566c 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestAppTests2/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestAppTests2/AndroidManifest.xml
@@ -4,7 +4,7 @@
     android:versionCode="1"
     android:versionName="1.0" >
 
-    <uses-sdk android:minSdkVersion="8" />
+    <uses-sdk android:minSdkVersion="21" />
     <instrumentation
         android:name="com.android.multidexlegacytestapp.test2.MultiDexAndroidJUnitRunner"
         android:targetPackage="com.android.multidexlegacytestapp" />
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestAppWithCorruptedDex/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyTestAppWithCorruptedDex/AndroidManifest.xml
index 840daab..3ad61ca 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestAppWithCorruptedDex/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestAppWithCorruptedDex/AndroidManifest.xml
@@ -5,7 +5,7 @@
      android:versionCode="1"
      android:versionName="1.0">
 
-    <uses-sdk android:minSdkVersion="19"
+    <uses-sdk android:minSdkVersion="21"
          android:targetSdkVersion="19"/>
 
     <application android:name="androidx.multidex.MultiDexApplication"
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests/AndroidManifest.xml
index e2fba4e..c644c36 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests/AndroidManifest.xml
@@ -4,7 +4,7 @@
     android:versionCode="1"
     android:versionName="1.0" >
 
-    <uses-sdk android:minSdkVersion="9" />
+    <uses-sdk android:minSdkVersion="21" />
     <instrumentation
         android:name="android.test.InstrumentationTestRunner"
         android:targetPackage="com.android.framework.multidexlegacytestservices" />
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/AndroidManifest.xml
index 01285e7..f511c5f 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyTestServicesTests2/AndroidManifest.xml
@@ -4,7 +4,7 @@
     android:versionCode="1"
     android:versionName="1.0" >
 
-    <uses-sdk android:minSdkVersion="9" />
+    <uses-sdk android:minSdkVersion="21" />
     <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>
     <instrumentation
         android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v1/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v1/AndroidManifest.xml
index 8c911c4..4730243 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v1/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v1/AndroidManifest.xml
@@ -5,7 +5,7 @@
      android:versionCode="1"
      android:versionName="1.0">
 
-    <uses-sdk android:minSdkVersion="9"
+    <uses-sdk android:minSdkVersion="21"
          android:targetSdkVersion="18"/>
 
     <application android:name="androidx.multidex.MultiDexApplication"
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v2/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v2/AndroidManifest.xml
index 1817e95..0bcf9fe 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v2/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v2/AndroidManifest.xml
@@ -5,7 +5,7 @@
      android:versionCode="2"
      android:versionName="2.0">
 
-    <uses-sdk android:minSdkVersion="9"
+    <uses-sdk android:minSdkVersion="21"
          android:targetSdkVersion="18"/>
 
     <application android:name="androidx.multidex.MultiDexApplication"
diff --git a/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v3/AndroidManifest.xml b/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v3/AndroidManifest.xml
index c8a41bc..5b7680d 100644
--- a/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v3/AndroidManifest.xml
+++ b/core/tests/hosttests/test-apps/MultiDexLegacyVersionedTestApp_v3/AndroidManifest.xml
@@ -5,7 +5,7 @@
      android:versionCode="3"
      android:versionName="3.0">
 
-    <uses-sdk android:minSdkVersion="9"
+    <uses-sdk android:minSdkVersion="21"
          android:targetSdkVersion="18"/>
 
     <application android:name="androidx.multidex.MultiDexApplication"
diff --git a/data/etc/core.protolog.pb b/data/etc/core.protolog.pb
index b41a607..ddb706e 100644
--- a/data/etc/core.protolog.pb
+++ b/data/etc/core.protolog.pb
Binary files differ
diff --git a/data/etc/preinstalled-packages-platform.xml b/data/etc/preinstalled-packages-platform.xml
index 7823277..f9fb84d 100644
--- a/data/etc/preinstalled-packages-platform.xml
+++ b/data/etc/preinstalled-packages-platform.xml
@@ -134,4 +134,19 @@
     <install-in-user-type package="com.android.avatarpicker">
         <install-in user-type="FULL" />
     </install-in-user-type>
+
+    <!-- AiLabs Warp app pre-installed in hardware/google/pixel/common/pixel-common-device.mk -->
+    <install-in-user-type package="com.google.android.apps.warp">
+        <install-in user-type="FULL" />
+        <install-in user-type="PROFILE" />
+        <do-not-install-in user-type="android.os.usertype.profile.PRIVATE" />
+    </install-in-user-type>
+
+    <!-- Google Home app pre-installed on tangor devices in vendor/google/products/tangor_common.mk
+     -->
+    <install-in-user-type package="com.google.android.apps.chromecast.app">
+        <install-in user-type="FULL" />
+        <install-in user-type="PROFILE" />
+        <do-not-install-in user-type="android.os.usertype.profile.PRIVATE" />
+    </install-in-user-type>
 </config>
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 500e9b7..80f143c 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -3223,6 +3223,36 @@
       "group": "WM_DEBUG_WINDOW_TRANSITIONS",
       "at": "com\/android\/server\/wm\/TransitionController.java"
     },
+    "-4546322749928357965": {
+      "message": "Registering transition player %s over %d other players",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS_MIN",
+      "at": "com\/android\/server\/wm\/TransitionController.java"
+    },
+    "-4250307779892136611": {
+      "message": "Registering transition player %s ",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS_MIN",
+      "at": "com\/android\/server\/wm\/TransitionController.java"
+    },
+    "3242771541905259983": {
+      "message": "Attempt to unregister transition player %s but it isn't registered",
+      "level": "WARN",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS_MIN",
+      "at": "com\/android\/server\/wm\/TransitionController.java"
+    },
+    "3691912781236221027": {
+      "message": "Unregistering active transition player %s at index=%d leaving %d in stack",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS_MIN",
+      "at": "com\/android\/server\/wm\/TransitionController.java"
+    },
+    "-2879980134100946679": {
+      "message": "Unregistering transition player %s at index=%d leaving %d in stack",
+      "level": "VERBOSE",
+      "group": "WM_DEBUG_WINDOW_TRANSITIONS_MIN",
+      "at": "com\/android\/server\/wm\/TransitionController.java"
+    },
     "-4235778637051052061": {
       "message": "Disabling player for transition #%d because display isn't enabled yet",
       "level": "VERBOSE",
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 67f1650..08e695b 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -92,3 +92,10 @@
     description: "Enables Taskbar on phones"
     bug: "341784466"
 }
+
+flag {
+    name: "enable_bubble_anything"
+    namespace: "multitasking"
+    description: "Enable UI affordances to put other content into a bubble"
+    bug: "342245211"
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
index 35a4a62..0efdbdc 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
@@ -23,6 +23,7 @@
 import android.graphics.Color
 import android.graphics.drawable.Icon
 import android.os.UserHandle
+import android.platform.test.flag.junit.SetFlagsRule
 import android.view.IWindowManager
 import android.view.WindowManager
 import android.view.WindowManagerGlobal
@@ -33,6 +34,7 @@
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.internal.protolog.common.ProtoLog
 import com.android.launcher3.icons.BubbleIconFactory
+import com.android.wm.shell.Flags
 import com.android.wm.shell.R
 import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
 import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix
@@ -44,19 +46,24 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.util.concurrent.MoreExecutors.directExecutor
 import org.junit.After
-import java.util.concurrent.Semaphore
-import java.util.concurrent.TimeUnit
-import java.util.function.Consumer
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.mock
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
 
 /** Unit tests for [BubbleStackView]. */
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class BubbleStackViewTest {
 
+    @get:Rule val setFlagsRule = SetFlagsRule()
+
     private val context = ApplicationProvider.getApplicationContext<Context>()
     private lateinit var positioner: BubblePositioner
     private lateinit var iconFactory: BubbleIconFactory
@@ -66,6 +73,8 @@
     private lateinit var windowManager: IWindowManager
     private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory
     private lateinit var bubbleData: BubbleData
+    private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager
+    private var sysuiProxy = mock<SysuiProxy>()
 
     @Before
     fun setUp() {
@@ -86,7 +95,6 @@
                 )
             )
         positioner = BubblePositioner(context, windowManager)
-        val bubbleStackViewManager = FakeBubbleStackViewManager()
         bubbleData =
             BubbleData(
                 context,
@@ -95,8 +103,7 @@
                 BubbleEducationController(context),
                 shellExecutor
             )
-
-        val sysuiProxy = mock<SysuiProxy>()
+        bubbleStackViewManager = FakeBubbleStackViewManager()
         expandedViewManager = FakeBubbleExpandedViewManager()
         bubbleTaskViewFactory = FakeBubbleTaskViewFactory()
         bubbleStackView =
@@ -234,6 +241,115 @@
             .inOrder()
     }
 
+    @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+    @Test
+    fun testCreateStackView_noOverflowContents_noOverflow() {
+        bubbleStackView =
+                BubbleStackView(
+                        context,
+                        bubbleStackViewManager,
+                        positioner,
+                        bubbleData,
+                        null,
+                        FloatingContentCoordinator(),
+                        { sysuiProxy },
+                        shellExecutor
+                )
+
+        assertThat(bubbleData.overflowBubbles).isEmpty()
+        val bubbleOverflow = bubbleData.overflow
+        // Overflow shouldn't be attached
+        assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1)
+    }
+
+    @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+    @Test
+    fun testCreateStackView_hasOverflowContents_hasOverflow() {
+        // Add a bubble to the overflow
+        val bubble1 = createAndInflateChatBubble(key = "bubble1")
+        bubbleData.notificationEntryUpdated(bubble1, false, false)
+        bubbleData.dismissBubbleWithKey(bubble1.key, Bubbles.DISMISS_USER_GESTURE)
+        assertThat(bubbleData.overflowBubbles).isNotEmpty()
+
+        bubbleStackView =
+                BubbleStackView(
+                        context,
+                        bubbleStackViewManager,
+                        positioner,
+                        bubbleData,
+                        null,
+                        FloatingContentCoordinator(),
+                        { sysuiProxy },
+                        shellExecutor
+                )
+        val bubbleOverflow = bubbleData.overflow
+        assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
+    }
+
+    @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+    @Test
+    fun testCreateStackView_noOverflowContents_hasOverflow() {
+        bubbleStackView =
+                BubbleStackView(
+                        context,
+                        bubbleStackViewManager,
+                        positioner,
+                        bubbleData,
+                        null,
+                        FloatingContentCoordinator(),
+                        { sysuiProxy },
+                        shellExecutor
+                )
+
+        assertThat(bubbleData.overflowBubbles).isEmpty()
+        val bubbleOverflow = bubbleData.overflow
+        assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
+    }
+
+    @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+    @Test
+    fun showOverflow_true() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleStackView.showOverflow(true)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+        val bubbleOverflow = bubbleData.overflow
+        assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
+    }
+
+    @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+    @Test
+    fun showOverflow_false() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleStackView.showOverflow(true)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+        val bubbleOverflow = bubbleData.overflow
+        assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleStackView.showOverflow(false)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+        // The overflow should've been removed
+        assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1)
+    }
+
+    @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+    @Test
+    fun showOverflow_ignored() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            bubbleStackView.showOverflow(false)
+        }
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+        // showOverflow should've been ignored, so the overflow would be attached
+        val bubbleOverflow = bubbleData.overflow
+        assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
+    }
+
     private fun createAndInflateChatBubble(key: String): Bubble {
         val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button)
         val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build()
diff --git a/libs/WindowManager/Shell/res/drawable/circular_progress.xml b/libs/WindowManager/Shell/res/drawable/circular_progress.xml
index 294b1f0..0d64527 100644
--- a/libs/WindowManager/Shell/res/drawable/circular_progress.xml
+++ b/libs/WindowManager/Shell/res/drawable/circular_progress.xml
@@ -24,7 +24,7 @@
             android:toDegrees="275">
             <shape
                 android:shape="ring"
-                android:thickness="3dp"
+                android:thickness="2dp"
                 android:innerRadius="14dp"
                 android:useLevel="true">
             </shape>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml
index 5d9fe67..9566f2f 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml
@@ -15,7 +15,8 @@
   ~ limitations under the License.
   -->
 <shape android:shape="rectangle"
-    xmlns:android="http://schemas.android.com/apk/res/android">
-    <solid android:color="@android:color/white" />
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+    <solid android:color="?androidprv:attr/materialColorSurfaceContainerLow" />
     <corners android:radius="@dimen/desktop_mode_maximize_menu_corner_radius" />
 </shape>
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
index 4d06dd3..84e1449 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
@@ -28,13 +28,11 @@
     <LinearLayout
         android:id="@+id/open_menu_button"
         android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:tint="?androidprv:attr/materialColorOnSurface"
-        android:background="?android:selectableItemBackground"
+        android:layout_height="40dp"
         android:orientation="horizontal"
         android:clickable="true"
         android:focusable="true"
-        android:paddingStart="12dp">
+        android:layout_marginStart="12dp">
         <ImageView
             android:id="@+id/application_icon"
             android:layout_width="@dimen/desktop_mode_caption_icon_radius"
@@ -85,8 +83,6 @@
         android:layout_height="40dp"
         android:layout_gravity="end"
         android:layout_marginHorizontal="8dp"
-        android:paddingHorizontal="5dp"
-        android:paddingVertical="3dp"
         android:clickable="true"
         android:focusable="true"/>
 
@@ -97,7 +93,6 @@
         android:paddingHorizontal="10dp"
         android:paddingVertical="8dp"
         android:layout_marginEnd="8dp"
-        android:background="?android:selectableItemBackgroundBorderless"
         android:contentDescription="@string/close_button_text"
         android:src="@drawable/desktop_mode_header_ic_close"
         android:scaleType="centerCrop"
diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
index 77507a4..cf1b894 100644
--- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
+++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
@@ -16,20 +16,28 @@
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
-    <ProgressBar
-        android:id="@+id/progress_bar"
-        style="?android:attr/progressBarStyleHorizontal"
-        android:progressDrawable="@drawable/circular_progress"
-        android:layout_width="34dp"
-        android:layout_height="34dp"
-        android:indeterminate="false"
-        android:visibility="invisible"/>
+
+    <FrameLayout
+        android:layout_width="44dp"
+        android:layout_height="40dp">
+        <ProgressBar
+            android:id="@+id/progress_bar"
+            style="?android:attr/progressBarStyleHorizontal"
+            android:progressDrawable="@drawable/circular_progress"
+            android:layout_width="32dp"
+            android:layout_height="32dp"
+            android:indeterminate="false"
+            android:layout_marginHorizontal="6dp"
+            android:layout_marginVertical="4dp"
+            android:visibility="invisible"/>
+    </FrameLayout>
 
     <ImageButton
         android:id="@+id/maximize_window"
-        android:layout_width="34dp"
-        android:layout_height="34dp"
-        android:padding="5dp"
+        android:layout_width="44dp"
+        android:layout_height="40dp"
+        android:paddingHorizontal="10dp"
+        android:paddingVertical="8dp"
         android:contentDescription="@string/maximize_button_text"
         android:src="@drawable/decor_desktop_mode_maximize_button_dark"
         android:scaleType="fitCenter" />
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 8bc3004..f27f46c 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -553,6 +553,25 @@
     <!-- The corner radius of a task that was dragged from fullscreen. -->
     <dimen name="desktop_mode_dragged_task_radius">28dp</dimen>
 
+    <!-- The corner radius of the app chip, maximize and close button's ripple drawable -->
+    <dimen name="desktop_mode_header_buttons_ripple_radius">16dp</dimen>
+    <!-- The vertical inset to apply to the app chip's ripple drawable -->
+    <dimen name="desktop_mode_header_app_chip_ripple_inset_vertical">4dp</dimen>
+
+    <!-- The corner radius of the maximize button's ripple drawable -->
+    <dimen name="desktop_mode_header_maximize_ripple_radius">18dp</dimen>
+    <!-- The vertical inset to apply to the maximize button's ripple drawable -->
+    <dimen name="desktop_mode_header_maximize_ripple_inset_vertical">4dp</dimen>
+    <!-- The horizontal inset to apply to the maximize button's ripple drawable -->
+    <dimen name="desktop_mode_header_maximize_ripple_inset_horizontal">6dp</dimen>
+
+    <!-- The corner radius of the close button's ripple drawable -->
+    <dimen name="desktop_mode_header_close_ripple_radius">18dp</dimen>
+    <!-- The vertical inset to apply to the close button's ripple drawable -->
+    <dimen name="desktop_mode_header_close_ripple_inset_vertical">4dp</dimen>
+    <!-- The horizontal inset to apply to the close button's ripple drawable -->
+    <dimen name="desktop_mode_header_close_ripple_inset_horizontal">6dp</dimen>
+
     <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) -->
     <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item>
     <!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) -->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 3244837..f2095b1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -552,10 +552,12 @@
                 // Notify the compat UI if the listener or task info changed.
                 notifyCompatUI(taskInfo, newListener);
             }
-            if (data.getTaskInfo().getWindowingMode() != taskInfo.getWindowingMode()) {
-                // Notify the recent tasks when a task changes windowing modes
+            final boolean windowModeChanged =
+                    data.getTaskInfo().getWindowingMode() != taskInfo.getWindowingMode();
+            final boolean visibilityChanged = data.getTaskInfo().isVisible != taskInfo.isVisible;
+            if (windowModeChanged || visibilityChanged) {
                 mRecentTasks.ifPresent(recentTasks ->
-                        recentTasks.onTaskWindowingModeChanged(taskInfo));
+                        recentTasks.onTaskRunningInfoChanged(taskInfo));
             }
             // TODO (b/207687679): Remove check for HOME once bug is fixed
             final boolean isFocusedOrHome = taskInfo.isFocused
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java
index 7749394..d754d04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java
@@ -19,8 +19,6 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
 
-import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD;
-
 import android.annotation.NonNull;
 import android.graphics.Color;
 import android.graphics.Rect;
@@ -45,6 +43,7 @@
     private boolean mIsRequestingStatusBarAppearance;
     private boolean mBackgroundIsDark;
     private Rect mStartBounds;
+    private int mStatusbarHeight;
 
     public BackAnimationBackground(RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
         mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
@@ -56,9 +55,10 @@
      * @param startRect The start bounds of the closing target.
      * @param color The background color.
      * @param transaction The animation transaction.
+     * @param statusbarHeight The height of the statusbar (in px).
      */
-    public void ensureBackground(
-            Rect startRect, int color, @NonNull SurfaceControl.Transaction transaction) {
+    public void ensureBackground(Rect startRect, int color,
+            @NonNull SurfaceControl.Transaction transaction, int statusbarHeight) {
         if (mBackgroundSurface != null) {
             return;
         }
@@ -80,6 +80,7 @@
                 .show(mBackgroundSurface);
         mStartBounds = startRect;
         mIsRequestingStatusBarAppearance = false;
+        mStatusbarHeight = statusbarHeight;
     }
 
     /**
@@ -111,14 +112,14 @@
     /**
      * Update back animation background with for the progress.
      *
-     * @param progress Progress value from {@link android.window.BackProgressAnimator}
+     * @param top The top coordinate of the closing target
      */
-    public void onBackProgressed(float progress) {
+    public void customizeStatusBarAppearance(int top) {
         if (mCustomizer == null || mStartBounds.isEmpty()) {
             return;
         }
 
-        final boolean shouldCustomizeSystemBar = progress > UPDATE_SYSUI_FLAGS_THRESHOLD;
+        final boolean shouldCustomizeSystemBar = top > mStatusbarHeight / 2;
         if (shouldCustomizeSystemBar == mIsRequestingStatusBarAppearance) {
             return;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
index 7c0837e..ee740fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -46,6 +46,7 @@
 import com.android.internal.dynamicanimation.animation.SpringForce
 import com.android.internal.jank.Cuj
 import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.internal.policy.SystemBarUtils
 import com.android.internal.protolog.common.ProtoLog
 import com.android.wm.shell.R
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
@@ -76,6 +77,7 @@
     private val tempRectF = RectF()
 
     private var cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+    private var statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
 
     private val backAnimationRunner =
         BackAnimationRunner(Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY)
@@ -129,6 +131,11 @@
     abstract fun preparePreCommitEnteringRectMovement()
 
     /**
+     * Subclasses must provide a duration (in ms) for the post-commit part of the animation
+     */
+    abstract fun getPostCommitAnimationDuration(): Long
+
+    /**
      * Returns a base transformation to apply to the entering target during pre-commit. The system
      * will apply the default animation on top of it.
      */
@@ -137,6 +144,7 @@
 
     override fun onConfigurationChanged(newConfiguration: Configuration) {
         cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+        statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
     }
 
     override fun getRunner() = backAnimationRunner
@@ -182,7 +190,8 @@
         background.ensureBackground(
             closingTarget!!.windowConfiguration.bounds,
             getBackgroundColor(),
-            transaction
+            transaction,
+            statusbarHeight
         )
         ensureScrimLayer()
         if (isLetterboxed && enteringHasSameLetterbox) {
@@ -203,7 +212,6 @@
 
     private fun onGestureProgress(backEvent: BackEvent) {
         val progress = gestureInterpolator.getInterpolation(backEvent.progress)
-        background.onBackProgressed(progress)
         currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
         val yOffset = getYOffset(currentClosingRect, backEvent.touchY)
         currentClosingRect.offset(0f, yOffset)
@@ -218,6 +226,7 @@
             enteringTransformation
         )
         applyTransaction()
+        background.customizeStatusBarAppearance(currentClosingRect.top.toInt())
     }
 
     private fun getYOffset(centeredRect: RectF, touchY: Float): Float {
@@ -255,7 +264,8 @@
             .setSpring(postCommitFlingSpring)
         flingAnimation.start()
 
-        val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(POST_COMMIT_DURATION)
+        val valueAnimator =
+            ValueAnimator.ofFloat(1f, 0f).setDuration(getPostCommitAnimationDuration())
         valueAnimator.addUpdateListener { animation: ValueAnimator ->
             val progress = animation.animatedFraction
             onPostCommitProgress(progress)
@@ -518,7 +528,6 @@
         internal const val MAX_SCALE = 0.9f
         private const val MAX_SCRIM_ALPHA_DARK = 0.8f
         private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f
-        private const val POST_COMMIT_DURATION = 300L
         private const val SPRING_SCALE = 100f
         private const val MAX_FLING_SCALE = 0.6f
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index ee898a7..381914a5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -48,6 +48,7 @@
 import android.window.IOnBackInvokedCallback;
 
 import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.policy.SystemBarUtils;
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.Interpolators;
@@ -82,6 +83,7 @@
 
     private final Rect mStartTaskRect = new Rect();
     private float mCornerRadius;
+    private int mStatusbarHeight;
 
     // The closing window properties.
     private final Rect mClosingStartRect = new Rect();
@@ -114,16 +116,21 @@
 
     @Inject
     public CrossTaskBackAnimation(Context context, BackAnimationBackground background) {
-        mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
         mBackAnimationRunner = new BackAnimationRunner(
                 new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_TASK);
         mBackground = background;
         mContext = context;
+        loadResources();
     }
 
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
+        loadResources();
+    }
+
+    private void loadResources() {
         mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+        mStatusbarHeight = SystemBarUtils.getStatusBarHeight(mContext);
     }
 
     private static float mapRange(float value, float min, float max) {
@@ -149,7 +156,7 @@
 
         // Draw background.
         mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
-                BACKGROUNDCOLOR, mTransaction);
+                BACKGROUNDCOLOR, mTransaction, mStatusbarHeight);
         mInterWindowMargin = mContext.getResources()
                 .getDimension(R.dimen.cross_task_back_inter_window_margin);
         mVerticalMargin = mContext.getResources()
@@ -201,7 +208,7 @@
         applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
         applyTransaction();
 
-        mBackground.onBackProgressed(progress);
+        mBackground.customizeStatusBarAppearance((int) scaledTop);
     }
 
     private void updatePostCommitClosingAnimation(float progress) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
index ab359bd..c4aafea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
@@ -33,6 +33,8 @@
 import com.android.wm.shell.protolog.ShellProtoLogGroup
 import com.android.wm.shell.shared.annotations.ShellMainThread
 import javax.inject.Inject
+import kotlin.math.max
+import kotlin.math.min
 
 /** Class that handles customized predictive cross activity back animations. */
 @ShellMainThread
@@ -96,6 +98,12 @@
         targetEnteringRect.set(startClosingRect)
     }
 
+    override fun getPostCommitAnimationDuration(): Long {
+        return min(
+            MAX_POST_COMMIT_ANIM_DURATION, max(closeAnimation!!.duration, enterAnimation!!.duration)
+        )
+    }
+
     override fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation {
         gestureProgress = progress
         transformation.clear()
@@ -107,9 +115,9 @@
         super.startBackAnimation(backMotionEvent)
         if (
             closeAnimation == null ||
-                enterAnimation == null ||
-                closingTarget == null ||
-                enteringTarget == null
+            enterAnimation == null ||
+            closingTarget == null ||
+            enteringTarget == null
         ) {
             ProtoLog.d(
                 ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
@@ -125,10 +133,13 @@
         super.onPostCommitProgress(linearProgress)
         if (closingTarget == null || enteringTarget == null) return
 
-        // TODO: Should we use the duration from the custom xml spec for the post-commit animation?
-        applyTransform(closingTarget!!.leash, currentClosingRect, linearProgress, closeAnimation!!)
-        val enteringProgress =
-            MathUtils.lerp(gestureProgress * PRE_COMMIT_MAX_PROGRESS, 1f, linearProgress)
+        val closingProgress = closeAnimation!!.getPostCommitProgress(linearProgress)
+        applyTransform(closingTarget!!.leash, currentClosingRect, closingProgress, closeAnimation!!)
+        val enteringProgress = MathUtils.lerp(
+            gestureProgress * PRE_COMMIT_MAX_PROGRESS,
+            1f,
+            enterAnimation!!.getPostCommitProgress(linearProgress)
+        )
         applyTransform(
             enteringTarget!!.leash,
             currentEnteringRect,
@@ -175,6 +186,19 @@
         return false
     }
 
+    private fun Animation.getPostCommitProgress(linearProgress: Float): Float {
+        return when (duration) {
+            0L -> 1f
+            else -> min(
+                1f,
+                getPostCommitAnimationDuration() / min(
+                    MAX_POST_COMMIT_ANIM_DURATION,
+                    duration
+                ).toFloat() * linearProgress
+            )
+        }
+    }
+
     class AnimationLoadResult {
         var closeAnimation: Animation? = null
         var enterAnimation: Animation? = null
@@ -183,6 +207,7 @@
 
     companion object {
         private const val PRE_COMMIT_MAX_PROGRESS = 0.2f
+        private const val MAX_POST_COMMIT_ANIM_DURATION = 2000L
     }
 }
 
@@ -226,7 +251,7 @@
         // Try to get animation from Activity#overrideActivityTransition
         if (
             enterAnimation && animationInfo.customEnterAnim != 0 ||
-                !enterAnimation && animationInfo.customExitAnim != 0
+            !enterAnimation && animationInfo.customExitAnim != 0
         ) {
             a =
                 transitionAnimation.loadAppTransitionAnimation(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
index 9f07e5b..44752fe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
@@ -71,6 +71,8 @@
         targetEnteringRect.scaleCentered(MAX_SCALE)
     }
 
+    override fun getPostCommitAnimationDuration() = POST_COMMIT_DURATION
+
     override fun onGestureCommitted(velocity: Float) {
         // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
         // coordinate of the gesture driven phase. Let's update the start and target rects and kick
@@ -93,4 +95,9 @@
         applyTransform(enteringTarget?.leash, currentEnteringRect, 1f)
         applyTransaction()
     }
+
+
+    companion object {
+        private const val POST_COMMIT_DURATION = 300L
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 42a4ab2..317e00a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1772,7 +1772,7 @@
         if (groupKey == null) {
             return bubbleChildren;
         }
-        for (Bubble bubble : mBubbleData.getActiveBubbles()) {
+        for (Bubble bubble : mBubbleData.getBubbles()) {
             if (bubble.getGroupKey() != null && groupKey.equals(bubble.getGroupKey())) {
                 bubbleChildren.add(bubble);
             }
@@ -2228,6 +2228,7 @@
         pw.print(prefix); pw.println("  currentUserId= " + mCurrentUserId);
         pw.print(prefix); pw.println("  isStatusBarShade= " + mIsStatusBarShade);
         pw.print(prefix); pw.println("  isShowingAsBubbleBar= " + isShowingAsBubbleBar());
+        pw.print(prefix); pw.println("  isImeVisible= " + mBubblePositioner.isImeVisible());
         pw.println();
 
         mBubbleData.dump(pw);
@@ -2730,7 +2731,7 @@
         public boolean canShowBubbleNotification() {
             // in bubble bar mode, when the IME is visible we can't animate new bubbles.
             if (BubbleController.this.isShowingAsBubbleBar()) {
-                return !BubbleController.this.mBubblePositioner.getIsImeVisible();
+                return !BubbleController.this.mBubblePositioner.isImeVisible();
             }
             return true;
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index 26483c8..874102c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -339,11 +339,6 @@
         return mOverflow;
     }
 
-    /** Return a read-only current active bubble lists. */
-    public List<Bubble> getActiveBubbles() {
-        return Collections.unmodifiableList(mBubbles);
-    }
-
     public void setExpanded(boolean expanded) {
         setExpandedInternal(expanded);
         dispatchPendingChanges();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 1e482ca..2382545 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -325,7 +325,7 @@
     }
 
     /** Returns whether the IME is visible. */
-    public boolean getIsImeVisible() {
+    public boolean isImeVisible() {
         return mImeVisible;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 69bf5fd..fac9bf6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -344,7 +344,7 @@
             pw.println("Expanded bubble state:");
             pw.println("  expandedBubbleKey: " + mExpandedBubble.getKey());
 
-            final BubbleExpandedView expandedView = mExpandedBubble.getExpandedView();
+            final BubbleExpandedView expandedView = getExpandedView();
 
             if (expandedView != null) {
                 pw.println("  expandedViewVis:    " + expandedView.getVisibility());
@@ -815,10 +815,11 @@
 
         private float getScrimAlphaForDrag(float dragAmount) {
             // dragAmount should be negative as we allow scroll up only
-            if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+            BubbleExpandedView expandedView = getExpandedView();
+            if (expandedView != null) {
                 float alphaRange = BUBBLE_EXPANDED_SCRIM_ALPHA - MIN_SCRIM_ALPHA_FOR_DRAG;
 
-                int dragMax = mExpandedBubble.getExpandedView().getContentHeight();
+                int dragMax = expandedView.getContentHeight();
                 float dragFraction = dragAmount / dragMax;
 
                 return Math.max(BUBBLE_EXPANDED_SCRIM_ALPHA - alphaRange * dragFraction,
@@ -1120,33 +1121,35 @@
         mExpandedViewAlphaAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationStart(Animator animation) {
-                if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+                BubbleExpandedView expandedView = getExpandedView();
+                if (expandedView != null) {
                     // We need to be Z ordered on top in order for alpha animations to work.
-                    mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true);
-                    mExpandedBubble.getExpandedView().setAnimating(true);
+                    expandedView.setSurfaceZOrderedOnTop(true);
+                    expandedView.setAnimating(true);
                     mExpandedViewContainer.setVisibility(VISIBLE);
                 }
             }
 
             @Override
             public void onAnimationEnd(Animator animation) {
-                if (mExpandedBubble != null
-                        && mExpandedBubble.getExpandedView() != null
+                BubbleExpandedView expandedView = getExpandedView();
+                if (expandedView != null
                         // The surface needs to be Z ordered on top for alpha values to work on the
                         // TaskView, and if we're temporarily hidden, we are still on the screen
                         // with alpha = 0f until we animate back. Stay Z ordered on top so the alpha
                         // = 0f remains in effect.
                         && !mExpandedViewTemporarilyHidden) {
-                    mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
-                    mExpandedBubble.getExpandedView().setAnimating(false);
+                    expandedView.setSurfaceZOrderedOnTop(false);
+                    expandedView.setAnimating(false);
                 }
             }
         });
         mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> {
-            if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+            BubbleExpandedView expandedView = getExpandedView();
+            if (expandedView != null) {
                 float alpha = (float) valueAnimator.getAnimatedValue();
-                mExpandedBubble.getExpandedView().setContentAlpha(alpha);
-                mExpandedBubble.getExpandedView().setBackgroundAlpha(alpha);
+                expandedView.setContentAlpha(alpha);
+                expandedView.setBackgroundAlpha(alpha);
             }
         });
 
@@ -1390,7 +1393,7 @@
         }
         final boolean seen = getPrefBoolean(ManageEducationView.PREF_MANAGED_EDUCATION);
         final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext))
-                && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null;
+                && getExpandedView() != null;
         ProtoLog.d(WM_SHELL_BUBBLES, "Show manage edu=%b", shouldShow);
         if (shouldShow && BubbleDebugConfig.neverShowUserEducation(mContext)) {
             Log.w(TAG, "Want to show manage edu, but it is forced hidden");
@@ -1417,9 +1420,9 @@
      * Show manage education if was not showing before.
      */
     private void showManageEdu() {
-        if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) return;
-        mManageEduView.show(mExpandedBubble.getExpandedView(),
-                mStackAnimationController.isStackOnLeftSide());
+        BubbleExpandedView expandedView = getExpandedView();
+        if (expandedView == null) return;
+        mManageEduView.show(expandedView, mStackAnimationController.isStackOnLeftSide());
     }
 
     @VisibleForTesting
@@ -1931,6 +1934,11 @@
         return mExpandedBubble;
     }
 
+    @Nullable
+    private BubbleExpandedView getExpandedView() {
+        return mExpandedBubble != null ? mExpandedBubble.getExpandedView() : null;
+    }
+
     // via BubbleData.Listener
     @SuppressLint("ClickableViewAccessibility")
     void addBubble(Bubble bubble) {
@@ -2110,13 +2118,11 @@
 
         // If we're expanded, screenshot the currently expanded bubble (before expanding the newly
         // selected bubble) so we can animate it out.
-        if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null
-                && !mExpandedViewTemporarilyHidden) {
-            if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
-                // Before screenshotting, have the real TaskView show on top of other surfaces
-                // so that the screenshot doesn't flicker on top of it.
-                mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true);
-            }
+        BubbleExpandedView expandedView = getExpandedView();
+        if (mIsExpanded && expandedView != null && !mExpandedViewTemporarilyHidden) {
+            // Before screenshotting, have the real TaskView show on top of other surfaces
+            // so that the screenshot doesn't flicker on top of it.
+            expandedView.setSurfaceZOrderedOnTop(true);
 
             try {
                 screenshotAnimatingOutBubbleIntoSurface((success) -> {
@@ -2136,7 +2142,7 @@
     private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) {
         final BubbleViewProvider previouslySelected = mExpandedBubble;
         mExpandedBubble = bubbleToSelect;
-        mExpandedViewAnimationController.setExpandedView(mExpandedBubble.getExpandedView());
+        mExpandedViewAnimationController.setExpandedView(getExpandedView());
 
         if (mIsExpanded) {
             hideCurrentInputMethod();
@@ -2445,8 +2451,7 @@
             mBubbleContainer.animate().translationX(0).start();
         }
         mExpandedAnimationController.expandFromStack(() -> {
-            if (mIsExpanded && mExpandedBubble != null
-                    && mExpandedBubble.getExpandedView() != null) {
+            if (mIsExpanded && getExpandedView() != null) {
                 maybeShowManageEdu();
             }
             updateOverflowDotVisibility(true /* expanding */);
@@ -2509,13 +2514,14 @@
         }
         mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
 
-        if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
-            mExpandedBubble.getExpandedView().setContentAlpha(0f);
-            mExpandedBubble.getExpandedView().setBackgroundAlpha(0f);
+        BubbleExpandedView expandedView = getExpandedView();
+        if (expandedView != null) {
+            expandedView.setContentAlpha(0f);
+            expandedView.setBackgroundAlpha(0f);
 
             // We'll be starting the alpha animation after a slight delay, so set this flag early
             // here.
-            mExpandedBubble.getExpandedView().setAnimating(true);
+            expandedView.setAnimating(true);
         }
 
         mDelayedAnimation = () -> {
@@ -2545,10 +2551,9 @@
                     .withEndActions(() -> {
                         mExpandedViewContainer.setAnimationMatrix(null);
                         afterExpandedViewAnimation();
-                        if (mExpandedBubble != null
-                                && mExpandedBubble.getExpandedView() != null) {
-                            mExpandedBubble.getExpandedView()
-                                    .setSurfaceZOrderedOnTop(false);
+                        BubbleExpandedView expView = getExpandedView();
+                        if (expView != null) {
+                            expView.setSurfaceZOrderedOnTop(false);
                         }
                     })
                     .start();
@@ -2613,12 +2618,13 @@
         };
         mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after,
                 collapsePosition);
-        if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+        BubbleExpandedView expandedView = getExpandedView();
+        if (expandedView != null) {
             // When the animation completes, we should no longer be showing the content.
             // This won't actually update content visibility immediately, if we are currently
             // animating. But updates the internal state for the content to be hidden after
             // animation completes.
-            mExpandedBubble.getExpandedView().setContentVisibility(false);
+            expandedView.setContentVisibility(false);
         }
     }
 
@@ -2710,10 +2716,10 @@
                         // expanded view animation might not actually set the z ordering for the
                         // expanded view correctly, because the view may still be temporarily
                         // hidden. So set it again here.
-                        BubbleExpandedView bev = mExpandedBubble.getExpandedView();
-                        if (bev != null) {
-                            mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
-                            mExpandedBubble.getExpandedView().setAnimating(false);
+                        BubbleExpandedView expandedView = getExpandedView();
+                        if (expandedView != null) {
+                            expandedView.setSurfaceZOrderedOnTop(false);
+                            expandedView.setAnimating(false);
                         }
                     })
                     .start();
@@ -2785,13 +2791,13 @@
 
         if (mIsExpanded) {
             mExpandedViewAnimationController.animateForImeVisibilityChange(visible);
-            if (mPositioner.showBubblesVertically()
-                    && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+            BubbleExpandedView expandedView = getExpandedView();
+            if (mPositioner.showBubblesVertically() && expandedView != null) {
                 float selectedY = mPositioner.getExpandedBubbleXY(getState().selectedIndex,
                         getState()).y;
                 float newExpandedViewTop = mPositioner.getExpandedViewY(mExpandedBubble, selectedY);
-                mExpandedBubble.getExpandedView().setImeVisible(visible);
-                if (!mExpandedBubble.getExpandedView().isUsingMaxHeight()) {
+                expandedView.setImeVisible(visible);
+                if (!expandedView.isUsingMaxHeight()) {
                     mExpandedViewContainer.animate().translationY(newExpandedViewTop);
                 }
                 List<Animator> animList = new ArrayList<>();
@@ -3148,7 +3154,8 @@
 
         // This should not happen, since the manage menu is only visible when there's an expanded
         // bubble. If we end up in this state, just hide the menu immediately.
-        if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
+        BubbleExpandedView expandedView = getExpandedView();
+        if (expandedView == null) {
             mManageMenu.setVisibility(View.INVISIBLE);
             mManageMenuScrim.setVisibility(INVISIBLE);
             mSysuiProxyProvider.getSysuiProxy().onManageMenuExpandChanged(false /* show */);
@@ -3194,8 +3201,8 @@
             }
         }
 
-        if (mExpandedBubble.getExpandedView().getTaskView() != null) {
-            mExpandedBubble.getExpandedView().getTaskView().setObscuredTouchRect(mShowingManage
+        if (expandedView.getTaskView() != null) {
+            expandedView.getTaskView().setObscuredTouchRect(mShowingManage
                     ? new Rect(0, 0, getWidth(), getHeight())
                     : null);
         }
@@ -3205,8 +3212,8 @@
 
         // When the menu is open, it should be at these coordinates. The menu pops out to the right
         // in LTR and to the left in RTL.
-        mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
-        final float margin = mExpandedBubble.getExpandedView().getManageButtonMargin();
+        expandedView.getManageButtonBoundsOnScreen(mTempRect);
+        final float margin = expandedView.getManageButtonMargin();
         final float targetX = isLtr
                 ? mTempRect.left - margin
                 : mTempRect.right + margin - mManageMenu.getWidth();
@@ -3230,9 +3237,10 @@
                     .withEndActions(() -> {
                         View child = mManageMenu.getChildAt(0);
                         child.requestAccessibilityFocus();
-                        if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+                        BubbleExpandedView expView = getExpandedView();
+                        if (expView != null) {
                             // Update the AV's obscured touchable region for the new state.
-                            mExpandedBubble.getExpandedView().updateObscuredTouchableRegion();
+                            expView.updateObscuredTouchableRegion();
                         }
                     })
                     .start();
@@ -3247,9 +3255,10 @@
                     .spring(DynamicAnimation.TRANSLATION_Y, targetY + menuHeight / 4f)
                     .withEndActions(() -> {
                         mManageMenu.setVisibility(View.INVISIBLE);
-                        if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+                        BubbleExpandedView expView = getExpandedView();
+                        if (expView != null) {
                             // Update the AV's obscured touchable region for the new state.
-                            mExpandedBubble.getExpandedView().updateObscuredTouchableRegion();
+                            expView.updateObscuredTouchableRegion();
                         }
                     })
                     .start();
@@ -3276,9 +3285,8 @@
 
     private void updateExpandedBubble() {
         mExpandedViewContainer.removeAllViews();
-        if (mIsExpanded && mExpandedBubble != null
-                && mExpandedBubble.getExpandedView() != null) {
-            BubbleExpandedView bev = mExpandedBubble.getExpandedView();
+        BubbleExpandedView bev = getExpandedView();
+        if (mIsExpanded && bev != null) {
             bev.setContentVisibility(false);
             bev.setAnimating(!mIsExpansionAnimating);
             mExpandedViewContainerMatrix.setScaleX(0f);
@@ -3306,9 +3314,8 @@
     }
 
     private void updateManageButtonListener() {
-        if (mIsExpanded && mExpandedBubble != null
-                && mExpandedBubble.getExpandedView() != null) {
-            BubbleExpandedView bev = mExpandedBubble.getExpandedView();
+        BubbleExpandedView bev = getExpandedView();
+        if (mIsExpanded && bev != null) {
             bev.setManageClickListener((view) -> {
                 showManageMenu(true /* show */);
             });
@@ -3325,14 +3332,13 @@
      *                   expanded bubble.
      */
     private void screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete) {
-        if (!mIsExpanded || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
+        final BubbleExpandedView animatingOutExpandedView = getExpandedView();
+        if (!mIsExpanded || animatingOutExpandedView == null) {
             // You can't animate null.
             onComplete.accept(false);
             return;
         }
 
-        final BubbleExpandedView animatingOutExpandedView = mExpandedBubble.getExpandedView();
-
         // Release the previous screenshot if it hasn't been released already.
         if (mAnimatingOutBubbleBuffer != null) {
             releaseAnimatingOutBubbleBuffer();
@@ -3364,8 +3370,7 @@
         mAnimatingOutSurfaceContainer.setTranslationX(translationX);
         mAnimatingOutSurfaceContainer.setTranslationY(0);
 
-        final int[] taskViewLocation =
-                mExpandedBubble.getExpandedView().getTaskViewLocationOnScreen();
+        final int[] taskViewLocation = animatingOutExpandedView.getTaskViewLocationOnScreen();
         final int[] surfaceViewLocation = mAnimatingOutSurfaceView.getLocationOnScreen();
 
         // Translate the surface to overlap the real TaskView.
@@ -3427,15 +3432,15 @@
         int[] paddings = mPositioner.getExpandedViewContainerPadding(
                 mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded);
         mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]);
-        if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+        BubbleExpandedView expandedView = getExpandedView();
+        if (expandedView != null) {
             PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble),
                     getState());
             mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble,
                     mPositioner.showBubblesVertically() ? p.y : p.x));
             mExpandedViewContainer.setTranslationX(0f);
-            mExpandedBubble.getExpandedView().updateTaskViewContentWidth();
-            mExpandedBubble.getExpandedView().updateView(
-                    mExpandedViewContainer.getLocationOnScreen());
+            expandedView.updateTaskViewContentWidth();
+            expandedView.updateView(mExpandedViewContainer.getLocationOnScreen());
             updatePointerPosition(false /* forIme */);
         }
 
@@ -3508,7 +3513,8 @@
      *               the pointer is animated to the location.
      */
     private void updatePointerPosition(boolean forIme) {
-        if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
+        BubbleExpandedView expandedView = getExpandedView();
+        if (mExpandedBubble == null || expandedView == null) {
             return;
         }
         int index = getBubbleIndex(mExpandedBubble);
@@ -3519,7 +3525,7 @@
         float bubblePosition = mPositioner.showBubblesVertically()
                 ? position.y
                 : position.x;
-        mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition,
+        expandedView.setPointerPosition(bubblePosition,
                 mStackOnLeftOrWillBe, forIme /* animate */);
     }
 
@@ -3541,7 +3547,7 @@
      */
     int getBubbleIndex(@Nullable BubbleViewProvider provider) {
         if (provider == null) {
-            return 0;
+            return -1;
         }
         return mBubbleContainer.indexOfChild(provider.getIconView());
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index 95d4714..109868d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -20,9 +20,7 @@
 import com.android.wm.shell.protolog.ShellProtoLogGroup
 import com.android.wm.shell.util.KtProtoLog
 
-/**
- * Event logger for logging desktop mode session events
- */
+/** Event logger for logging desktop mode session events */
 class DesktopModeEventLogger {
     /**
      * Logs the enter of desktop mode having session id [sessionId] and the reason [enterReason] for
@@ -32,13 +30,16 @@
         KtProtoLog.v(
             ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
             "DesktopModeLogger: Logging session enter, session: %s reason: %s",
-            sessionId, enterReason.name
+            sessionId,
+            enterReason.name
         )
-        FrameworkStatsLog.write(DESKTOP_MODE_ATOM_ID,
+        FrameworkStatsLog.write(
+            DESKTOP_MODE_ATOM_ID,
             /* event */ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER,
             /* enterReason */ enterReason.reason,
             /* exitReason */ 0,
-            /* session_id */ sessionId)
+            /* session_id */ sessionId
+        )
     }
 
     /**
@@ -49,13 +50,16 @@
         KtProtoLog.v(
             ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
             "DesktopModeLogger: Logging session exit, session: %s reason: %s",
-            sessionId, exitReason.name
+            sessionId,
+            exitReason.name
         )
-        FrameworkStatsLog.write(DESKTOP_MODE_ATOM_ID,
+        FrameworkStatsLog.write(
+            DESKTOP_MODE_ATOM_ID,
             /* event */ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT,
             /* enterReason */ 0,
             /* exitReason */ exitReason.reason,
-            /* session_id */ sessionId)
+            /* session_id */ sessionId
+        )
     }
 
     /**
@@ -66,9 +70,11 @@
         KtProtoLog.v(
             ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
             "DesktopModeLogger: Logging task added, session: %s taskId: %s",
-            sessionId, taskUpdate.instanceId
+            sessionId,
+            taskUpdate.instanceId
         )
-        FrameworkStatsLog.write(DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
+        FrameworkStatsLog.write(
+            DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
             /* task_event */
             FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED,
             /* instance_id */
@@ -84,7 +90,8 @@
             /* task_y */
             taskUpdate.taskY,
             /* session_id */
-            sessionId)
+            sessionId
+        )
     }
 
     /**
@@ -95,9 +102,11 @@
         KtProtoLog.v(
             ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
             "DesktopModeLogger: Logging task remove, session: %s taskId: %s",
-            sessionId, taskUpdate.instanceId
+            sessionId,
+            taskUpdate.instanceId
         )
-        FrameworkStatsLog.write(DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
+        FrameworkStatsLog.write(
+            DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
             /* task_event */
             FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED,
             /* instance_id */
@@ -113,7 +122,8 @@
             /* task_y */
             taskUpdate.taskY,
             /* session_id */
-            sessionId)
+            sessionId
+        )
     }
 
     /**
@@ -124,9 +134,11 @@
         KtProtoLog.v(
             ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
             "DesktopModeLogger: Logging task info changed, session: %s taskId: %s",
-            sessionId, taskUpdate.instanceId
+            sessionId,
+            taskUpdate.instanceId
         )
-        FrameworkStatsLog.write(DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
+        FrameworkStatsLog.write(
+            DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
             /* task_event */
             FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
             /* instance_id */
@@ -142,7 +154,8 @@
             /* task_y */
             taskUpdate.taskY,
             /* session_id */
-            sessionId)
+            sessionId
+        )
     }
 
     companion object {
@@ -160,12 +173,8 @@
          * stats/atoms/desktopmode/desktopmode_extensions_atoms.proto
          */
         enum class EnterReason(val reason: Int) {
-            UNKNOWN_ENTER(
-                FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__UNKNOWN_ENTER
-            ),
-            OVERVIEW(
-                FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__OVERVIEW
-            ),
+            UNKNOWN_ENTER(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__UNKNOWN_ENTER),
+            OVERVIEW(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__OVERVIEW),
             APP_HANDLE_DRAG(
                 FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__APP_HANDLE_DRAG
             ),
@@ -178,9 +187,7 @@
             KEYBOARD_SHORTCUT_ENTER(
                 FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER
             ),
-            SCREEN_ON(
-                FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__SCREEN_ON
-            );
+            SCREEN_ON(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__SCREEN_ON)
         }
 
         /**
@@ -188,12 +195,8 @@
          * stats/atoms/desktopmode/desktopmode_extensions_atoms.proto
          */
         enum class ExitReason(val reason: Int) {
-            UNKNOWN_EXIT(
-                FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__UNKNOWN_EXIT
-            ),
-            DRAG_TO_EXIT(
-                FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT
-            ),
+            UNKNOWN_EXIT(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__UNKNOWN_EXIT),
+            DRAG_TO_EXIT(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT),
             APP_HANDLE_MENU_BUTTON_EXIT(
                 FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__APP_HANDLE_MENU_BUTTON_EXIT
             ),
@@ -203,16 +206,12 @@
             RETURN_HOME_OR_OVERVIEW(
                 FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME
             ),
-            TASK_FINISHED(
-                FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__TASK_FINISHED
-            ),
-            SCREEN_OFF(
-                FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__SCREEN_OFF
-            )
+            TASK_FINISHED(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__TASK_FINISHED),
+            SCREEN_OFF(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__SCREEN_OFF)
         }
 
         private const val DESKTOP_MODE_ATOM_ID = FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED
         private const val DESKTOP_MODE_TASK_UPDATE_ATOM_ID =
             FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index 0b7a3e8..5d8e340 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -60,8 +60,9 @@
     private val idSequence: InstanceIdSequence by lazy { InstanceIdSequence(Int.MAX_VALUE) }
 
     init {
-        if (Transitions.ENABLE_SHELL_TRANSITIONS &&
-            DesktopModeStatus.canEnterDesktopMode(context)) {
+        if (
+            Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.canEnterDesktopMode(context)
+        ) {
             shellInit.addInitCallback(this::onInit, this)
         }
     }
@@ -350,4 +351,4 @@
         return this.type == WindowManager.TRANSIT_TO_FRONT &&
             this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
index f1a475a..bc27f34 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
@@ -20,9 +20,7 @@
 import com.android.wm.shell.sysui.ShellCommandHandler
 import java.io.PrintWriter
 
-/**
- * Handles the shell commands for the DesktopTasksController.
- */
+/** Handles the shell commands for the DesktopTasksController. */
 class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) :
     ShellCommandHandler.ShellCommandActionHandler {
 
@@ -58,12 +56,13 @@
             return false
         }
 
-        val taskId = try {
-            args[1].toInt()
-        } catch (e: NumberFormatException) {
-            pw.println("Error: task id should be an integer")
-            return false
-        }
+        val taskId =
+            try {
+                args[1].toInt()
+            } catch (e: NumberFormatException) {
+                pw.println("Error: task id should be an integer")
+                return false
+            }
 
         return controller.moveToDesktop(taskId, WindowContainerTransaction())
     }
@@ -75,12 +74,13 @@
             return false
         }
 
-        val taskId = try {
-            args[1].toInt()
-        } catch (e: NumberFormatException) {
-            pw.println("Error: task id should be an integer")
-            return false
-        }
+        val taskId =
+            try {
+                args[1].toInt()
+            } catch (e: NumberFormatException) {
+                pw.println("Error: task id should be an integer")
+                return false
+            }
 
         controller.moveToNextDisplay(taskId)
         return true
@@ -92,4 +92,4 @@
         pw.println("$prefix moveToNextDisplay <taskId> ")
         pw.println("$prefix  Move a task with given id to next display.")
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 7e0234e..7d01580 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -32,9 +32,7 @@
 import java.util.concurrent.Executor
 import java.util.function.Consumer
 
-/**
- * Keeps track of task data related to desktop mode.
- */
+/** Keeps track of task data related to desktop mode. */
 class DesktopModeTaskRepository {
 
     /** Task data that is tracked per display */
@@ -48,12 +46,12 @@
         val activeTasks: ArraySet<Int> = ArraySet(),
         val visibleTasks: ArraySet<Int> = ArraySet(),
         val minimizedTasks: ArraySet<Int> = ArraySet(),
+        // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
+        val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
     )
 
     // Token of the current wallpaper activity, used to remove it when the last task is removed
     var wallpaperActivityToken: WindowContainerToken? = null
-    // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
-    private val freeformTasksInZOrder = mutableListOf<Int>()
     private val activeTasksListeners = ArraySet<ActiveTasksListener>()
     // Track visible tasks separately because a task may be part of the desktop but not visible.
     private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
@@ -84,13 +82,8 @@
         activeTasksListeners.add(activeTasksListener)
     }
 
-    /**
-     * Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not.
-     */
-    fun addVisibleTasksListener(
-        visibleTasksListener: VisibleTasksListener,
-        executor: Executor
-    ) {
+    /** Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. */
+    fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) {
         visibleTasksListeners[visibleTasksListener] = executor
         displayData.keyIterator().forEach { displayId ->
             val visibleTasksCount = getVisibleTaskCount(displayId)
@@ -112,9 +105,7 @@
         }
     }
 
-    /**
-     * Create a new merged region representative of all exclusion regions in all desktop tasks.
-     */
+    /** Create a new merged region representative of all exclusion regions in all desktop tasks. */
     private fun calculateDesktopExclusionRegion(): Region {
         val desktopExclusionRegion = Region()
         desktopExclusionRegions.valueIterator().forEach { taskExclusionRegion ->
@@ -123,16 +114,12 @@
         return desktopExclusionRegion
     }
 
-    /**
-     * Remove a previously registered [ActiveTasksListener]
-     */
+    /** Remove a previously registered [ActiveTasksListener] */
     fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) {
         activeTasksListeners.remove(activeTasksListener)
     }
 
-    /**
-     * Remove a previously registered [VisibleTasksListener]
-     */
+    /** Remove a previously registered [VisibleTasksListener] */
     fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) {
         visibleTasksListeners.remove(visibleTasksListener)
     }
@@ -182,18 +169,14 @@
         return result
     }
 
-    /**
-     * Check if a task with the given [taskId] was marked as an active task
-     */
+    /** Check if a task with the given [taskId] was marked as an active task */
     fun isActiveTask(taskId: Int): Boolean {
         return displayData.valueIterator().asSequence().any { data ->
             data.activeTasks.contains(taskId)
         }
     }
 
-    /**
-     * Whether a task is visible.
-     */
+    /** Whether a task is visible. */
     fun isVisibleTask(taskId: Int): Boolean {
         return displayData.valueIterator().asSequence().any { data ->
             data.visibleTasks.contains(taskId)
@@ -207,18 +190,14 @@
         }
     }
 
-    /**
-     *  Check if a task with the given [taskId] is the only active task on its display
-     */
+    /** Check if a task with the given [taskId] is the only active task on its display */
     fun isOnlyActiveTask(taskId: Int): Boolean {
         return displayData.valueIterator().asSequence().any { data ->
             data.activeTasks.singleOrNull() == taskId
         }
     }
 
-    /**
-     * Get a set of the active tasks for given [displayId]
-     */
+    /** Get a set of the active tasks for given [displayId] */
     fun getActiveTasks(displayId: Int): ArraySet<Int> {
         return ArraySet(displayData[displayId]?.activeTasks)
     }
@@ -235,20 +214,16 @@
      */
     fun getActiveNonMinimizedTasksOrderedFrontToBack(displayId: Int): List<Int> {
         val activeTasks = getActiveTasks(displayId)
-        val allTasksInZOrder = getFreeformTasksInZOrder()
+        val allTasksInZOrder = getFreeformTasksInZOrder(displayId)
         return activeTasks
-                // Don't show already minimized Tasks
-                .filter { taskId -> !isMinimizedTask(taskId) }
-                .sortedBy { taskId -> allTasksInZOrder.indexOf(taskId) }
+            // Don't show already minimized Tasks
+            .filter { taskId -> !isMinimizedTask(taskId) }
+            .sortedBy { taskId -> allTasksInZOrder.indexOf(taskId) }
     }
 
-    /**
-     * Get a list of freeform tasks, ordered from top-bottom (top at index 0).
-     */
-     // TODO(b/278084491): pass in display id
-    fun getFreeformTasksInZOrder(): List<Int> {
-        return freeformTasksInZOrder
-    }
+    /** Get a list of freeform tasks, ordered from top-bottom (top at index 0). */
+    fun getFreeformTasksInZOrder(displayId: Int): ArrayList<Int> =
+        ArrayList(displayData[displayId]?.freeformTasksInZOrder ?: emptyList())
 
     /**
      * Updates whether a freeform task with this id is visible or not and notifies listeners.
@@ -262,8 +237,10 @@
             val otherDisplays = displayData.keyIterator().asSequence().filter { it != displayId }
             for (otherDisplayId in otherDisplays) {
                 if (displayData[otherDisplayId].visibleTasks.remove(taskId)) {
-                    notifyVisibleTaskListeners(otherDisplayId,
-                        displayData[otherDisplayId].visibleTasks.size)
+                    notifyVisibleTaskListeners(
+                        otherDisplayId,
+                        displayData[otherDisplayId].visibleTasks.size
+                    )
                 }
             }
         } else if (displayId == INVALID_DISPLAY) {
@@ -310,9 +287,7 @@
         }
     }
 
-    /**
-     * Get number of tasks that are marked as visible on given [displayId]
-     */
+    /** Get number of tasks that are marked as visible on given [displayId] */
     fun getVisibleTaskCount(displayId: Int): Int {
         KtProtoLog.d(
             WM_SHELL_DESKTOP_MODE,
@@ -322,60 +297,62 @@
         return displayData[displayId]?.visibleTasks?.size ?: 0
     }
 
-    /**
-     * Add (or move if it already exists) the task to the top of the ordered list.
-     */
-    fun addOrMoveFreeformTaskToTop(taskId: Int) {
+    /** Add (or move if it already exists) the task to the top of the ordered list. */
+    // TODO(b/342417921): Identify if there is additional checks needed to move tasks for
+    // multi-display scenarios.
+    fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) {
         KtProtoLog.d(
             WM_SHELL_DESKTOP_MODE,
-            "DesktopTaskRepo: add or move task to top taskId=%d",
+            "DesktopTaskRepo: add or move task to top: display=%d, taskId=%d",
+            displayId,
             taskId
         )
-        if (freeformTasksInZOrder.contains(taskId)) {
-            freeformTasksInZOrder.remove(taskId)
-        }
-        freeformTasksInZOrder.add(0, taskId)
+        displayData[displayId]?.freeformTasksInZOrder?.remove(taskId)
+        displayData.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId)
     }
 
     /** Mark a Task as minimized. */
     fun minimizeTask(displayId: Int, taskId: Int) {
         KtProtoLog.v(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopModeTaskRepository: minimize Task: display=%d, task=%d",
-                displayId, taskId)
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopModeTaskRepository: minimize Task: display=%d, task=%d",
+            displayId,
+            taskId
+        )
         displayData.getOrCreate(displayId).minimizedTasks.add(taskId)
     }
 
     /** Mark a Task as non-minimized. */
     fun unminimizeTask(displayId: Int, taskId: Int) {
         KtProtoLog.v(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopModeTaskRepository: unminimize Task: display=%d, task=%d",
-                displayId, taskId)
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopModeTaskRepository: unminimize Task: display=%d, task=%d",
+            displayId,
+            taskId
+        )
         displayData[displayId]?.minimizedTasks?.remove(taskId)
     }
 
-    /**
-     * Remove the task from the ordered list.
-     */
-    fun removeFreeformTask(taskId: Int) {
+    /** Remove the task from the ordered list. */
+    fun removeFreeformTask(displayId: Int, taskId: Int) {
         KtProtoLog.d(
             WM_SHELL_DESKTOP_MODE,
-            "DesktopTaskRepo: remove freeform task from ordered list taskId=%d",
+            "DesktopTaskRepo: remove freeform task from ordered list: display=%d, taskId=%d",
+            displayId,
             taskId
         )
-        freeformTasksInZOrder.remove(taskId)
+        displayData[displayId]?.freeformTasksInZOrder?.remove(taskId)
         boundsBeforeMaximizeByTaskId.remove(taskId)
         KtProtoLog.d(
             WM_SHELL_DESKTOP_MODE,
-            "DesktopTaskRepo: remaining freeform tasks: %s", freeformTasksInZOrder.toDumpString(),
+            "DesktopTaskRepo: remaining freeform tasks: %s",
+            displayData[displayId]?.freeformTasksInZOrder?.toDumpString() ?: ""
         )
     }
 
     /**
      * Updates the active desktop gesture exclusion regions; if desktopExclusionRegions has been
-     * accepted by desktopGestureExclusionListener, it will be updated in the
-     * appropriate classes.
+     * accepted by desktopGestureExclusionListener, it will be updated in the appropriate classes.
      */
     fun updateTaskExclusionRegions(taskId: Int, taskExclusionRegions: Region) {
         desktopExclusionRegions.put(taskId, taskExclusionRegions)
@@ -385,9 +362,9 @@
     }
 
     /**
-     * Removes the desktop gesture exclusion region for the specified task; if exclusionRegion
-     * has been accepted by desktopGestureExclusionListener, it will be updated in the
-     * appropriate classes.
+     * Removes the desktop gesture exclusion region for the specified task; if exclusionRegion has
+     * been accepted by desktopGestureExclusionListener, it will be updated in the appropriate
+     * classes.
      */
     fun removeExclusionRegion(taskId: Int) {
         desktopExclusionRegions.delete(taskId)
@@ -396,16 +373,12 @@
         }
     }
 
-    /**
-     * Removes and returns the bounds saved before maximizing the given task.
-     */
+    /** Removes and returns the bounds saved before maximizing the given task. */
     fun removeBoundsBeforeMaximize(taskId: Int): Rect? {
         return boundsBeforeMaximizeByTaskId.removeReturnOld(taskId)
     }
 
-    /**
-     * Saves the bounds of the given task before maximizing.
-     */
+    /** Saves the bounds of the given task before maximizing. */
     fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) {
         boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds))
     }
@@ -414,7 +387,6 @@
         val innerPrefix = "$prefix  "
         pw.println("${prefix}DesktopModeTaskRepository")
         dumpDisplayData(pw, innerPrefix)
-        pw.println("${innerPrefix}freeformTasksInZOrder=${freeformTasksInZOrder.toDumpString()}")
         pw.println("${innerPrefix}activeTasksListeners=${activeTasksListeners.size}")
         pw.println("${innerPrefix}visibleTasksListeners=${visibleTasksListeners.size}")
     }
@@ -425,6 +397,9 @@
             pw.println("${prefix}Display $displayId:")
             pw.println("${innerPrefix}activeTasks=${data.activeTasks.toDumpString()}")
             pw.println("${innerPrefix}visibleTasks=${data.visibleTasks.toDumpString()}")
+            pw.println(
+                "${innerPrefix}freeformTasksInZOrder=${data.freeformTasksInZOrder.toDumpString()}"
+            )
         }
     }
 
@@ -432,9 +407,7 @@
      * Defines interface for classes that can listen to changes for active tasks in desktop mode.
      */
     interface ActiveTasksListener {
-        /**
-         * Called when the active tasks change in desktop mode.
-         */
+        /** Called when the active tasks change in desktop mode. */
         fun onActiveTasksChanged(displayId: Int) {}
     }
 
@@ -442,9 +415,7 @@
      * Defines interface for classes that can listen to changes for visible tasks in desktop mode.
      */
     interface VisibleTasksListener {
-        /**
-         * Called when the desktop changes the number of visible freeform tasks.
-         */
+        /** Called when the desktop changes the number of visible freeform tasks. */
         fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {}
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt
index aa11a7d..a9d4e5f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt
@@ -24,11 +24,11 @@
 import com.android.wm.shell.dagger.WMSingleton
 import javax.inject.Inject
 
-/**
- * Log Aster UIEvents for desktop windowing mode.
- */
+/** Log Aster UIEvents for desktop windowing mode. */
 @WMSingleton
-class DesktopModeUiEventLogger @Inject constructor(
+class DesktopModeUiEventLogger
+@Inject
+constructor(
     private val mUiEventLogger: UiEventLogger,
     private val mInstanceIdSequence: InstanceIdSequence
 ) {
@@ -47,16 +47,14 @@
         mUiEventLogger.log(event, uid, packageName)
     }
 
-    /**
-     * Retrieves a new instance id for a new interaction.
-     */
+    /** Retrieves a new instance id for a new interaction. */
     fun getNewInstanceId(): InstanceId = mInstanceIdSequence.newInstanceId()
 
     /**
      * Logs an event as part of a particular CUI, on a particular package.
      *
      * @param instanceId The id identifying an interaction, potentially taking place across multiple
-     * surfaces. There should be a new id generated for each distinct CUI.
+     *   surfaces. There should be a new id generated for each distinct CUI.
      * @param uid The user id associated with the package the user is interacting with
      * @param packageName The name of the package the user is interacting with
      * @param event The event type to generate
@@ -75,20 +73,15 @@
     }
 
     companion object {
-        /**
-         * Enums for logging desktop windowing mode UiEvents.
-         */
+        /** Enums for logging desktop windowing mode UiEvents. */
         enum class DesktopUiEventEnum(private val mId: Int) : UiEventLogger.UiEventEnum {
 
             @UiEvent(doc = "Resize the window in desktop windowing mode by dragging the edge")
             DESKTOP_WINDOW_EDGE_DRAG_RESIZE(1721),
-
             @UiEvent(doc = "Resize the window in desktop windowing mode by dragging the corner")
             DESKTOP_WINDOW_CORNER_DRAG_RESIZE(1722),
-
             @UiEvent(doc = "Tap on the window header maximize button in desktop windowing mode")
             DESKTOP_WINDOW_MAXIMIZE_BUTTON_TAP(1723),
-
             @UiEvent(doc = "Double tap on window header to maximize it in desktop windowing mode")
             DESKTOP_WINDOW_HEADER_DOUBLE_TAP_TO_MAXIMIZE(1724);
 
@@ -97,4 +90,4 @@
 
         private const val TAG = "DesktopModeUiEventLogger"
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index 6da3741..217b1d3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -28,13 +28,11 @@
 import android.util.Size
 import com.android.wm.shell.common.DisplayLayout
 
+val DESKTOP_MODE_INITIAL_BOUNDS_SCALE: Float =
+    SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
 
-val DESKTOP_MODE_INITIAL_BOUNDS_SCALE: Float = SystemProperties
-        .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
-
-val DESKTOP_MODE_LANDSCAPE_APP_PADDING: Int = SystemProperties
-        .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25)
-
+val DESKTOP_MODE_LANDSCAPE_APP_PADDING: Int =
+    SystemProperties.getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25)
 
 /**
  * Calculates the initial bounds required for an application to fill a scale of the display bounds
@@ -52,51 +50,53 @@
     val idealSize = calculateIdealSize(screenBounds, scale)
     // If no top activity exists, apps fullscreen bounds and aspect ratio cannot be calculated.
     // Instead default to the desired initial bounds.
-    val topActivityInfo = taskInfo.topActivityInfo
-        ?: return positionInScreen(idealSize, screenBounds)
+    val topActivityInfo =
+        taskInfo.topActivityInfo ?: return positionInScreen(idealSize, screenBounds)
 
-    val initialSize: Size = when (taskInfo.configuration.orientation) {
-        ORIENTATION_LANDSCAPE -> {
-            if (taskInfo.isResizeable) {
-                if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) {
-                    // Respect apps fullscreen width
-                    Size(taskInfo.appCompatTaskInfo.topActivityLetterboxWidth, idealSize.height)
+    val initialSize: Size =
+        when (taskInfo.configuration.orientation) {
+            ORIENTATION_LANDSCAPE -> {
+                if (taskInfo.isResizeable) {
+                    if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) {
+                        // Respect apps fullscreen width
+                        Size(taskInfo.appCompatTaskInfo.topActivityLetterboxWidth, idealSize.height)
+                    } else {
+                        idealSize
+                    }
                 } else {
-                    idealSize
-                }
-            } else {
-                maximumSizeMaintainingAspectRatio(taskInfo, idealSize,
-                    appAspectRatio)
-            }
-        }
-        ORIENTATION_PORTRAIT -> {
-            val customPortraitWidthForLandscapeApp = screenBounds.width() -
-                    (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2)
-            if (taskInfo.isResizeable) {
-                if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
-                    // Respect apps fullscreen height and apply custom app width
-                    Size(customPortraitWidthForLandscapeApp,
-                        taskInfo.appCompatTaskInfo.topActivityLetterboxHeight)
-                } else {
-                    idealSize
-                }
-            } else {
-                if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
-                    // Apply custom app width and calculate maximum size
-                    maximumSizeMaintainingAspectRatio(
-                        taskInfo,
-                        Size(customPortraitWidthForLandscapeApp, idealSize.height),
-                        appAspectRatio)
-                } else {
-                    maximumSizeMaintainingAspectRatio(taskInfo, idealSize,
-                        appAspectRatio)
+                    maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
                 }
             }
+            ORIENTATION_PORTRAIT -> {
+                val customPortraitWidthForLandscapeApp =
+                    screenBounds.width() - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2)
+                if (taskInfo.isResizeable) {
+                    if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+                        // Respect apps fullscreen height and apply custom app width
+                        Size(
+                            customPortraitWidthForLandscapeApp,
+                            taskInfo.appCompatTaskInfo.topActivityLetterboxHeight
+                        )
+                    } else {
+                        idealSize
+                    }
+                } else {
+                    if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+                        // Apply custom app width and calculate maximum size
+                        maximumSizeMaintainingAspectRatio(
+                            taskInfo,
+                            Size(customPortraitWidthForLandscapeApp, idealSize.height),
+                            appAspectRatio
+                        )
+                    } else {
+                        maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+                    }
+                }
+            }
+            else -> {
+                idealSize
+            }
         }
-        else -> {
-            idealSize
-        }
-    }
 
     return positionInScreen(initialSize, screenBounds)
 }
@@ -136,19 +136,17 @@
     return Size(finalWidth, finalHeight)
 }
 
-/**
- * Calculates the aspect ratio of an activity from its fullscreen bounds.
- */
+/** Calculates the aspect ratio of an activity from its fullscreen bounds. */
 private fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float {
     if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) {
         val appLetterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxWidth
         val appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxHeight
         return maxOf(appLetterboxWidth, appLetterboxHeight) /
-                minOf(appLetterboxWidth, appLetterboxHeight).toFloat()
+            minOf(appLetterboxWidth, appLetterboxHeight).toFloat()
     }
     val appBounds = taskInfo.configuration.windowConfiguration.appBounds ?: return 1f
     return maxOf(appBounds.height(), appBounds.width()) /
-                minOf(appBounds.height(), appBounds.width()).toFloat()
+        minOf(appBounds.height(), appBounds.width()).toFloat()
 }
 
 /**
@@ -161,13 +159,15 @@
     return Size(width, height)
 }
 
-/**
- * Adjusts bounds to be positioned in the middle of the screen.
- */
+/** Adjusts bounds to be positioned in the middle of the screen. */
 private fun positionInScreen(desiredSize: Size, screenBounds: Rect): Rect {
     // TODO(b/325240051): Position apps with bottom heavy offset
     val heightOffset = (screenBounds.height() - desiredSize.height) / 2
     val widthOffset = (screenBounds.width() - desiredSize.width) / 2
-    return Rect(widthOffset, heightOffset,
-        desiredSize.width + widthOffset, desiredSize.height + heightOffset)
+    return Rect(
+        widthOffset,
+        heightOffset,
+        desiredSize.width + widthOffset,
+        desiredSize.height + heightOffset
+    )
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index 2e40ba7..38db1eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -17,10 +17,10 @@
 package com.android.wm.shell.desktopmode;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -186,7 +186,7 @@
         // In freeform, keep the top corners clear.
         int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM
                 ? mContext.getResources().getDimensionPixelSize(
-                        com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) :
+                com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) :
                 -captionHeight;
         region.union(new Rect(0, transitionHeight, transitionEdgeWidth, layout.height()));
         return region;
@@ -315,10 +315,11 @@
         private static final float INDICATOR_FINAL_OPACITY = 0.35f;
         private static final int MAXIMUM_OPACITY = 255;
 
-        /** Determines how this animator will interact with the view's alpha:
-         *  Fade in, fade out, or no change to alpha
+        /**
+         * Determines how this animator will interact with the view's alpha:
+         * Fade in, fade out, or no change to alpha
          */
-        private enum AlphaAnimType{
+        private enum AlphaAnimType {
             ALPHA_FADE_IN_ANIM, ALPHA_FADE_OUT_ANIM, ALPHA_NO_CHANGE_ANIM
         }
 
@@ -365,10 +366,10 @@
          * Create animator for visual indicator changing type (i.e., fullscreen to freeform,
          * freeform to split, etc.)
          *
-         * @param view the view for this indicator
+         * @param view          the view for this indicator
          * @param displayLayout information about the display the transitioning task is currently on
-         * @param origType the original indicator type
-         * @param newType the new indicator type
+         * @param origType      the original indicator type
+         * @param newType       the new indicator type
          */
         private static VisualIndicatorAnimator animateIndicatorType(@NonNull View view,
                 @NonNull DisplayLayout displayLayout, IndicatorType origType,
@@ -469,7 +470,7 @@
          */
         private static Rect getMaxBounds(Rect startBounds) {
             return new Rect((int) (startBounds.left
-                            - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())),
+                    - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())),
                     (int) (startBounds.top
                             - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.height())),
                     (int) (startBounds.right
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index e5bf53a..6e45397 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -97,69 +97,74 @@
 
 /** Handles moving tasks in and out of desktop */
 class DesktopTasksController(
-        private val context: Context,
-        shellInit: ShellInit,
-        private val shellCommandHandler: ShellCommandHandler,
-        private val shellController: ShellController,
-        private val displayController: DisplayController,
-        private val shellTaskOrganizer: ShellTaskOrganizer,
-        private val syncQueue: SyncTransactionQueue,
-        private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
-        private val dragAndDropController: DragAndDropController,
-        private val transitions: Transitions,
-        private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler,
-        private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
-        private val toggleResizeDesktopTaskTransitionHandler:
-        ToggleResizeDesktopTaskTransitionHandler,
-        private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
-        private val desktopModeTaskRepository: DesktopModeTaskRepository,
-        private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
-        private val launchAdjacentController: LaunchAdjacentController,
-        private val recentsTransitionHandler: RecentsTransitionHandler,
-        private val multiInstanceHelper: MultiInstanceHelper,
-        @ShellMainThread private val mainExecutor: ShellExecutor,
-        private val desktopTasksLimiter: Optional<DesktopTasksLimiter>,
-) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler,
+    private val context: Context,
+    shellInit: ShellInit,
+    private val shellCommandHandler: ShellCommandHandler,
+    private val shellController: ShellController,
+    private val displayController: DisplayController,
+    private val shellTaskOrganizer: ShellTaskOrganizer,
+    private val syncQueue: SyncTransactionQueue,
+    private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+    private val dragAndDropController: DragAndDropController,
+    private val transitions: Transitions,
+    private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler,
+    private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
+    private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
+    private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
+    private val desktopModeTaskRepository: DesktopModeTaskRepository,
+    private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
+    private val launchAdjacentController: LaunchAdjacentController,
+    private val recentsTransitionHandler: RecentsTransitionHandler,
+    private val multiInstanceHelper: MultiInstanceHelper,
+    @ShellMainThread private val mainExecutor: ShellExecutor,
+    private val desktopTasksLimiter: Optional<DesktopTasksLimiter>,
+) :
+    RemoteCallable<DesktopTasksController>,
+    Transitions.TransitionHandler,
     DragAndDropController.DragAndDropListener {
 
     private val desktopMode: DesktopModeImpl
     private var visualIndicator: DesktopModeVisualIndicator? = null
     private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler =
         DesktopModeShellCommandHandler(this)
-    private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> {
-        t: SurfaceControl.Transaction ->
-        visualIndicator?.releaseVisualIndicator(t)
-        visualIndicator = null
-    }
-    private val taskVisibilityListener = object : VisibleTasksListener {
-        override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
-            launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0
-        }
-    }
-    private val dragToDesktopStateListener = object : DragToDesktopStateListener {
-        override fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) {
-            removeVisualIndicator(tx)
-        }
-
-        override fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) {
-            removeVisualIndicator(tx)
-        }
-
-        private fun removeVisualIndicator(tx: SurfaceControl.Transaction) {
-            visualIndicator?.releaseVisualIndicator(tx)
+    private val mOnAnimationFinishedCallback =
+        Consumer<SurfaceControl.Transaction> { t: SurfaceControl.Transaction ->
+            visualIndicator?.releaseVisualIndicator(t)
             visualIndicator = null
         }
-    }
+    private val taskVisibilityListener =
+        object : VisibleTasksListener {
+            override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
+                launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0
+            }
+        }
+    private val dragToDesktopStateListener =
+        object : DragToDesktopStateListener {
+            override fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) {
+                removeVisualIndicator(tx)
+            }
+
+            override fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) {
+                removeVisualIndicator(tx)
+            }
+
+            private fun removeVisualIndicator(tx: SurfaceControl.Transaction) {
+                visualIndicator?.releaseVisualIndicator(tx)
+                visualIndicator = null
+            }
+        }
 
     private val transitionAreaHeight
-        get() = context.resources.getDimensionPixelSize(
+        get() =
+            context.resources.getDimensionPixelSize(
                 com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height
-        )
+            )
 
     private val transitionAreaWidth
-        get() = context.resources.getDimensionPixelSize(
-            com.android.wm.shell.R.dimen.desktop_mode_transition_area_width
-        )
+        get() =
+            context.resources.getDimensionPixelSize(
+                com.android.wm.shell.R.dimen.desktop_mode_transition_area_width
+            )
 
     /** Task id of the task currently being dragged from fullscreen/split. */
     val draggingTaskId
@@ -178,11 +183,7 @@
     private fun onInit() {
         KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController")
         shellCommandHandler.addDumpCallback(this::dump, this)
-        shellCommandHandler.addCommandCallback(
-            "desktopmode",
-            desktopModeShellCommandHandler,
-            this
-        )
+        shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler, this)
         shellController.addExternalInterface(
             ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE,
             { createExternalInterface() },
@@ -232,9 +233,10 @@
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             // TODO(b/309014605): ensure remote transition is supplied once state is introduced
             val transitionType = if (remoteTransition == null) TRANSIT_NONE else TRANSIT_TO_FRONT
-            val handler = remoteTransition?.let {
-                OneShotRemoteHandler(transitions.mainExecutor, remoteTransition)
-            }
+            val handler =
+                remoteTransition?.let {
+                    OneShotRemoteHandler(transitions.mainExecutor, remoteTransition)
+                }
             transitions.startTransition(transitionType, wct, handler).also { t ->
                 handler?.setTransition(t)
             }
@@ -253,9 +255,9 @@
         val allFocusedTasks =
             shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo ->
                 taskInfo.isFocused &&
-                        (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN ||
-                                taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) &&
-                        taskInfo.activityType != ACTIVITY_TYPE_HOME
+                    (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN ||
+                        taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) &&
+                    taskInfo.activityType != ACTIVITY_TYPE_HOME
             }
         if (allFocusedTasks.isNotEmpty()) {
             when (allFocusedTasks.size) {
@@ -278,7 +280,7 @@
                     KtProtoLog.w(
                         WM_SHELL_DESKTOP_MODE,
                         "DesktopTasksController: Cannot enter desktop, expected less " +
-                                "than 3 focused tasks but found %d",
+                            "than 3 focused tasks but found %d",
                         allFocusedTasks.size
                     )
                 }
@@ -288,27 +290,24 @@
 
     /** Move a task with given `taskId` to desktop */
     fun moveToDesktop(
-            taskId: Int,
-            wct: WindowContainerTransaction = WindowContainerTransaction()
+        taskId: Int,
+        wct: WindowContainerTransaction = WindowContainerTransaction()
     ): Boolean {
-        shellTaskOrganizer.getRunningTaskInfo(taskId)?.let {
-            task -> moveToDesktop(task, wct)
-        } ?: return false
+        shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToDesktop(task, wct) }
+            ?: return false
         return true
     }
 
-    /**
-     * Move a task to desktop
-     */
+    /** Move a task to desktop */
     fun moveToDesktop(
-            task: RunningTaskInfo,
-            wct: WindowContainerTransaction = WindowContainerTransaction()
+        task: RunningTaskInfo,
+        wct: WindowContainerTransaction = WindowContainerTransaction()
     ) {
         if (!DesktopModeStatus.canEnterDesktopMode(context)) {
             KtProtoLog.w(
                 WM_SHELL_DESKTOP_MODE,
                 "DesktopTasksController: Cannot enter desktop, " +
-                        "display does not meet minimum size requirements"
+                    "display does not meet minimum size requirements"
             )
             return
         }
@@ -316,7 +315,7 @@
             KtProtoLog.w(
                 WM_SHELL_DESKTOP_MODE,
                 "DesktopTasksController: Cannot enter desktop, " +
-                        "translucent top activity found. This is likely a modal dialog."
+                    "translucent top activity found. This is likely a modal dialog."
             )
             return
         }
@@ -328,7 +327,7 @@
         exitSplitIfApplicable(wct, task)
         // Bring other apps to front first
         val taskToMinimize =
-                bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
+            bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
         addMoveToDesktopChanges(wct, task)
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -340,21 +339,21 @@
     }
 
     /**
-     * The first part of the animated drag to desktop transition. This is
-     * followed with a call to [finalizeDragToDesktop] or [cancelDragToDesktop].
+     * The first part of the animated drag to desktop transition. This is followed with a call to
+     * [finalizeDragToDesktop] or [cancelDragToDesktop].
      */
     fun startDragToDesktop(
-            taskInfo: RunningTaskInfo,
-            dragToDesktopValueAnimator: MoveToDesktopAnimator,
+        taskInfo: RunningTaskInfo,
+        dragToDesktopValueAnimator: MoveToDesktopAnimator,
     ) {
         KtProtoLog.v(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: startDragToDesktop taskId=%d",
-                taskInfo.taskId
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopTasksController: startDragToDesktop taskId=%d",
+            taskInfo.taskId
         )
         dragToDesktopTransitionHandler.startDragToDesktopTransition(
-                taskInfo.taskId,
-                dragToDesktopValueAnimator
+            taskInfo.taskId,
+            dragToDesktopValueAnimator
         )
     }
 
@@ -364,16 +363,15 @@
      */
     private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
         KtProtoLog.v(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: finalizeDragToDesktop taskId=%d",
-                taskInfo.taskId
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopTasksController: finalizeDragToDesktop taskId=%d",
+            taskInfo.taskId
         )
         val wct = WindowContainerTransaction()
         exitSplitIfApplicable(wct, taskInfo)
         moveHomeTaskToFront(wct)
         val taskToMinimize =
-                bringDesktopAppsToFrontBeforeShowingNewTask(
-                        taskInfo.displayId, wct, taskInfo.taskId)
+            bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
         addMoveToDesktopChanges(wct, taskInfo)
         wct.setBounds(taskInfo.token, freeformBounds)
         val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
@@ -381,9 +379,9 @@
     }
 
     /**
-     * Perform needed cleanup transaction once animation is complete. Bounds need to be set
-     * here instead of initial wct to both avoid flicker and to have task bounds to use for
-     * the staging animation.
+     * Perform needed cleanup transaction once animation is complete. Bounds need to be set here
+     * instead of initial wct to both avoid flicker and to have task bounds to use for the staging
+     * animation.
      *
      * @param taskInfo task entering split that requires a bounds update
      */
@@ -395,16 +393,13 @@
     }
 
     /**
-     * Perform clean up of the desktop wallpaper activity if the closed window task is
-     * the last active task.
+     * Perform clean up of the desktop wallpaper activity if the closed window task is the last
+     * active task.
      *
      * @param wct transaction to modify if the last active task is closed
      * @param taskId task id of the window that's being closed
      */
-    fun onDesktopWindowClose(
-        wct: WindowContainerTransaction,
-        taskId: Int
-    ) {
+    fun onDesktopWindowClose(wct: WindowContainerTransaction, taskId: Int) {
         if (desktopModeTaskRepository.isOnlyActiveTask(taskId)) {
             removeWallpaperActivity(wct)
         }
@@ -419,8 +414,9 @@
 
     /** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */
     fun enterFullscreen(displayId: Int) {
-        getFocusedFreeformTask(displayId)
-                ?.let { moveToFullscreenWithAnimation(it, it.positionInParent) }
+        getFocusedFreeformTask(displayId)?.let {
+            moveToFullscreenWithAnimation(it, it.positionInParent)
+        }
     }
 
     /** Move a desktop app to split screen. */
@@ -449,8 +445,7 @@
                 splitScreenController.getStageOfTask(taskInfo.taskId),
                 EXIT_REASON_DESKTOP_MODE
             )
-            splitScreenController.transitionHandler
-                ?.onSplitToDesktop()
+            splitScreenController.transitionHandler?.onSplitToDesktop()
         }
     }
 
@@ -469,16 +464,16 @@
 
     private fun moveToFullscreenWithAnimation(task: RunningTaskInfo, position: Point) {
         KtProtoLog.v(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: moveToFullscreen with animation taskId=%d",
-                task.taskId
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopTasksController: moveToFullscreen with animation taskId=%d",
+            task.taskId
         )
         val wct = WindowContainerTransaction()
         addMoveToFullscreenChanges(wct, task)
 
         if (Transitions.ENABLE_SHELL_TRANSITIONS) {
             exitDesktopTaskTransitionHandler.startTransition(
-            Transitions.TRANSIT_EXIT_DESKTOP_MODE,
+                Transitions.TRANSIT_EXIT_DESKTOP_MODE,
                 wct,
                 position,
                 mOnAnimationFinishedCallback
@@ -517,12 +512,12 @@
      * Move task to the next display.
      *
      * Queries all current known display ids and sorts them in ascending order. Then iterates
-     * through the list and looks for the display id that is larger than the display id for
-     * the passed in task. If a display with a higher id is not found, iterates through the list and
+     * through the list and looks for the display id that is larger than the display id for the
+     * passed in task. If a display with a higher id is not found, iterates through the list and
      * finds the first display id that is not the display id for the passed in task.
      *
-     * If a display matching the above criteria is found, re-parents the task to that display.
-     * No-op if no such display is found.
+     * If a display matching the above criteria is found, re-parents the task to that display. No-op
+     * if no such display is found.
      */
     fun moveToNextDisplay(taskId: Int) {
         val task = shellTaskOrganizer.getRunningTaskInfo(taskId)
@@ -533,7 +528,7 @@
         KtProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
             "moveToNextDisplay: taskId=%d taskDisplayId=%d",
-                taskId,
+            taskId,
             task.displayId
         )
 
@@ -560,7 +555,7 @@
         KtProtoLog.v(
             WM_SHELL_DESKTOP_MODE,
             "moveToDisplay: taskId=%d displayId=%d",
-                task.taskId,
+            task.taskId,
             displayId
         )
 
@@ -585,9 +580,9 @@
     }
 
     /**
-     * Quick-resizes a desktop task, toggling between a fullscreen state (represented by the
-     * stable bounds) and a free floating state (either the last saved bounds if available or the
-     * default bounds otherwise).
+     * Quick-resizes a desktop task, toggling between a fullscreen state (represented by the stable
+     * bounds) and a free floating state (either the last saved bounds if available or the default
+     * bounds otherwise).
      */
     fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) {
         val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
@@ -600,11 +595,11 @@
             // before the task was toggled to stable bounds were saved, toggle the task to those
             // bounds. Otherwise, toggle to the default bounds.
             val taskBoundsBeforeMaximize =
-                    desktopModeTaskRepository.removeBoundsBeforeMaximize(taskInfo.taskId)
+                desktopModeTaskRepository.removeBoundsBeforeMaximize(taskInfo.taskId)
             if (taskBoundsBeforeMaximize != null) {
                 destinationBounds.set(taskBoundsBeforeMaximize)
             } else {
-                if (Flags.enableWindowingDynamicInitialBounds()){
+                if (Flags.enableWindowingDynamicInitialBounds()) {
                     destinationBounds.set(calculateInitialBounds(displayLayout, taskInfo))
                 } else {
                     destinationBounds.set(getDefaultDesktopTaskBounds(displayLayout))
@@ -650,8 +645,12 @@
         val desiredHeight = (displayLayout.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt()
         val heightOffset = (displayLayout.height() - desiredHeight) / 2
         val widthOffset = (displayLayout.width() - desiredWidth) / 2
-        return Rect(widthOffset, heightOffset,
-            desiredWidth + widthOffset, desiredHeight + heightOffset)
+        return Rect(
+            widthOffset,
+            heightOffset,
+            desiredWidth + widthOffset,
+            desiredHeight + heightOffset
+        )
     }
 
     private fun getSnapBounds(taskInfo: RunningTaskInfo, position: SnapPosition): Rect {
@@ -693,19 +692,21 @@
     }
 
     private fun bringDesktopAppsToFrontBeforeShowingNewTask(
-            displayId: Int,
-            wct: WindowContainerTransaction,
-            newTaskIdInFront: Int
+        displayId: Int,
+        wct: WindowContainerTransaction,
+        newTaskIdInFront: Int
     ): RunningTaskInfo? = bringDesktopAppsToFront(displayId, wct, newTaskIdInFront)
 
     private fun bringDesktopAppsToFront(
-            displayId: Int,
-            wct: WindowContainerTransaction,
-            newTaskIdInFront: Int? = null
+        displayId: Int,
+        wct: WindowContainerTransaction,
+        newTaskIdInFront: Int? = null
     ): RunningTaskInfo? {
-        KtProtoLog.v(WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: bringDesktopAppsToFront, newTaskIdInFront=%s",
-                newTaskIdInFront ?: "null")
+        KtProtoLog.v(
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopTasksController: bringDesktopAppsToFront, newTaskIdInFront=%s",
+            newTaskIdInFront ?: "null"
+        )
 
         if (Flags.enableDesktopWindowingWallpaperActivity()) {
             // Add translucent wallpaper activity to show the wallpaper underneath
@@ -716,19 +717,25 @@
         }
 
         val nonMinimizedTasksOrderedFrontToBack =
-                desktopModeTaskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId)
+            desktopModeTaskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId)
         // If we're adding a new Task we might need to minimize an old one
         val taskToMinimize: RunningTaskInfo? =
-                if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) {
-                    desktopTasksLimiter.get().getTaskToMinimizeIfNeeded(
-                            nonMinimizedTasksOrderedFrontToBack, newTaskIdInFront)
-                } else { null }
+            if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) {
+                desktopTasksLimiter
+                    .get()
+                    .getTaskToMinimizeIfNeeded(
+                        nonMinimizedTasksOrderedFrontToBack,
+                        newTaskIdInFront
+                    )
+            } else {
+                null
+            }
         nonMinimizedTasksOrderedFrontToBack
-                // If there is a Task to minimize, let it stay behind the Home Task
-                .filter { taskId -> taskId != taskToMinimize?.taskId }
-                .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) }
-                .reversed() // Start from the back so the front task is brought forward last
-                .forEach { task -> wct.reorder(task.token, true /* onTop */) }
+            // If there is a Task to minimize, let it stay behind the Home Task
+            .filter { taskId -> taskId != taskToMinimize?.taskId }
+            .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) }
+            .reversed() // Start from the back so the front task is brought forward last
+            .forEach { task -> wct.reorder(task.token, true /* onTop */) }
         return taskToMinimize
     }
 
@@ -742,13 +749,19 @@
     private fun addWallpaperActivity(wct: WindowContainerTransaction) {
         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: addWallpaper")
         val intent = Intent(context, DesktopWallpaperActivity::class.java)
-        val options = ActivityOptions.makeBasic().apply {
-            isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
-            pendingIntentBackgroundActivityStartMode =
-                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
-        }
-        val pendingIntent = PendingIntent.getActivity(context, /* requestCode = */ 0, intent,
-            PendingIntent.FLAG_IMMUTABLE)
+        val options =
+            ActivityOptions.makeBasic().apply {
+                isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
+                pendingIntentBackgroundActivityStartMode =
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+            }
+        val pendingIntent =
+            PendingIntent.getActivity(
+                context,
+                /* requestCode = */ 0,
+                intent,
+                PendingIntent.FLAG_IMMUTABLE
+            )
         wct.sendPendingIntent(pendingIntent, intent, options.toBundle())
     }
 
@@ -807,8 +820,7 @@
                     false
                 }
                 // Handle back navigation for the last window if wallpaper available
-                shouldRemoveWallpaper(request) ->
-                    true
+                shouldRemoveWallpaper(request) -> true
                 // Only handle open or to front transitions
                 request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> {
                     reason = "transition type not handled (${request.type})"
@@ -826,7 +838,7 @@
                 }
                 // Only handle fullscreen or freeform tasks
                 triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN &&
-                        triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> {
+                    triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> {
                     reason = "windowingMode not handled (${triggerTask.windowingMode})"
                     false
                 }
@@ -836,31 +848,32 @@
 
         if (!shouldHandleRequest) {
             KtProtoLog.v(
-                    WM_SHELL_DESKTOP_MODE,
-                    "DesktopTasksController: skipping handleRequest reason=%s",
-                    reason
+                WM_SHELL_DESKTOP_MODE,
+                "DesktopTasksController: skipping handleRequest reason=%s",
+                reason
             )
             return null
         }
 
-        val result = triggerTask?.let { task ->
-            when {
-                request.type == TRANSIT_TO_BACK -> handleBackNavigation(task)
-                // Check if the task has a top transparent activity
-                shouldLaunchAsModal(task) -> handleTransparentTaskLaunch(task)
-                // Check if fullscreen task should be updated
-                task.isFullscreen -> handleFullscreenTaskLaunch(task, transition)
-                // Check if freeform task should be updated
-                task.isFreeform -> handleFreeformTaskLaunch(task, transition)
-                else -> {
-                    null
+        val result =
+            triggerTask?.let { task ->
+                when {
+                    request.type == TRANSIT_TO_BACK -> handleBackNavigation(task)
+                    // Check if the task has a top transparent activity
+                    shouldLaunchAsModal(task) -> handleTransparentTaskLaunch(task)
+                    // Check if fullscreen task should be updated
+                    task.isFullscreen -> handleFullscreenTaskLaunch(task, transition)
+                    // Check if freeform task should be updated
+                    task.isFreeform -> handleFreeformTaskLaunch(task, transition)
+                    else -> {
+                        null
+                    }
                 }
             }
-        }
         KtProtoLog.v(
-                WM_SHELL_DESKTOP_MODE,
-                "DesktopTasksController: handleRequest result=%s",
-                result ?: "null"
+            WM_SHELL_DESKTOP_MODE,
+            "DesktopTasksController: handleRequest result=%s",
+            result ?: "null"
         )
         return result
     }
@@ -870,18 +883,15 @@
      * This is intended to be used when desktop mode is part of another animation but isn't, itself,
      * animating.
      */
-    fun syncSurfaceState(
-            info: TransitionInfo,
-            finishTransaction: SurfaceControl.Transaction
-    ) {
+    fun syncSurfaceState(info: TransitionInfo, finishTransaction: SurfaceControl.Transaction) {
         // Add rounded corners to freeform windows
         if (!DesktopModeStatus.useRoundedCorners()) {
             return
         }
         val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
         info.changes
-                .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM }
-                .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
+            .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM }
+            .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
     }
 
     private fun shouldLaunchAsModal(task: TaskInfo) =
@@ -889,23 +899,23 @@
 
     private fun shouldRemoveWallpaper(request: TransitionRequestInfo): Boolean {
         return Flags.enableDesktopWindowingWallpaperActivity() &&
-                request.type == TRANSIT_TO_BACK &&
-                request.triggerTask?.let { task ->
-                    desktopModeTaskRepository.isOnlyActiveTask(task.taskId)
-                } ?: false
+            request.type == TRANSIT_TO_BACK &&
+            request.triggerTask?.let { task ->
+                desktopModeTaskRepository.isOnlyActiveTask(task.taskId)
+            } ?: false
     }
 
     private fun handleFreeformTaskLaunch(
-            task: RunningTaskInfo,
-            transition: IBinder
+        task: RunningTaskInfo,
+        transition: IBinder
     ): WindowContainerTransaction? {
         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch")
         if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) {
             KtProtoLog.d(
-                    WM_SHELL_DESKTOP_MODE,
-                    "DesktopTasksController: switch freeform task to fullscreen oon transition" +
-                            " taskId=%d",
-                    task.taskId
+                WM_SHELL_DESKTOP_MODE,
+                "DesktopTasksController: switch freeform task to fullscreen oon transition" +
+                    " taskId=%d",
+                task.taskId
             )
             return WindowContainerTransaction().also { wct ->
                 bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
@@ -927,16 +937,16 @@
     }
 
     private fun handleFullscreenTaskLaunch(
-            task: RunningTaskInfo,
-            transition: IBinder
+        task: RunningTaskInfo,
+        transition: IBinder
     ): WindowContainerTransaction? {
         KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch")
         if (desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) {
             KtProtoLog.d(
-                    WM_SHELL_DESKTOP_MODE,
-                    "DesktopTasksController: switch fullscreen task to freeform on transition" +
-                            " taskId=%d",
-                    task.taskId
+                WM_SHELL_DESKTOP_MODE,
+                "DesktopTasksController: switch fullscreen task to freeform on transition" +
+                    " taskId=%d",
+                task.taskId
             )
             return WindowContainerTransaction().also { wct ->
                 addMoveToDesktopChanges(wct, task)
@@ -952,21 +962,18 @@
     // Always launch transparent tasks in fullscreen.
     private fun handleTransparentTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
         // Already fullscreen, no-op.
-        if (task.isFullscreen)
-            return null
-        return WindowContainerTransaction().also { wct ->
-            addMoveToFullscreenChanges(wct, task)
-        }
+        if (task.isFullscreen) return null
+        return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) }
     }
 
     /** Handle back navigation by removing wallpaper activity if it's the last active task */
     private fun handleBackNavigation(task: RunningTaskInfo): WindowContainerTransaction? {
-        if (desktopModeTaskRepository.isOnlyActiveTask(task.taskId) &&
-            desktopModeTaskRepository.wallpaperActivityToken != null) {
+        if (
+            desktopModeTaskRepository.isOnlyActiveTask(task.taskId) &&
+                desktopModeTaskRepository.wallpaperActivityToken != null
+        ) {
             // Remove wallpaper activity when the last active task is removed
-            return WindowContainerTransaction().also { wct ->
-                removeWallpaperActivity(wct)
-            }
+            return WindowContainerTransaction().also { wct -> removeWallpaperActivity(wct) }
         } else {
             return null
         }
@@ -979,12 +986,13 @@
         val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
         val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
         val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
-        val targetWindowingMode = if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) {
-            // Display windowing is freeform, set to undefined and inherit it
-            WINDOWING_MODE_UNDEFINED
-        } else {
-            WINDOWING_MODE_FREEFORM
-        }
+        val targetWindowingMode =
+            if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) {
+                // Display windowing is freeform, set to undefined and inherit it
+                WINDOWING_MODE_UNDEFINED
+            } else {
+                WINDOWING_MODE_FREEFORM
+            }
         if (Flags.enableWindowingDynamicInitialBounds()) {
             wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo))
         }
@@ -1001,12 +1009,13 @@
     ) {
         val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
         val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
-        val targetWindowingMode = if (tdaWindowingMode == WINDOWING_MODE_FULLSCREEN) {
-            // Display windowing is fullscreen, set to undefined and inherit it
-            WINDOWING_MODE_UNDEFINED
-        } else {
-            WINDOWING_MODE_FULLSCREEN
-        }
+        val targetWindowingMode =
+            if (tdaWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+                // Display windowing is fullscreen, set to undefined and inherit it
+                WINDOWING_MODE_UNDEFINED
+            } else {
+                WINDOWING_MODE_FULLSCREEN
+            }
         wct.setWindowingMode(taskInfo.token, targetWindowingMode)
         wct.setBounds(taskInfo.token, Rect())
         if (isDesktopDensityOverrideSet()) {
@@ -1018,10 +1027,7 @@
      * Adds split screen changes to a transaction. Note that bounds are not reset here due to
      * animation; see {@link onDesktopSplitSelectAnimComplete}
      */
-    private fun addMoveToSplitChanges(
-        wct: WindowContainerTransaction,
-        taskInfo: RunningTaskInfo
-    ) {
+    private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
         // This windowing mode is to get the transition animation started; once we complete
         // split select, we will change windowing mode to undefined and inherit from split stage.
         // Going to undefined here causes task to flicker to the top left.
@@ -1034,38 +1040,35 @@
 
     /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */
     private fun addAndGetMinimizeChangesIfNeeded(
-            displayId: Int,
-            wct: WindowContainerTransaction,
-            newTaskInfo: RunningTaskInfo
+        displayId: Int,
+        wct: WindowContainerTransaction,
+        newTaskInfo: RunningTaskInfo
     ): RunningTaskInfo? {
         if (!desktopTasksLimiter.isPresent) return null
-        return desktopTasksLimiter.get().addAndGetMinimizeTaskChangesIfNeeded(
-                displayId, wct, newTaskInfo)
+        return desktopTasksLimiter
+            .get()
+            .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskInfo)
     }
 
     private fun addPendingMinimizeTransition(
-            transition: IBinder,
-            taskToMinimize: RunningTaskInfo?
+        transition: IBinder,
+        taskToMinimize: RunningTaskInfo?
     ) {
         if (taskToMinimize == null) return
         desktopTasksLimiter.ifPresent {
-            it.addPendingMinimizeChange(
-                    transition, taskToMinimize.displayId, taskToMinimize.taskId)
+            it.addPendingMinimizeChange(transition, taskToMinimize.displayId, taskToMinimize.taskId)
         }
     }
 
     /** Enter split by using the focused desktop task in given `displayId`. */
-    fun enterSplit(
-        displayId: Int,
-        leftOrTop: Boolean
-    ) {
+    fun enterSplit(displayId: Int, leftOrTop: Boolean) {
         getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) }
     }
 
     private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? {
-        return shellTaskOrganizer.getRunningTasks(displayId)
-                .find { taskInfo -> taskInfo.isFocused &&
-                        taskInfo.windowingMode == WINDOWING_MODE_FREEFORM }
+        return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo ->
+            taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
+        }
     }
 
     /**
@@ -1078,7 +1081,8 @@
         leftOrTop: Boolean = false,
     ) {
         val windowingMode = taskInfo.windowingMode
-        if (windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_FREEFORM
+        if (
+            windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_FREEFORM
         ) {
             val wct = WindowContainerTransaction()
             addMoveToSplitChanges(wct, taskInfo)
@@ -1107,9 +1111,9 @@
 
     /**
      * Perform checks required on drag move. Create/release fullscreen indicator as needed.
-     * Different sources for x and y coordinates are used due to different needs for each:
-     * We want split transitions to be based on input coordinates but fullscreen transition
-     * to be based on task edge coordinate.
+     * Different sources for x and y coordinates are used due to different needs for each: We want
+     * split transitions to be based on input coordinates but fullscreen transition to be based on
+     * task edge coordinate.
      *
      * @param taskInfo the task being dragged.
      * @param taskSurface SurfaceControl of dragged task.
@@ -1133,9 +1137,16 @@
         taskTop: Float
     ): DesktopModeVisualIndicator.IndicatorType {
         // If the visual indicator does not exist, create it.
-        val indicator = visualIndicator ?: DesktopModeVisualIndicator(
-            syncQueue, taskInfo, displayController, context, taskSurface,
-            rootTaskDisplayAreaOrganizer)
+        val indicator =
+            visualIndicator
+                ?: DesktopModeVisualIndicator(
+                    syncQueue,
+                    taskInfo,
+                    displayController,
+                    context,
+                    taskSurface,
+                    rootTaskDisplayAreaOrganizer
+                )
         if (visualIndicator == null) visualIndicator = indicator
         return indicator.updateIndicatorType(PointF(inputX, taskTop), taskInfo.windowingMode)
     }
@@ -1161,10 +1172,11 @@
         }
 
         val indicator = visualIndicator ?: return
-        val indicatorType = indicator.updateIndicatorType(
-            PointF(inputCoordinate.x, taskBounds.top.toFloat()),
-            taskInfo.windowingMode
-        )
+        val indicatorType =
+            indicator.updateIndicatorType(
+                PointF(inputCoordinate.x, taskBounds.top.toFloat()),
+                taskInfo.windowingMode
+            )
         when (indicatorType) {
             DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> {
                 moveToFullscreenWithAnimation(taskInfo, position)
@@ -1180,8 +1192,12 @@
             DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR -> {
                 // If task bounds are outside valid drag area, snap them inward and perform a
                 // transaction to set bounds.
-                if (DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(
-                        taskBounds, validDragArea)) {
+                if (
+                    DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(
+                        taskBounds,
+                        validDragArea
+                    )
+                ) {
                     val wct = WindowContainerTransaction()
                     wct.setBounds(taskInfo.token, taskBounds)
                     transitions.startTransition(TRANSIT_CHANGE, wct, null)
@@ -1189,8 +1205,9 @@
                 releaseVisualIndicator()
             }
             DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> {
-                throw IllegalArgumentException("Should not be receiving TO_DESKTOP_INDICATOR for " +
-                        "a freeform task.")
+                throw IllegalArgumentException(
+                    "Should not be receiving TO_DESKTOP_INDICATOR for " + "a freeform task."
+                )
             }
         }
         // A freeform drag-move ended, remove the indicator immediately.
@@ -1205,8 +1222,7 @@
      */
     fun onDragPositioningEndThroughStatusBar(inputCoordinates: PointF, taskInfo: RunningTaskInfo) {
         val indicator = getVisualIndicator() ?: return
-        val indicatorType = indicator
-            .updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
+        val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
         when (indicatorType) {
             DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> {
                 val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
@@ -1229,16 +1245,12 @@
         }
     }
 
-    /**
-     * Update the exclusion region for a specified task
-     */
+    /** Update the exclusion region for a specified task */
     fun onExclusionRegionChanged(taskId: Int, exclusionRegion: Region) {
         desktopModeTaskRepository.updateTaskExclusionRegions(taskId, exclusionRegion)
     }
 
-    /**
-     * Remove a previously tracked exclusion region for a specified task.
-     */
+    /** Remove a previously tracked exclusion region for a specified task. */
     fun removeExclusionRegionForTask(taskId: Int) {
         desktopModeTaskRepository.removeExclusionRegion(taskId)
     }
@@ -1259,10 +1271,7 @@
      * @param listener the listener to add.
      * @param callbackExecutor the executor to call the listener on.
      */
-    fun setTaskRegionListener(
-            listener: Consumer<Region>,
-            callbackExecutor: Executor
-    ) {
+    fun setTaskRegionListener(listener: Consumer<Region>, callbackExecutor: Executor) {
         desktopModeTaskRepository.setExclusionRegionListener(listener, callbackExecutor)
     }
 
@@ -1287,15 +1296,16 @@
         }
 
         // Start a new transition to launch the app
-        val opts = ActivityOptions.makeBasic().apply {
-            launchWindowingMode = WINDOWING_MODE_FREEFORM
-            pendingIntentLaunchFlags =
-                Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
-            setPendingIntentBackgroundActivityStartMode(
-                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED
-            )
-            isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
-        }
+        val opts =
+            ActivityOptions.makeBasic().apply {
+                launchWindowingMode = WINDOWING_MODE_FREEFORM
+                pendingIntentLaunchFlags =
+                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+                setPendingIntentBackgroundActivityStartMode(
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED
+                )
+                isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
+            }
         val wct = WindowContainerTransaction()
         wct.sendPendingIntent(launchIntent, null, opts.toBundle())
         transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */)
@@ -1321,8 +1331,8 @@
     @ExternalThread
     private inner class DesktopModeImpl : DesktopMode {
         override fun addVisibleTasksListener(
-                listener: VisibleTasksListener,
-                callbackExecutor: Executor
+            listener: VisibleTasksListener,
+            callbackExecutor: Executor
         ) {
             mainExecutor.execute {
                 this@DesktopTasksController.addVisibleTasksListener(listener, callbackExecutor)
@@ -1330,8 +1340,8 @@
         }
 
         override fun addDesktopGestureExclusionRegionListener(
-                listener: Consumer<Region>,
-                callbackExecutor: Executor
+            listener: Consumer<Region>,
+            callbackExecutor: Executor
         ) {
             mainExecutor.execute {
                 this@DesktopTasksController.setTaskRegionListener(listener, callbackExecutor)
@@ -1339,21 +1349,15 @@
         }
 
         override fun moveFocusedTaskToDesktop(displayId: Int) {
-            mainExecutor.execute {
-                this@DesktopTasksController.moveFocusedTaskToDesktop(displayId)
-            }
+            mainExecutor.execute { this@DesktopTasksController.moveFocusedTaskToDesktop(displayId) }
         }
 
         override fun moveFocusedTaskToFullscreen(displayId: Int) {
-            mainExecutor.execute {
-                this@DesktopTasksController.enterFullscreen(displayId)
-            }
+            mainExecutor.execute { this@DesktopTasksController.enterFullscreen(displayId) }
         }
 
         override fun moveFocusedTaskToStageSplit(displayId: Int, leftOrTop: Boolean) {
-            mainExecutor.execute {
-                this@DesktopTasksController.enterSplit(displayId, leftOrTop)
-            }
+            mainExecutor.execute { this@DesktopTasksController.enterSplit(displayId, leftOrTop) }
         }
     }
 
@@ -1363,36 +1367,35 @@
         IDesktopMode.Stub(), ExternalInterfaceBinder {
 
         private lateinit var remoteListener:
-                SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>
+            SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>
 
-        private val listener: VisibleTasksListener = object : VisibleTasksListener {
-            override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
-                KtProtoLog.v(
+        private val listener: VisibleTasksListener =
+            object : VisibleTasksListener {
+                override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
+                    KtProtoLog.v(
                         WM_SHELL_DESKTOP_MODE,
                         "IDesktopModeImpl: onVisibilityChanged display=%d visible=%d",
                         displayId,
                         visibleTasksCount
-                )
-                remoteListener.call {
-                    l -> l.onTasksVisibilityChanged(displayId, visibleTasksCount)
+                    )
+                    remoteListener.call { l ->
+                        l.onTasksVisibilityChanged(displayId, visibleTasksCount)
+                    }
                 }
             }
-        }
 
         init {
             remoteListener =
-                    SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>(
-                            controller,
-                            { c ->
-                                c.desktopModeTaskRepository.addVisibleTasksListener(
-                                        listener,
-                                        c.mainExecutor
-                                )
-                            },
-                            { c ->
-                                c.desktopModeTaskRepository.removeVisibleTasksListener(listener)
-                            }
-                    )
+                SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>(
+                    controller,
+                    { c ->
+                        c.desktopModeTaskRepository.addVisibleTasksListener(
+                            listener,
+                            c.mainExecutor
+                        )
+                    },
+                    { c -> c.desktopModeTaskRepository.removeVisibleTasksListener(listener) }
+                )
         }
 
         /** Invalidates this instance, preventing future calls from updating the controller. */
@@ -1402,24 +1405,19 @@
         }
 
         override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) {
-            ExecutorUtils.executeRemoteCallWithTaskPermission(
-                controller,
-                "showDesktopApps"
-            ) { c -> c.showDesktopApps(displayId, remoteTransition) }
+            ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c ->
+                c.showDesktopApps(displayId, remoteTransition)
+            }
         }
 
         override fun showDesktopApp(taskId: Int) {
-            ExecutorUtils.executeRemoteCallWithTaskPermission(
-                    controller,
-                    "showDesktopApp"
-            ) { c -> c.moveTaskToFront(taskId) }
+            ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "showDesktopApp") { c ->
+                c.moveTaskToFront(taskId)
+            }
         }
 
         override fun stashDesktopApps(displayId: Int) {
-            KtProtoLog.w(
-                WM_SHELL_DESKTOP_MODE,
-                "IDesktopModeImpl: stashDesktopApps is deprecated"
-            )
+            KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: stashDesktopApps is deprecated")
         }
 
         override fun hideStashedDesktopApps(displayId: Int) {
@@ -1444,35 +1442,38 @@
             ExecutorUtils.executeRemoteCallWithTaskPermission(
                 controller,
                 "onDesktopSplitSelectAnimComplete"
-            ) { c -> c.onDesktopSplitSelectAnimComplete(taskInfo) }
+            ) { c ->
+                c.onDesktopSplitSelectAnimComplete(taskInfo)
+            }
         }
 
         override fun setTaskListener(listener: IDesktopTaskListener?) {
             KtProtoLog.v(
-                    WM_SHELL_DESKTOP_MODE,
-                    "IDesktopModeImpl: set task listener=%s",
-                    listener ?: "null"
+                WM_SHELL_DESKTOP_MODE,
+                "IDesktopModeImpl: set task listener=%s",
+                listener ?: "null"
             )
-            ExecutorUtils.executeRemoteCallWithTaskPermission(
-                    controller,
-                    "setTaskListener"
-            ) { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() }
+            ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ ->
+                listener?.let { remoteListener.register(it) } ?: remoteListener.unregister()
+            }
         }
 
         override fun moveToDesktop(taskId: Int) {
-            ExecutorUtils.executeRemoteCallWithTaskPermission(
-                controller,
-                "moveToDesktop"
-            ) { c -> c.moveToDesktop(taskId) }
+            ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "moveToDesktop") { c ->
+                c.moveToDesktop(taskId)
+            }
         }
     }
 
     companion object {
         @JvmField
-        val DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties
-                .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
+        val DESKTOP_MODE_INITIAL_BOUNDS_SCALE =
+            SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
     }
 
     /** The positions on a screen that a task can snap to. */
-    enum class SnapPosition { RIGHT, LEFT }
+    enum class SnapPosition {
+        RIGHT,
+        LEFT
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index e5e435d..98c79d7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -50,26 +50,25 @@
  * gesture.
  */
 class DragToDesktopTransitionHandler(
-        private val context: Context,
-        private val transitions: Transitions,
-        private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
-        private val transactionSupplier: Supplier<SurfaceControl.Transaction>
+    private val context: Context,
+    private val transitions: Transitions,
+    private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+    private val transactionSupplier: Supplier<SurfaceControl.Transaction>
 ) : TransitionHandler {
 
     constructor(
-            context: Context,
-            transitions: Transitions,
-            rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+        context: Context,
+        transitions: Transitions,
+        rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
     ) : this(
-            context,
-            transitions,
-            rootTaskDisplayAreaOrganizer,
-            Supplier { SurfaceControl.Transaction() }
+        context,
+        transitions,
+        rootTaskDisplayAreaOrganizer,
+        Supplier { SurfaceControl.Transaction() }
     )
 
     private val rectEvaluator = RectEvaluator(Rect())
-    private val launchHomeIntent = Intent(Intent.ACTION_MAIN)
-            .addCategory(Intent.CATEGORY_HOME)
+    private val launchHomeIntent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
 
     private var dragToDesktopStateListener: DragToDesktopStateListener? = null
     private lateinit var splitScreenController: SplitScreenController
@@ -107,52 +106,55 @@
      * after one of the "end" or "cancel" transitions is merged into this transition.
      */
     fun startDragToDesktopTransition(
-            taskId: Int,
-            dragToDesktopAnimator: MoveToDesktopAnimator,
+        taskId: Int,
+        dragToDesktopAnimator: MoveToDesktopAnimator,
     ) {
         if (inProgress) {
             KtProtoLog.v(
-                    ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
-                    "DragToDesktop: Drag to desktop transition already in progress."
+                ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+                "DragToDesktop: Drag to desktop transition already in progress."
             )
             return
         }
 
-        val options = ActivityOptions.makeBasic().apply {
-            setTransientLaunch()
-            setSourceInfo(SourceInfo.TYPE_DESKTOP_ANIMATION, SystemClock.uptimeMillis())
-            pendingIntentCreatorBackgroundActivityStartMode =
-                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
-        }
-        val pendingIntent = PendingIntent.getActivity(
+        val options =
+            ActivityOptions.makeBasic().apply {
+                setTransientLaunch()
+                setSourceInfo(SourceInfo.TYPE_DESKTOP_ANIMATION, SystemClock.uptimeMillis())
+                pendingIntentCreatorBackgroundActivityStartMode =
+                    ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+            }
+        val pendingIntent =
+            PendingIntent.getActivity(
                 context,
                 0 /* requestCode */,
                 launchHomeIntent,
                 FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT,
                 options.toBundle()
-        )
+            )
         val wct = WindowContainerTransaction()
         wct.sendPendingIntent(pendingIntent, launchHomeIntent, Bundle())
-        val startTransitionToken = transitions
-                .startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this)
+        val startTransitionToken =
+            transitions.startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this)
 
-        transitionState = if (isSplitTask(taskId)) {
-            val otherTask = getOtherSplitTask(taskId) ?: throw IllegalStateException(
-                "Expected split task to have a counterpart."
-            )
-            TransitionState.FromSplit(
+        transitionState =
+            if (isSplitTask(taskId)) {
+                val otherTask =
+                    getOtherSplitTask(taskId)
+                        ?: throw IllegalStateException("Expected split task to have a counterpart.")
+                TransitionState.FromSplit(
                     draggedTaskId = taskId,
                     dragAnimator = dragToDesktopAnimator,
                     startTransitionToken = startTransitionToken,
                     otherSplitTask = otherTask
-            )
-        } else {
-            TransitionState.FromFullscreen(
+                )
+            } else {
+                TransitionState.FromFullscreen(
                     draggedTaskId = taskId,
                     dragAnimator = dragToDesktopAnimator,
                     startTransitionToken = startTransitionToken
-            )
-        }
+                )
+            }
     }
 
     /**
@@ -216,15 +218,16 @@
     }
 
     override fun startAnimation(
-            transition: IBinder,
-            info: TransitionInfo,
-            startTransaction: SurfaceControl.Transaction,
-            finishTransaction: SurfaceControl.Transaction,
-            finishCallback: Transitions.TransitionFinishCallback
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: Transitions.TransitionFinishCallback
     ): Boolean {
         val state = requireTransitionState()
 
-        val isStartDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP &&
+        val isStartDragToDesktop =
+            info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP &&
                 transition == state.startTransitionToken
         if (!isStartDragToDesktop) {
             return false
@@ -257,13 +260,16 @@
                 when (state) {
                     is TransitionState.FromSplit -> {
                         state.splitRootChange = change
-                        val layer = if (!state.cancelled) {
-                            // Normal case, split root goes to the bottom behind everything else.
-                            appLayers - i
-                        } else {
-                            // Cancel-early case, pretend nothing happened so split root stays top.
-                            dragLayer
-                        }
+                        val layer =
+                            if (!state.cancelled) {
+                                // Normal case, split root goes to the bottom behind everything
+                                // else.
+                                appLayers - i
+                            } else {
+                                // Cancel-early case, pretend nothing happened so split root stays
+                                // top.
+                                dragLayer
+                            }
                         startTransaction.apply {
                             setLayer(change.leash, layer)
                             show(change.leash)
@@ -308,7 +314,10 @@
                 if (change.taskInfo?.taskId == state.draggedTaskId && !state.cancelled) {
                     state.draggedTaskChange = change
                     taskDisplayAreaOrganizer.reparentToDisplayArea(
-                            change.endDisplayId, change.leash, startTransaction)
+                        change.endDisplayId,
+                        change.leash,
+                        startTransaction
+                    )
                     val bounds = change.endAbsBounds
                     startTransaction.apply {
                         setLayer(change.leash, dragLayer)
@@ -339,28 +348,34 @@
     }
 
     override fun mergeAnimation(
-            transition: IBinder,
-            info: TransitionInfo,
-            t: SurfaceControl.Transaction,
-            mergeTarget: IBinder,
-            finishCallback: Transitions.TransitionFinishCallback
+        transition: IBinder,
+        info: TransitionInfo,
+        t: SurfaceControl.Transaction,
+        mergeTarget: IBinder,
+        finishCallback: Transitions.TransitionFinishCallback
     ) {
         val state = requireTransitionState()
-        val isCancelTransition = info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
+        val isCancelTransition =
+            info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
                 transition == state.cancelTransitionToken &&
                 mergeTarget == state.startTransitionToken
-        val isEndTransition = info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
+        val isEndTransition =
+            info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
                 mergeTarget == state.startTransitionToken
 
-        val startTransactionFinishT = state.startTransitionFinishTransaction
+        val startTransactionFinishT =
+            state.startTransitionFinishTransaction
                 ?: error("Start transition expected to be waiting for merge but wasn't")
-        val startTransitionFinishCb = state.startTransitionFinishCb
+        val startTransitionFinishCb =
+            state.startTransitionFinishCb
                 ?: error("Start transition expected to be waiting for merge but wasn't")
         if (isEndTransition) {
             info.changes.withIndex().forEach { (i, change) ->
                 // If we're exiting split, hide the remaining split task.
-                if (state is TransitionState.FromSplit &&
-                    change.taskInfo?.taskId == state.otherSplitTask) {
+                if (
+                    state is TransitionState.FromSplit &&
+                        change.taskInfo?.taskId == state.otherSplitTask
+                ) {
                     t.hide(change.leash)
                     startTransactionFinishT.hide(change.leash)
                 }
@@ -373,14 +388,16 @@
                     state.draggedTaskChange = change
                 } else if (change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM) {
                     // Other freeform tasks that are being restored go behind the dragged task.
-                    val draggedTaskLeash = state.draggedTaskChange?.leash
+                    val draggedTaskLeash =
+                        state.draggedTaskChange?.leash
                             ?: error("Expected dragged leash to be non-null")
                     t.setRelativeLayer(change.leash, draggedTaskLeash, -i)
                     startTransactionFinishT.setRelativeLayer(change.leash, draggedTaskLeash, -i)
                 }
             }
 
-            val draggedTaskChange = state.draggedTaskChange
+            val draggedTaskChange =
+                state.draggedTaskChange
                     ?: throw IllegalStateException("Expected non-null change of dragged task")
             val draggedTaskLeash = draggedTaskChange.leash
             val startBounds = draggedTaskChange.startAbsBounds
@@ -395,57 +412,59 @@
             val startPosition = state.dragAnimator.position
             val unscaledStartWidth = startBounds.width()
             val unscaledStartHeight = startBounds.height()
-            val unscaledStartBounds = Rect(
-                startPosition.x.toInt(),
-                startPosition.y.toInt(),
-                startPosition.x.toInt() + unscaledStartWidth,
-                startPosition.y.toInt() + unscaledStartHeight
-            )
+            val unscaledStartBounds =
+                Rect(
+                    startPosition.x.toInt(),
+                    startPosition.y.toInt(),
+                    startPosition.x.toInt() + unscaledStartWidth,
+                    startPosition.y.toInt() + unscaledStartHeight
+                )
 
             dragToDesktopStateListener?.onCommitToDesktopAnimationStart(t)
             // Accept the merge by applying the merging transaction (applied by #showResizeVeil)
             // and finish callback. Show the veil and position the task at the first frame before
             // starting the final animation.
-            onTaskResizeAnimationListener.onAnimationStart(state.draggedTaskId, t,
-                unscaledStartBounds)
+            onTaskResizeAnimationListener.onAnimationStart(
+                state.draggedTaskId,
+                t,
+                unscaledStartBounds
+            )
             finishCallback.onTransitionFinished(null /* wct */)
             val tx: SurfaceControl.Transaction = transactionSupplier.get()
             ValueAnimator.ofObject(rectEvaluator, unscaledStartBounds, endBounds)
-                    .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
-                    .apply {
-                        addUpdateListener { animator ->
-                            val animBounds = animator.animatedValue as Rect
-                            val animFraction = animator.animatedFraction
-                            // Progress scale from starting value to 1 as animation plays.
-                            val animScale = startScale + animFraction * (1 - startScale)
-                            tx.apply {
-                                setScale(draggedTaskLeash, animScale, animScale)
-                                setPosition(
-                                     draggedTaskLeash,
-                                     animBounds.left.toFloat(),
-                                     animBounds.top.toFloat()
-                                )
-                                setWindowCrop(
-                                    draggedTaskLeash,
-                                    animBounds.width(),
-                                    animBounds.height()
-                                )
-                            }
-                            onTaskResizeAnimationListener.onBoundsChange(
-                                    state.draggedTaskId,
-                                    tx,
-                                    animBounds
+                .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
+                .apply {
+                    addUpdateListener { animator ->
+                        val animBounds = animator.animatedValue as Rect
+                        val animFraction = animator.animatedFraction
+                        // Progress scale from starting value to 1 as animation plays.
+                        val animScale = startScale + animFraction * (1 - startScale)
+                        tx.apply {
+                            setScale(draggedTaskLeash, animScale, animScale)
+                            setPosition(
+                                draggedTaskLeash,
+                                animBounds.left.toFloat(),
+                                animBounds.top.toFloat()
                             )
+                            setWindowCrop(draggedTaskLeash, animBounds.width(), animBounds.height())
                         }
-                        addListener(object : AnimatorListenerAdapter() {
+                        onTaskResizeAnimationListener.onBoundsChange(
+                            state.draggedTaskId,
+                            tx,
+                            animBounds
+                        )
+                    }
+                    addListener(
+                        object : AnimatorListenerAdapter() {
                             override fun onAnimationEnd(animation: Animator) {
                                 onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId)
                                 startTransitionFinishCb.onTransitionFinished(null /* null */)
                                 clearState()
                             }
-                        })
-                        start()
-                    }
+                        }
+                    )
+                    start()
+                }
         } else if (isCancelTransition) {
             info.changes.forEach { change ->
                 t.show(change.leash)
@@ -459,8 +478,8 @@
     }
 
     override fun handleRequest(
-            transition: IBinder,
-            request: TransitionRequestInfo
+        transition: IBinder,
+        request: TransitionRequestInfo
     ): WindowContainerTransaction? {
         // Only handle transitions started from shell.
         return null
@@ -489,8 +508,8 @@
         val state = requireTransitionState()
         val dragToDesktopAnimator = state.dragAnimator
 
-        val draggedTaskChange = state.draggedTaskChange
-                ?: throw IllegalStateException("Expected non-null task change")
+        val draggedTaskChange =
+            state.draggedTaskChange ?: throw IllegalStateException("Expected non-null task change")
         val sc = draggedTaskChange.leash
         // Pause the animation that shrinks the window when task is first dragged from fullscreen
         dragToDesktopAnimator.cancelAnimator()
@@ -503,29 +522,31 @@
         val dy = targetY - y
         val tx: SurfaceControl.Transaction = transactionSupplier.get()
         ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
-                .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
-                .apply {
-                    addUpdateListener { animator ->
-                        val scale = animator.animatedValue as Float
-                        val fraction = animator.animatedFraction
-                        val animX = x + (dx * fraction)
-                        val animY = y + (dy * fraction)
-                        tx.apply {
-                            setPosition(sc, animX, animY)
-                            setScale(sc, scale, scale)
-                            show(sc)
-                            apply()
-                        }
+            .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
+            .apply {
+                addUpdateListener { animator ->
+                    val scale = animator.animatedValue as Float
+                    val fraction = animator.animatedFraction
+                    val animX = x + (dx * fraction)
+                    val animY = y + (dy * fraction)
+                    tx.apply {
+                        setPosition(sc, animX, animY)
+                        setScale(sc, scale, scale)
+                        show(sc)
+                        apply()
                     }
-                    addListener(object : AnimatorListenerAdapter() {
+                }
+                addListener(
+                    object : AnimatorListenerAdapter() {
                         override fun onAnimationEnd(animation: Animator) {
                             dragToDesktopStateListener?.onCancelToDesktopAnimationEnd(tx)
                             // Start the cancel transition to restore order.
                             startCancelDragToDesktopTransition()
                         }
-                    })
-                    start()
-                }
+                    }
+                )
+                start()
+            }
     }
 
     private fun startCancelDragToDesktopTransition() {
@@ -536,19 +557,23 @@
                 // There may have been tasks sent behind home that are not the dragged task (like
                 // when the dragged task is translucent and that makes the task behind it visible).
                 // Restore the order of those first.
-                state.otherRootChanges.mapNotNull { it.container }.forEach { wc ->
-                    // TODO(b/322852244): investigate why even though these "other" tasks are
-                    //  reordered in front of home and behind the translucent dragged task, its
-                    //  surface is not visible on screen.
-                    wct.reorder(wc, true /* toTop */)
-                }
-                val wc = state.draggedTaskChange?.container
+                state.otherRootChanges
+                    .mapNotNull { it.container }
+                    .forEach { wc ->
+                        // TODO(b/322852244): investigate why even though these "other" tasks are
+                        //  reordered in front of home and behind the translucent dragged task, its
+                        //  surface is not visible on screen.
+                        wct.reorder(wc, true /* toTop */)
+                    }
+                val wc =
+                    state.draggedTaskChange?.container
                         ?: error("Dragged task should be non-null before cancelling")
                 // Then the dragged task a the very top.
                 wct.reorder(wc, true /* toTop */)
             }
             is TransitionState.FromSplit -> {
-                val wc = state.splitRootChange?.container
+                val wc =
+                    state.splitRootChange?.container
                         ?: error("Split root should be non-null before cancelling")
                 wct.reorder(wc, true /* toTop */)
             }
@@ -556,8 +581,8 @@
         val homeWc = state.homeToken ?: error("Home task should be non-null before cancelling")
         wct.restoreTransientOrder(homeWc)
 
-        state.cancelTransitionToken = transitions.startTransition(
-                TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this)
+        state.cancelTransitionToken =
+            transitions.startTransition(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this)
     }
 
     private fun clearState() {
@@ -571,11 +596,12 @@
     private fun getOtherSplitTask(taskId: Int): Int? {
         val splitPos = splitScreenController.getSplitPosition(taskId)
         if (splitPos == SPLIT_POSITION_UNDEFINED) return null
-        val otherTaskPos = if (splitPos == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
-            SPLIT_POSITION_TOP_OR_LEFT
-        } else {
-            SPLIT_POSITION_BOTTOM_OR_RIGHT
-        }
+        val otherTaskPos =
+            if (splitPos == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
+                SPLIT_POSITION_TOP_OR_LEFT
+            } else {
+                SPLIT_POSITION_BOTTOM_OR_RIGHT
+            }
         return splitScreenController.getTaskInfo(otherTaskPos)?.taskId
     }
 
@@ -585,6 +611,7 @@
 
     interface DragToDesktopStateListener {
         fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction)
+
         fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction)
     }
 
@@ -601,31 +628,32 @@
         abstract var startAborted: Boolean
 
         data class FromFullscreen(
-                override val draggedTaskId: Int,
-                override val dragAnimator: MoveToDesktopAnimator,
-                override val startTransitionToken: IBinder,
-                override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
-                override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
-                override var cancelTransitionToken: IBinder? = null,
-                override var homeToken: WindowContainerToken? = null,
-                override var draggedTaskChange: Change? = null,
-                override var cancelled: Boolean = false,
-                override var startAborted: Boolean = false,
-                var otherRootChanges: MutableList<Change> = mutableListOf()
+            override val draggedTaskId: Int,
+            override val dragAnimator: MoveToDesktopAnimator,
+            override val startTransitionToken: IBinder,
+            override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
+            override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
+            override var cancelTransitionToken: IBinder? = null,
+            override var homeToken: WindowContainerToken? = null,
+            override var draggedTaskChange: Change? = null,
+            override var cancelled: Boolean = false,
+            override var startAborted: Boolean = false,
+            var otherRootChanges: MutableList<Change> = mutableListOf()
         ) : TransitionState()
+
         data class FromSplit(
-                override val draggedTaskId: Int,
-                override val dragAnimator: MoveToDesktopAnimator,
-                override val startTransitionToken: IBinder,
-                override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
-                override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
-                override var cancelTransitionToken: IBinder? = null,
-                override var homeToken: WindowContainerToken? = null,
-                override var draggedTaskChange: Change? = null,
-                override var cancelled: Boolean = false,
-                override var startAborted: Boolean = false,
-                var splitRootChange: Change? = null,
-                var otherSplitTask: Int
+            override val draggedTaskId: Int,
+            override val dragAnimator: MoveToDesktopAnimator,
+            override val startTransitionToken: IBinder,
+            override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
+            override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
+            override var cancelTransitionToken: IBinder? = null,
+            override var homeToken: WindowContainerToken? = null,
+            override var draggedTaskChange: Change? = null,
+            override var cancelled: Boolean = false,
+            override var startAborted: Boolean = false,
+            var splitRootChange: Change? = null,
+            var otherSplitTask: Int
         ) : TransitionState()
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
index 74b8f83..526cf4d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
@@ -59,6 +59,7 @@
     private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
 
     private OnTaskResizeAnimationListener mOnTaskResizeAnimationListener;
+
     public EnterDesktopTaskTransitionHandler(
             Transitions transitions) {
         this(transitions, SurfaceControl.Transaction::new);
@@ -72,11 +73,12 @@
     }
 
     void setOnTaskResizeAnimationListener(OnTaskResizeAnimationListener listener) {
-        mOnTaskResizeAnimationListener =  listener;
+        mOnTaskResizeAnimationListener = listener;
     }
 
     /**
      * Starts Transition of type TRANSIT_MOVE_TO_DESKTOP
+     *
      * @param wct WindowContainerTransaction for transition
      * @return the token representing the started transition
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
index 7342bd1..9f9e256 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
@@ -56,7 +56,7 @@
     private final Transitions mTransitions;
     private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
     private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback;
-    private Supplier<SurfaceControl.Transaction> mTransactionSupplier;
+    private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
     private Point mPosition;
 
     public ExitDesktopTaskTransitionHandler(
@@ -76,9 +76,10 @@
 
     /**
      * Starts Transition of a given type
-     * @param type Transition type
-     * @param wct WindowContainerTransaction for transition
-     * @param position Position of the task when transition is started
+     *
+     * @param type                   Transition type
+     * @param wct                    WindowContainerTransaction for transition
+     * @param position               Position of the task when transition is started
      * @param onAnimationEndCallback to be called after animation
      */
     public void startTransition(@WindowManager.TransitionType int type,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
index c469e65..88d0554 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
@@ -86,9 +86,9 @@
                                 .setWindowCrop(leash, startBounds.width(), startBounds.height())
                                 .show(leash)
                             onTaskResizeAnimationListener.onAnimationStart(
-                                    taskId,
-                                    startTransaction,
-                                    startBounds
+                                taskId,
+                                startTransaction,
+                                startBounds
                             )
                         },
                         onEnd = {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index e0e2e706..7d2aa27 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -98,7 +98,7 @@
 
         if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
-                repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
+                repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId);
                 repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
                 if (taskInfo.isVisible) {
                     if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) {
@@ -120,7 +120,7 @@
 
         if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
-                repository.removeFreeformTask(taskInfo.taskId);
+                repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
                 repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
                 if (repository.removeActiveTask(taskInfo.taskId)) {
                     ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
@@ -167,7 +167,7 @@
                 taskInfo.taskId, taskInfo.isFocused);
         if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.isFocused) {
             mDesktopModeTaskRepository.ifPresent(repository -> {
-                repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
+                repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId);
                 repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
             });
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index c53e7fe..d8f2c02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -253,8 +253,12 @@
         notifyRunningTaskVanished(taskInfo);
     }
 
-    /** Notify listeners that the windowing mode of the given Task was updated. */
-    public void onTaskWindowingModeChanged(ActivityManager.RunningTaskInfo taskInfo) {
+    /**
+     * Notify listeners that the running infos related to recent tasks was updated.
+     *
+     * This currently includes windowing mode and visibility.
+     */
+    public void onTaskRunningInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
         notifyRecentTasksChanged();
         notifyRunningTaskChanged(taskInfo);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java
index 6adbe4f..456658c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.transition.tracing;
 
-import static android.tracing.perfetto.DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT;
+import static android.tracing.perfetto.DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP;
 
 import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMapping;
 import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMappings;
@@ -52,7 +52,7 @@
         DataSourceParams params =
                 new DataSourceParams.Builder()
                         .setBufferExhaustedPolicy(
-                                PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT)
+                                PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP)
                         .build();
         mDataSource.register(params);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 9afb057..74b091f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -278,11 +278,9 @@
             public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
                 if (visible && stage != STAGE_TYPE_UNDEFINED) {
                     DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId);
-                    if (decor == null || !DesktopModeStatus.canEnterDesktopMode(mContext)
-                            || decor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
-                        return;
+                    if (decor != null && DesktopModeStatus.canEnterDesktopMode(mContext)) {
+                        mDesktopTasksController.moveToSplit(decor.mTaskInfo);
                     }
-                    mDesktopTasksController.moveToSplit(decor.mTaskInfo);
                 }
             }
         });
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
index 53bdda1..78f0ef7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
@@ -28,7 +28,6 @@
 import android.widget.FrameLayout
 import android.widget.ImageButton
 import android.widget.ProgressBar
-import androidx.annotation.ColorInt
 import androidx.core.animation.doOnEnd
 import androidx.core.animation.doOnStart
 import androidx.core.content.ContextCompat
@@ -106,30 +105,17 @@
     fun setAnimationTints(
         darkMode: Boolean,
         iconForegroundColor: ColorStateList? = null,
-        baseForegroundColor: Int? = null
+        baseForegroundColor: Int? = null,
+        rippleDrawable: RippleDrawable? = null
     ) {
         if (Flags.enableThemedAppHeaders()) {
             requireNotNull(iconForegroundColor) { "Icon foreground color must be non-null" }
             requireNotNull(baseForegroundColor) { "Base foreground color must be non-null" }
+            requireNotNull(rippleDrawable) { "Ripple drawable must be non-null" }
             maximizeWindow.imageTintList = iconForegroundColor
-            maximizeWindow.background = RippleDrawable(
-                ColorStateList(
-                    arrayOf(
-                        intArrayOf(android.R.attr.state_hovered),
-                        intArrayOf(android.R.attr.state_pressed),
-                        intArrayOf(),
-                    ),
-                    intArrayOf(
-                        replaceColorAlpha(baseForegroundColor, OPACITY_8),
-                        replaceColorAlpha(baseForegroundColor, OPACITY_12),
-                        Color.TRANSPARENT
-                    )
-                ),
-                null,
-                null
-            )
+            maximizeWindow.background = rippleDrawable
             progressBar.progressTintList = ColorStateList.valueOf(baseForegroundColor)
-                .withAlpha(OPACITY_12)
+                .withAlpha(OPACITY_15)
             progressBar.progressBackgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT)
         } else {
             if (darkMode) {
@@ -146,18 +132,7 @@
         }
     }
 
-    @ColorInt
-    private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
-        return Color.argb(
-            alpha,
-            Color.red(color),
-            Color.green(color),
-            Color.blue(color)
-        )
-    }
-
     companion object {
-        private const val OPACITY_8 = 20
-        private const val OPACITY_12 = 31
+        private const val OPACITY_15 = 38
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
index 1c4b742..3c12da2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
@@ -9,6 +9,9 @@
 import android.graphics.Color
 import android.graphics.drawable.GradientDrawable
 import android.graphics.drawable.LayerDrawable
+import android.graphics.drawable.RippleDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.RoundRectShape
 import android.view.View
 import android.view.View.OnLongClickListener
 import android.widget.ImageButton
@@ -46,6 +49,38 @@
         onMaximizeHoverAnimationFinishedListener: () -> Unit
 ) : DesktopModeWindowDecorationViewHolder(rootView) {
 
+    /**
+     * The corner radius to apply to the app chip, maximize and close button's background drawable.
+     **/
+    private val headerButtonsRippleRadius = context.resources
+        .getDimensionPixelSize(R.dimen.desktop_mode_header_buttons_ripple_radius)
+
+    /**
+     * The app chip, maximize and close button's height extends to the top & bottom edges of the
+     * header, and their width may be larger than their height. This is by design to increase the
+     * clickable and hover-able bounds of the view as much as possible. However, to prevent the
+     * ripple drawable from being as large as the views (and asymmetrical), insets are applied to
+     * the background ripple drawable itself to give the appearance of a smaller button
+     * (with padding between itself and the header edges / sibling buttons) but without affecting
+     * its touchable region.
+     */
+    private val appChipDrawableInsets = DrawableInsets(
+        vertical = context.resources
+            .getDimensionPixelSize(R.dimen.desktop_mode_header_app_chip_ripple_inset_vertical)
+    )
+    private val maximizeDrawableInsets = DrawableInsets(
+        vertical = context.resources
+            .getDimensionPixelSize(R.dimen.desktop_mode_header_maximize_ripple_inset_vertical),
+        horizontal = context.resources
+            .getDimensionPixelSize(R.dimen.desktop_mode_header_maximize_ripple_inset_horizontal)
+    )
+    private val closeDrawableInsets = DrawableInsets(
+        vertical = context.resources
+            .getDimensionPixelSize(R.dimen.desktop_mode_header_close_ripple_inset_vertical),
+        horizontal = context.resources
+            .getDimensionPixelSize(R.dimen.desktop_mode_header_close_ripple_inset_horizontal)
+    )
+
     private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
     private val captionHandle: View = rootView.requireViewById(R.id.caption_handle)
     private val openMenuButton: View = rootView.requireViewById(R.id.open_menu_button)
@@ -97,7 +132,19 @@
         maximizeWindowButton.imageAlpha = alpha
         closeWindowButton.imageAlpha = alpha
         expandMenuButton.imageAlpha = alpha
-
+        context.withStyledAttributes(
+            set = null,
+            attrs = intArrayOf(
+                android.R.attr.selectableItemBackground,
+                android.R.attr.selectableItemBackgroundBorderless
+            ),
+            defStyleAttr = 0,
+            defStyleRes = 0
+        ) {
+            openMenuButton.background = getDrawable(0)
+            maximizeWindowButton.background = getDrawable(1)
+            closeWindowButton.background = getDrawable(1)
+        }
         maximizeButtonView.setAnimationTints(isDarkMode())
     }
 
@@ -126,18 +173,40 @@
         val foregroundColor = headerStyle.foreground.color
         val foregroundAlpha = headerStyle.foreground.opacity
         val colorStateList = ColorStateList.valueOf(foregroundColor).withAlpha(foregroundAlpha)
-        closeWindowButton.imageTintList = colorStateList
-        expandMenuButton.imageTintList = colorStateList
-        with (appNameTextView) {
-            isVisible = header.type == Header.Type.DEFAULT
-            setTextColor(colorStateList)
+        // App chip.
+        openMenuButton.apply {
+            background = createRippleDrawable(
+                color = foregroundColor,
+                cornerRadius = headerButtonsRippleRadius,
+                drawableInsets = appChipDrawableInsets,
+            )
+            expandMenuButton.imageTintList = colorStateList
+            appNameTextView.apply {
+                isVisible = header.type == Header.Type.DEFAULT
+                setTextColor(colorStateList)
+            }
+            appIconImageView.imageAlpha = foregroundAlpha
         }
-        appIconImageView.imageAlpha = foregroundAlpha
+        // Maximize button.
         maximizeButtonView.setAnimationTints(
             darkMode = header.appTheme == Header.Theme.DARK,
             iconForegroundColor = colorStateList,
-            baseForegroundColor = foregroundColor
+            baseForegroundColor = foregroundColor,
+            rippleDrawable = createRippleDrawable(
+                color = foregroundColor,
+                cornerRadius = headerButtonsRippleRadius,
+                drawableInsets = maximizeDrawableInsets
+            )
         )
+        // Close button.
+        closeWindowButton.apply {
+            imageTintList = colorStateList
+            background = createRippleDrawable(
+                color = foregroundColor,
+                cornerRadius = headerButtonsRippleRadius,
+                drawableInsets = closeDrawableInsets
+            )
+        }
     }
 
     override fun onHandleMenuOpened() {}
@@ -390,10 +459,61 @@
         context.withStyledAttributes(null, intArrayOf(attr), 0, 0) {
             return getColor(0, 0)
         }
-        return Color.BLACK
+        return Color.WHITE
     }
 
-    data class Header(
+    @ColorInt
+    private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
+        return Color.argb(
+            alpha,
+            Color.red(color),
+            Color.green(color),
+            Color.blue(color)
+        )
+    }
+
+    private fun createRippleDrawable(
+        @ColorInt color: Int,
+        cornerRadius: Int,
+        drawableInsets: DrawableInsets,
+    ): RippleDrawable {
+        return RippleDrawable(
+            ColorStateList(
+                arrayOf(
+                    intArrayOf(android.R.attr.state_hovered),
+                    intArrayOf(android.R.attr.state_pressed),
+                    intArrayOf(),
+                ),
+                intArrayOf(
+                    replaceColorAlpha(color, OPACITY_11),
+                    replaceColorAlpha(color, OPACITY_15),
+                    Color.TRANSPARENT
+                )
+            ),
+            null /* content */,
+            LayerDrawable(arrayOf(
+                ShapeDrawable().apply {
+                    shape = RoundRectShape(
+                        FloatArray(8) { cornerRadius.toFloat() },
+                        null /* inset */,
+                        null /* innerRadii */
+                    )
+                    paint.color = Color.WHITE
+                }
+            )).apply {
+                require(numberOfLayers == 1) { "Must only contain one layer" }
+                setLayerInset(0 /* index */,
+                    drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
+            }
+        )
+    }
+
+    private data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) {
+        constructor(vertical: Int = 0, horizontal: Int = 0) :
+                this(horizontal, vertical, horizontal, vertical)
+    }
+
+    private data class Header(
         val type: Type,
         val systemTheme: Theme,
         val appTheme: Theme,
@@ -408,7 +528,7 @@
 
     private fun Header.Theme.isDark(): Boolean = this == Header.Theme.DARK
 
-    data class HeaderStyle(
+    private data class HeaderStyle(
         val background: Background,
         val foreground: Foreground
     ) {
@@ -497,6 +617,8 @@
         private const val FOCUSED_OPACITY = 255
 
         private const val OPACITY_100 = 255
+        private const val OPACITY_11 = 28
+        private const val OPACITY_15 = 38
         private const val OPACITY_30 = 77
         private const val OPACITY_55 = 140
         private const val OPACITY_65 = 166
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
index f2f10ae..d03d779 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
@@ -188,6 +188,13 @@
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
         super.visibleLayersShownMoreThanOneConsecutiveEntry()
 
+    /** {@inheritDoc} */
+    @Test
+    @FlakyTest(bugId = 336510055)
+    override fun entireScreenCovered() {
+        super.entireScreenCovered()
+    }
+
     companion object {
         @Parameterized.Parameters(name = "{0}")
         @JvmStatic
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index 82c070c..f9b4108 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -20,6 +20,7 @@
 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
 import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
@@ -62,6 +63,7 @@
 
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.compatui.CompatUIController;
+import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellInit;
 
@@ -78,7 +80,7 @@
  * Tests for the shell task organizer.
  *
  * Build/Install/Run:
- *  atest WMShellUnitTests:ShellTaskOrganizerTests
+ * atest WMShellUnitTests:ShellTaskOrganizerTests
  */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -92,6 +94,8 @@
     private ShellExecutor mTestExecutor;
     @Mock
     private ShellCommandHandler mShellCommandHandler;
+    @Mock
+    private RecentTasksController mRecentTasksController;
 
     private ShellTaskOrganizer mOrganizer;
     private ShellInit mShellInit;
@@ -120,6 +124,7 @@
     private class TrackingLocusIdListener implements ShellTaskOrganizer.LocusIdListener {
         final SparseArray<LocusId> visibleLocusTasks = new SparseArray<>();
         final SparseArray<LocusId> invisibleLocusTasks = new SparseArray<>();
+
         @Override
         public void onVisibilityChanged(int taskId, LocusId locus, boolean visible) {
             if (visible) {
@@ -130,18 +135,18 @@
         }
     }
 
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         try {
             doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList())
                     .when(mTaskOrganizerController).registerTaskOrganizer(any());
-        } catch (RemoteException e) {}
+        } catch (RemoteException e) {
+        }
         mShellInit = spy(new ShellInit(mTestExecutor));
         mOrganizer = spy(new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
-                mTaskOrganizerController, mCompatUI, Optional.empty(), Optional.empty(),
-                mTestExecutor));
+                mTaskOrganizerController, mCompatUI, Optional.empty(),
+                Optional.of(mRecentTasksController), mTestExecutor));
         mShellInit.init();
     }
 
@@ -163,7 +168,7 @@
     @Test
     public void testTaskLeashReleasedAfterVanished() throws RemoteException {
         assumeFalse(ENABLE_SHELL_TRANSITIONS);
-        RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         SurfaceControl taskLeash = new SurfaceControl.Builder(new SurfaceSession())
                 .setName("task").build();
         mOrganizer.registerOrganizer();
@@ -188,8 +193,8 @@
     @Test
     public void testRegisterWithExistingTasks() throws RemoteException {
         // Setup some tasks
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
-        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_MULTI_WINDOW);
         ArrayList<TaskAppearedInfo> taskInfos = new ArrayList<>();
         taskInfos.add(new TaskAppearedInfo(task1, new SurfaceControl()));
         taskInfos.add(new TaskAppearedInfo(task2, new SurfaceControl()));
@@ -208,10 +213,10 @@
 
     @Test
     public void testAppearedVanished() {
-        RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         TrackingTaskListener listener = new TrackingTaskListener();
         mOrganizer.addListenerForType(listener, TASK_LISTENER_TYPE_MULTI_WINDOW);
-        mOrganizer.onTaskAppeared(taskInfo, null);
+        mOrganizer.onTaskAppeared(taskInfo, /* leash= */ null);
         assertTrue(listener.appeared.contains(taskInfo));
 
         mOrganizer.onTaskVanished(taskInfo);
@@ -220,7 +225,7 @@
 
     @Test
     public void testAddListenerExistingTasks() {
-        RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         mOrganizer.onTaskAppeared(taskInfo, null);
 
         TrackingTaskListener listener = new TrackingTaskListener();
@@ -230,9 +235,9 @@
 
     @Test
     public void testAddListenerForMultipleTypes() {
-        RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
+        RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN);
         mOrganizer.onTaskAppeared(taskInfo1, null);
-        RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo taskInfo2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_MULTI_WINDOW);
         mOrganizer.onTaskAppeared(taskInfo2, null);
 
         TrackingTaskListener listener = new TrackingTaskListener();
@@ -247,10 +252,10 @@
 
     @Test
     public void testRemoveListenerForMultipleTypes() {
-        RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
-        mOrganizer.onTaskAppeared(taskInfo1, null);
-        RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
-        mOrganizer.onTaskAppeared(taskInfo2, null);
+        RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN);
+        mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null);
+        RunningTaskInfo taskInfo2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_MULTI_WINDOW);
+        mOrganizer.onTaskAppeared(taskInfo2, /* leash= */ null);
 
         TrackingTaskListener listener = new TrackingTaskListener();
         mOrganizer.addListenerForType(listener,
@@ -267,12 +272,12 @@
 
     @Test
     public void testWindowingModeChange() {
-        RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         TrackingTaskListener mwListener = new TrackingTaskListener();
         TrackingTaskListener pipListener = new TrackingTaskListener();
         mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
         mOrganizer.addListenerForType(pipListener, TASK_LISTENER_TYPE_PIP);
-        mOrganizer.onTaskAppeared(taskInfo, null);
+        mOrganizer.onTaskAppeared(taskInfo, /* leash= */ null);
         assertTrue(mwListener.appeared.contains(taskInfo));
         assertTrue(pipListener.appeared.isEmpty());
 
@@ -284,11 +289,11 @@
 
     @Test
     public void testAddListenerForTaskId_afterTypeListener() {
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         TrackingTaskListener mwListener = new TrackingTaskListener();
         TrackingTaskListener task1Listener = new TrackingTaskListener();
         mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
-        mOrganizer.onTaskAppeared(task1, null);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
         assertTrue(mwListener.appeared.contains(task1));
 
         // Add task 1 specific listener
@@ -299,11 +304,11 @@
 
     @Test
     public void testAddListenerForTaskId_beforeTypeListener() {
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         TrackingTaskListener mwListener = new TrackingTaskListener();
         TrackingTaskListener task1Listener = new TrackingTaskListener();
-        mOrganizer.onTaskAppeared(task1, null);
-        mOrganizer.addListenerForTaskId(task1Listener, 1);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+        mOrganizer.addListenerForTaskId(task1Listener, /* taskId= */ 1);
         assertTrue(task1Listener.appeared.contains(task1));
 
         mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
@@ -312,7 +317,7 @@
 
     @Test
     public void testGetTaskListener() {
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
 
         TrackingTaskListener mwListener = new TrackingTaskListener();
         mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
@@ -324,7 +329,7 @@
 
         // Priority goes to the cookie listener so we would expect the task appear to show up there
         // instead of the multi-window type listener.
-        mOrganizer.onTaskAppeared(task1, null);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
         assertTrue(cookieListener.appeared.contains(task1));
         assertFalse(mwListener.appeared.contains(task1));
 
@@ -332,7 +337,7 @@
 
         boolean gotException = false;
         try {
-            mOrganizer.addListenerForTaskId(task1Listener, 1);
+            mOrganizer.addListenerForTaskId(task1Listener, /* taskId= */ 1);
         } catch (Exception e) {
             gotException = true;
         }
@@ -343,26 +348,27 @@
 
     @Test
     public void testGetParentTaskListener() {
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         TrackingTaskListener mwListener = new TrackingTaskListener();
-        mOrganizer.onTaskAppeared(task1, null);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
         mOrganizer.addListenerForTaskId(mwListener, task1.taskId);
         RunningTaskInfo task2 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
         task2.parentTaskId = task1.taskId;
 
-        mOrganizer.onTaskAppeared(task2, null);
+        mOrganizer.onTaskAppeared(task2, /* leash= */ null);
 
         assertTrue(mwListener.appeared.contains(task2));
     }
 
     @Test
     public void testOnSizeCompatActivityChanged() {
-        final RunningTaskInfo taskInfo1 = createTaskInfo(12, WINDOWING_MODE_FULLSCREEN);
+        final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 12,
+                WINDOWING_MODE_FULLSCREEN);
         taskInfo1.displayId = DEFAULT_DISPLAY;
         taskInfo1.appCompatTaskInfo.topActivityInSizeCompat = false;
         final TrackingTaskListener taskListener = new TrackingTaskListener();
         mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN);
-        mOrganizer.onTaskAppeared(taskInfo1, null);
+        mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null);
 
         // sizeCompatActivity is null if top activity is not in size compat.
         verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */);
@@ -394,12 +400,13 @@
 
     @Test
     public void testOnEligibleForLetterboxEducationActivityChanged() {
-        final RunningTaskInfo taskInfo1 = createTaskInfo(12, WINDOWING_MODE_FULLSCREEN);
+        final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 12,
+                WINDOWING_MODE_FULLSCREEN);
         taskInfo1.displayId = DEFAULT_DISPLAY;
         taskInfo1.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = false;
         final TrackingTaskListener taskListener = new TrackingTaskListener();
         mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN);
-        mOrganizer.onTaskAppeared(taskInfo1, null);
+        mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null);
 
         // Task listener sent to compat UI is null if top activity isn't eligible for letterbox
         // education.
@@ -433,13 +440,14 @@
 
     @Test
     public void testOnCameraCompatActivityChanged() {
-        final RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
+        final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 1,
+                WINDOWING_MODE_FULLSCREEN);
         taskInfo1.displayId = DEFAULT_DISPLAY;
         taskInfo1.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
                 CAMERA_COMPAT_CONTROL_HIDDEN;
         final TrackingTaskListener taskListener = new TrackingTaskListener();
         mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN);
-        mOrganizer.onTaskAppeared(taskInfo1, null);
+        mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null);
 
         // Task listener sent to compat UI is null if top activity doesn't request a camera
         // compat control.
@@ -510,20 +518,20 @@
 
     @Test
     public void testAddLocusListener() {
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         task1.isVisible = true;
         task1.mTopActivityLocusId = new LocusId("10");
 
-        RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_FULLSCREEN);
+        RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_FULLSCREEN);
         task2.isVisible = true;
         task2.mTopActivityLocusId = new LocusId("20");
 
-        RunningTaskInfo task3 = createTaskInfo(3, WINDOWING_MODE_FULLSCREEN);
+        RunningTaskInfo task3 = createTaskInfo(/* taskId= */ 3, WINDOWING_MODE_FULLSCREEN);
         task3.isVisible = true;
 
-        mOrganizer.onTaskAppeared(task1, null);
-        mOrganizer.onTaskAppeared(task2, null);
-        mOrganizer.onTaskAppeared(task3, null);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+        mOrganizer.onTaskAppeared(task2, /* leash= */ null);
+        mOrganizer.onTaskAppeared(task3, /* leash= */ null);
 
         TrackingLocusIdListener listener = new TrackingLocusIdListener();
         mOrganizer.addLocusIdListener(listener);
@@ -539,11 +547,11 @@
         TrackingLocusIdListener listener = new TrackingLocusIdListener();
         mOrganizer.addLocusIdListener(listener);
 
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN);
         task1.mTopActivityLocusId = new LocusId("10");
 
         task1.isVisible = true;
-        mOrganizer.onTaskAppeared(task1, null);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
         assertTrue(listener.visibleLocusTasks.contains(task1.taskId));
         assertEquals(listener.visibleLocusTasks.get(task1.taskId), task1.mTopActivityLocusId);
 
@@ -558,9 +566,9 @@
         TrackingLocusIdListener listener = new TrackingLocusIdListener();
         mOrganizer.addLocusIdListener(listener);
 
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         task1.isVisible = true;
-        mOrganizer.onTaskAppeared(task1, null);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
         assertEquals(listener.visibleLocusTasks.size(), 0);
 
         task1.mTopActivityLocusId = new LocusId("10");
@@ -585,9 +593,9 @@
         TrackingLocusIdListener listener = new TrackingLocusIdListener();
         mOrganizer.addLocusIdListener(listener);
 
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN);
         task1.isVisible = true;
-        mOrganizer.onTaskAppeared(task1, null);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
 
         task1.mTopActivityLocusId = new LocusId("10");
         mOrganizer.onTaskInfoChanged(task1);
@@ -609,9 +617,9 @@
         TrackingLocusIdListener listener = new TrackingLocusIdListener();
         mOrganizer.addLocusIdListener(listener);
 
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         task1.isVisible = true;
-        mOrganizer.onTaskAppeared(task1, null);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
         assertEquals(listener.visibleLocusTasks.size(), 0);
         assertEquals(listener.invisibleLocusTasks.size(), 0);
 
@@ -627,20 +635,63 @@
 
     @Test
     public void testOnSizeCompatRestartButtonClicked() throws RemoteException {
-        RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
         task1.token = mock(WindowContainerToken.class);
 
-        mOrganizer.onTaskAppeared(task1, null);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
 
         mOrganizer.onSizeCompatRestartButtonClicked(task1.taskId);
 
         verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token);
     }
 
+    @Test
+    public void testRecentTasks_onTaskAppeared_shouldNotifyTaskController() {
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+
+        mOrganizer.onTaskAppeared(task1, null);
+
+        verify(mRecentTasksController).onTaskAdded(task1);
+    }
+
+    @Test
+    public void testRecentTasks_onTaskVanished_shouldNotifyTaskController() {
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+
+        mOrganizer.onTaskVanished(task1);
+
+        verify(mRecentTasksController).onTaskRemoved(task1);
+    }
+
+    @Test
+    public void testRecentTasks_visibilityChanges_shouldNotifyTaskController() {
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+        RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+        task2.isVisible = false;
+
+        mOrganizer.onTaskInfoChanged(task2);
+
+        verify(mRecentTasksController).onTaskRunningInfoChanged(task2);
+    }
+
+    @Test
+    public void testRecentTasks_windowingModeChanges_shouldNotifyTaskController() {
+        RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN);
+        mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+        RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+
+        mOrganizer.onTaskInfoChanged(task2);
+
+        verify(mRecentTasksController).onTaskRunningInfoChanged(task2);
+    }
+
     private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
         RunningTaskInfo taskInfo = new RunningTaskInfo();
         taskInfo.taskId = taskId;
         taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
+        taskInfo.isVisible = true;
         return taskInfo;
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 8f59f30..310ccc2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -363,11 +363,11 @@
 
     @Test
     fun addOrMoveFreeformTaskToTop_didNotExist_addsToTop() {
-        repo.addOrMoveFreeformTaskToTop(5)
-        repo.addOrMoveFreeformTaskToTop(6)
-        repo.addOrMoveFreeformTaskToTop(7)
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5)
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6)
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7)
 
-        val tasks = repo.getFreeformTasksInZOrder()
+        val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
         assertThat(tasks.size).isEqualTo(3)
         assertThat(tasks[0]).isEqualTo(7)
         assertThat(tasks[1]).isEqualTo(6)
@@ -376,13 +376,13 @@
 
     @Test
     fun addOrMoveFreeformTaskToTop_alreadyExists_movesToTop() {
-        repo.addOrMoveFreeformTaskToTop(5)
-        repo.addOrMoveFreeformTaskToTop(6)
-        repo.addOrMoveFreeformTaskToTop(7)
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5)
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6)
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7)
 
-        repo.addOrMoveFreeformTaskToTop(6)
+        repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6)
 
-        val tasks = repo.getFreeformTasksInZOrder()
+        val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
         assertThat(tasks.size).isEqualTo(3)
         assertThat(tasks.first()).isEqualTo(6)
     }
@@ -391,7 +391,7 @@
     fun removeFreeformTask_removesTaskBoundsBeforeMaximize() {
         val taskId = 1
         repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200))
-        repo.removeFreeformTask(taskId)
+        repo.removeFreeformTask(THIRD_DISPLAY, taskId)
         assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull()
     }
 
@@ -480,31 +480,31 @@
 
     @Test
     fun getActiveNonMinimizedTasksOrderedFrontToBack_returnsFreeformTasksInCorrectOrder() {
-        repo.addActiveTask(displayId = 0, taskId = 1)
-        repo.addActiveTask(displayId = 0, taskId = 2)
-        repo.addActiveTask(displayId = 0, taskId = 3)
+        repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1)
+        repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2)
+        repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3)
         // The front-most task will be the one added last through addOrMoveFreeformTaskToTop
-        repo.addOrMoveFreeformTaskToTop(taskId = 3)
-        repo.addOrMoveFreeformTaskToTop(taskId = 2)
-        repo.addOrMoveFreeformTaskToTop(taskId = 1)
+        repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 3)
+        repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 2)
+        repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 1)
 
-        assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)).isEqualTo(
-                listOf(1, 2, 3))
+        assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0))
+            .containsExactly(1, 2, 3).inOrder()
     }
 
     @Test
     fun getActiveNonMinimizedTasksOrderedFrontToBack_minimizedTaskNotIncluded() {
-        repo.addActiveTask(displayId = 0, taskId = 1)
-        repo.addActiveTask(displayId = 0, taskId = 2)
-        repo.addActiveTask(displayId = 0, taskId = 3)
+        repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1)
+        repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2)
+        repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3)
         // The front-most task will be the one added last through addOrMoveFreeformTaskToTop
-        repo.addOrMoveFreeformTaskToTop(taskId = 3)
-        repo.addOrMoveFreeformTaskToTop(taskId = 2)
-        repo.addOrMoveFreeformTaskToTop(taskId = 1)
-        repo.minimizeTask(displayId = 0, taskId = 2)
+        repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 3)
+        repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 2)
+        repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 1)
+        repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2)
 
-        assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)).isEqualTo(
-                listOf(1, 3))
+        assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(
+            displayId = DEFAULT_DISPLAY)).containsExactly(1, 3).inOrder()
     }
 
 
@@ -544,5 +544,6 @@
 
     companion object {
         const val SECOND_DISPLAY = 1
+        const val THIRD_DISPLAY = 345
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index d8d534b..ac67bd1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -1614,7 +1614,7 @@
         val task = createFreeformTask(displayId, bounds)
         whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
         desktopModeTaskRepository.addActiveTask(displayId, task.taskId)
-        desktopModeTaskRepository.addOrMoveFreeformTaskToTop(task.taskId)
+        desktopModeTaskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId)
         runningTasks.add(task)
         return task
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
index 3c488ca..77f917c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -298,7 +298,7 @@
         val task = createFreeformTask(displayId)
         `when`(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
         desktopTaskRepo.addActiveTask(displayId, task.taskId)
-        desktopTaskRepo.addOrMoveFreeformTaskToTop(task.taskId)
+        desktopTaskRepo.addOrMoveFreeformTaskToTop(displayId, task.taskId)
         return task
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index cd68c69..3f3cafc 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -91,7 +91,8 @@
 
         mFreeformTaskListener.onFocusTaskChanged(task);
 
-        verify(mDesktopModeTaskRepository).addOrMoveFreeformTaskToTop(task.taskId);
+        verify(mDesktopModeTaskRepository)
+            .addOrMoveFreeformTaskToTop(task.displayId, task.taskId);
     }
 
     @Test
@@ -103,7 +104,7 @@
         mFreeformTaskListener.onFocusTaskChanged(fullscreenTask);
 
         verify(mDesktopModeTaskRepository, never())
-                .addOrMoveFreeformTaskToTop(fullscreenTask.taskId);
+                .addOrMoveFreeformTaskToTop(fullscreenTask.displayId, fullscreenTask.taskId);
     }
 
     @After
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 884cb6e..56c4cea 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -511,7 +511,7 @@
         mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
         ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
 
-        mRecentTasksControllerReal.onTaskWindowingModeChanged(taskInfo);
+        mRecentTasksControllerReal.onTaskRunningInfoChanged(taskInfo);
 
         verify(mRecentTasksListener).onRunningTaskChanged(taskInfo);
     }
@@ -525,7 +525,7 @@
         mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
         ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
 
-        mRecentTasksControllerReal.onTaskWindowingModeChanged(taskInfo);
+        mRecentTasksControllerReal.onTaskRunningInfoChanged(taskInfo);
 
         verify(mRecentTasksListener, never()).onRunningTaskChanged(any());
     }
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 63b4538..386a606c 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -8617,6 +8617,7 @@
     @SystemApi
     @NonNull
     @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING)
+    // TODO also open to MODIFY_AUDIO_SETTINGS_PRIVILEGED b/341780042
     public static List<AudioVolumeGroup> getAudioVolumeGroups() {
         final IAudioService service = getService();
         try {
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index e612645..c8b9da5 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -142,7 +142,7 @@
     @UnsupportedAppUsage
     int getStreamMaxVolume(int streamType);
 
-    @EnforcePermission("MODIFY_AUDIO_ROUTING")
+    @EnforcePermission(anyOf={"MODIFY_AUDIO_SETTINGS_PRIVILEGED", "MODIFY_AUDIO_ROUTING"})
     List<AudioVolumeGroup> getAudioVolumeGroups();
 
     @EnforcePermission(anyOf={"MODIFY_AUDIO_SETTINGS_PRIVILEGED", "MODIFY_AUDIO_ROUTING"})
diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java
index 838630f..0589c0f12 100644
--- a/media/java/android/media/MediaRoute2Info.java
+++ b/media/java/android/media/MediaRoute2Info.java
@@ -523,7 +523,7 @@
     private final int mConnectionState;
     private final String mClientPackageName;
     private final String mPackageName;
-    private final int mVolumeHandling;
+    @PlaybackVolume private final int mVolumeHandling;
     private final int mVolumeMax;
     private final int mVolume;
     private final String mAddress;
@@ -855,25 +855,7 @@
     }
 
     private void dumpVolume(@NonNull PrintWriter pw, @NonNull String prefix) {
-        String volumeHandlingName;
-
-        switch (mVolumeHandling) {
-            case PLAYBACK_VOLUME_FIXED:
-                volumeHandlingName = "FIXED";
-                break;
-            case PLAYBACK_VOLUME_VARIABLE:
-                volumeHandlingName = "VARIABLE";
-                break;
-            default:
-                volumeHandlingName = "UNKNOWN";
-                break;
-        }
-
-        String volume = String.format(Locale.US,
-                "volume(current=%d, max=%d, handling=%s(%d))",
-                mVolume, mVolumeMax, volumeHandlingName, mVolumeHandling);
-
-        pw.println(prefix + volume);
+        pw.println(prefix + getVolumeString(mVolume, mVolumeMax, mVolumeHandling));
     }
 
     @Override
@@ -936,47 +918,42 @@
     @Override
     public String toString() {
         // Note: mExtras is not printed here.
-        StringBuilder result =
-                new StringBuilder()
-                        .append("MediaRoute2Info{ ")
-                        .append("id=")
-                        .append(getId())
-                        .append(", name=")
-                        .append(getName())
-                        .append(", type=")
-                        .append(getDeviceTypeString(getType()))
-                        .append(", isSystem=")
-                        .append(isSystemRoute())
-                        .append(", features=")
-                        .append(getFeatures())
-                        .append(", iconUri=")
-                        .append(getIconUri())
-                        .append(", description=")
-                        .append(getDescription())
-                        .append(", connectionState=")
-                        .append(getConnectionState())
-                        .append(", clientPackageName=")
-                        .append(getClientPackageName())
-                        .append(", volumeHandling=")
-                        .append(getVolumeHandling())
-                        .append(", volumeMax=")
-                        .append(getVolumeMax())
-                        .append(", volume=")
-                        .append(getVolume())
-                        .append(", address=")
-                        .append(getAddress())
-                        .append(", deduplicationIds=")
-                        .append(String.join(",", getDeduplicationIds()))
-                        .append(", providerId=")
-                        .append(getProviderId())
-                        .append(", isVisibilityRestricted=")
-                        .append(mIsVisibilityRestricted)
-                        .append(", allowedPackages=")
-                        .append(String.join(",", mAllowedPackages))
-                        .append(", suitabilityStatus=")
-                        .append(mSuitabilityStatus)
-                        .append(" }");
-        return result.toString();
+        return new StringBuilder()
+                .append("MediaRoute2Info{ ")
+                .append("id=")
+                .append(getId())
+                .append(", name=")
+                .append(getName())
+                .append(", type=")
+                .append(getDeviceTypeString(getType()))
+                .append(", isSystem=")
+                .append(isSystemRoute())
+                .append(", features=")
+                .append(getFeatures())
+                .append(", iconUri=")
+                .append(getIconUri())
+                .append(", description=")
+                .append(getDescription())
+                .append(", connectionState=")
+                .append(getConnectionState())
+                .append(", clientPackageName=")
+                .append(getClientPackageName())
+                .append(", ")
+                .append(getVolumeString(mVolume, mVolumeMax, mVolumeHandling))
+                .append(", address=")
+                .append(getAddress())
+                .append(", deduplicationIds=")
+                .append(String.join(",", getDeduplicationIds()))
+                .append(", providerId=")
+                .append(getProviderId())
+                .append(", isVisibilityRestricted=")
+                .append(mIsVisibilityRestricted)
+                .append(", allowedPackages=")
+                .append(String.join(",", mAllowedPackages))
+                .append(", suitabilityStatus=")
+                .append(mSuitabilityStatus)
+                .append(" }")
+                .toString();
     }
 
     @Override
@@ -1008,6 +985,36 @@
         dest.writeInt(mSuitabilityStatus);
     }
 
+    /**
+     * Returns a human readable string describing the given volume values.
+     *
+     * @param volume The current volume.
+     * @param maxVolume The maximum volume.
+     * @param volumeHandling The {@link PlaybackVolume}.
+     */
+    /* package */ static String getVolumeString(
+            int volume, int maxVolume, @PlaybackVolume int volumeHandling) {
+        String volumeHandlingName;
+        switch (volumeHandling) {
+            case PLAYBACK_VOLUME_FIXED:
+                volumeHandlingName = "FIXED";
+                break;
+            case PLAYBACK_VOLUME_VARIABLE:
+                volumeHandlingName = "VARIABLE";
+                break;
+            default:
+                volumeHandlingName = "UNKNOWN";
+                break;
+        }
+        return String.format(
+                Locale.US,
+                "volume(current=%d, max=%d, handling=%s(%d))",
+                volume,
+                maxVolume,
+                volumeHandlingName,
+                volumeHandling);
+    }
+
     private static String getDeviceTypeString(@Type int deviceType) {
         switch (deviceType) {
             case TYPE_BUILTIN_SPEAKER:
@@ -1079,7 +1086,7 @@
         private int mConnectionState;
         private String mClientPackageName;
         private String mPackageName;
-        private int mVolumeHandling = PLAYBACK_VOLUME_FIXED;
+        @PlaybackVolume private int mVolumeHandling = PLAYBACK_VOLUME_FIXED;
         private int mVolumeMax;
         private int mVolume;
         private String mAddress;
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index e62d112..8fa0e49 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -402,9 +402,6 @@
     @Nullable
     public RoutingSessionInfo getRoutingSessionForMediaController(MediaController mediaController) {
         MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo();
-        if (playbackInfo == null) {
-            return null;
-        }
         if (playbackInfo.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) {
             return getSystemRoutingSession(mediaController.getPackageName());
         }
@@ -959,10 +956,6 @@
     private boolean areSessionsMatched(MediaController mediaController,
             RoutingSessionInfo sessionInfo) {
         MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo();
-        if (playbackInfo == null) {
-            return false;
-        }
-
         String volumeControlId = playbackInfo.getVolumeControlId();
         if (volumeControlId == null) {
             return false;
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index a3c8b68..9899e4e 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -107,7 +107,7 @@
     @NonNull
     final List<String> mTransferableRoutes;
 
-    final int mVolumeHandling;
+    @MediaRoute2Info.PlaybackVolume final int mVolumeHandling;
     final int mVolumeMax;
     final int mVolume;
 
@@ -207,9 +207,10 @@
         return controlHints;
     }
 
+    @MediaRoute2Info.PlaybackVolume
     private static int defineVolumeHandling(
             boolean isSystemSession,
-            int volumeHandling,
+            @MediaRoute2Info.PlaybackVolume int volumeHandling,
             List<String> selectedRoutes,
             boolean volumeAdjustmentForRemoteGroupSessions) {
         if (!isSystemSession
@@ -439,9 +440,7 @@
         pw.println(indent + "mSelectableRoutes=" + mSelectableRoutes);
         pw.println(indent + "mDeselectableRoutes=" + mDeselectableRoutes);
         pw.println(indent + "mTransferableRoutes=" + mTransferableRoutes);
-        pw.println(indent + "mVolumeHandling=" + mVolumeHandling);
-        pw.println(indent + "mVolumeMax=" + mVolumeMax);
-        pw.println(indent + "mVolume=" + mVolume);
+        pw.println(indent + MediaRoute2Info.getVolumeString(mVolume, mVolumeMax, mVolumeHandling));
         pw.println(indent + "mControlHints=" + mControlHints);
         pw.println(indent + "mIsSystemSession=" + mIsSystemSession);
         pw.println(indent + "mTransferReason=" + mTransferReason);
@@ -503,41 +502,36 @@
 
     @Override
     public String toString() {
-        StringBuilder result =
-                new StringBuilder()
-                        .append("RoutingSessionInfo{ ")
-                        .append("sessionId=")
-                        .append(getId())
-                        .append(", name=")
-                        .append(getName())
-                        .append(", clientPackageName=")
-                        .append(getClientPackageName())
-                        .append(", selectedRoutes={")
-                        .append(String.join(",", getSelectedRoutes()))
-                        .append("}")
-                        .append(", selectableRoutes={")
-                        .append(String.join(",", getSelectableRoutes()))
-                        .append("}")
-                        .append(", deselectableRoutes={")
-                        .append(String.join(",", getDeselectableRoutes()))
-                        .append("}")
-                        .append(", transferableRoutes={")
-                        .append(String.join(",", getTransferableRoutes()))
-                        .append("}")
-                        .append(", volumeHandling=")
-                        .append(getVolumeHandling())
-                        .append(", volumeMax=")
-                        .append(getVolumeMax())
-                        .append(", volume=")
-                        .append(getVolume())
-                        .append(", transferReason=")
-                        .append(getTransferReason())
-                        .append(", transferInitiatorUserHandle=")
-                        .append(getTransferInitiatorUserHandle())
-                        .append(", transferInitiatorPackageName=")
-                        .append(getTransferInitiatorPackageName())
-                        .append(" }");
-        return result.toString();
+        return new StringBuilder()
+                .append("RoutingSessionInfo{ ")
+                .append("sessionId=")
+                .append(getId())
+                .append(", name=")
+                .append(getName())
+                .append(", clientPackageName=")
+                .append(getClientPackageName())
+                .append(", selectedRoutes={")
+                .append(String.join(",", getSelectedRoutes()))
+                .append("}")
+                .append(", selectableRoutes={")
+                .append(String.join(",", getSelectableRoutes()))
+                .append("}")
+                .append(", deselectableRoutes={")
+                .append(String.join(",", getDeselectableRoutes()))
+                .append("}")
+                .append(", transferableRoutes={")
+                .append(String.join(",", getTransferableRoutes()))
+                .append("}")
+                .append(", ")
+                .append(MediaRoute2Info.getVolumeString(mVolume, mVolumeMax, mVolumeHandling))
+                .append(", transferReason=")
+                .append(getTransferReason())
+                .append(", transferInitiatorUserHandle=")
+                .append(getTransferInitiatorUserHandle())
+                .append(", transferInitiatorPackageName=")
+                .append(getTransferInitiatorPackageName())
+                .append(" }")
+                .toString();
     }
 
     /**
@@ -586,6 +580,7 @@
         private final List<String> mDeselectableRoutes;
         @NonNull
         private final List<String> mTransferableRoutes;
+        @MediaRoute2Info.PlaybackVolume
         private int mVolumeHandling = MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
         private int mVolumeMax;
         private int mVolume;
@@ -906,6 +901,14 @@
          * <p>By default the transfer initiation user handle and package name are set to {@code
          * null}.
          */
+        // The UserHandleName warning suggests the name should be "doFooAsUser". But the UserHandle
+        // parameter of this function is stored in a field, and not used to execute an operation on
+        // a specific user.
+        // The MissingGetterMatchingBuilder requires a getTransferInitiator function. But said
+        // getter is not included because the returned package name and user handle is always either
+        // null or the values that correspond to the calling app, and that information is obtainable
+        // via RoutingController#wasTransferInitiatedBySelf.
+        @SuppressWarnings({"UserHandleName", "MissingGetterMatchingBuilder"})
         @NonNull
         @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
         public Builder setTransferInitiator(
diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroup.java b/media/java/android/media/audiopolicy/AudioVolumeGroup.java
index d607126..0f5fbcc 100644
--- a/media/java/android/media/audiopolicy/AudioVolumeGroup.java
+++ b/media/java/android/media/audiopolicy/AudioVolumeGroup.java
@@ -31,6 +31,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * A class to create the association between different playback attributes
@@ -118,6 +119,12 @@
                 && Arrays.equals(mAudioAttributes, thatAvg.mAudioAttributes);
     }
 
+    @Override
+    public int hashCode() {
+        return Objects.hash(mName, mId,
+                Arrays.hashCode(mAudioAttributes), Arrays.hashCode(mLegacyStreamTypes));
+    }
+
     /**
      * @return List of {@link AudioAttributes} involved in this {@link AudioVolumeGroup}.
      */
diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl
index 2fb0af5..7a1cf92 100644
--- a/media/java/android/media/projection/IMediaProjection.aidl
+++ b/media/java/android/media/projection/IMediaProjection.aidl
@@ -39,8 +39,8 @@
     void unregisterCallback(IMediaProjectionCallback callback);
 
     /**
-     * Returns the {@link LaunchCookie} identifying the task to record, or {@code null} if
-     * there is none.
+     * Returns the {@link LaunchCookie} identifying the task to record. Will always be set
+     * regardless of starting a new task or recent task
      */
     @EnforcePermission("MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
@@ -48,8 +48,16 @@
     LaunchCookie getLaunchCookie();
 
     /**
-     * Updates the {@link LaunchCookie} identifying the task to record, or {@code null} if
-     * there is none.
+     * Returns the taskId identifying the task to record. Will only be set in the case of
+     * launching a recent task, otherwise set to -1.
+     */
+    @EnforcePermission("MANAGE_MEDIA_PROJECTION")
+    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+            + ".permission.MANAGE_MEDIA_PROJECTION)")
+    int getTaskId();
+
+    /**
+     * Updates the {@link LaunchCookie} identifying the task to record.
      */
     @EnforcePermission("MANAGE_MEDIA_PROJECTION")
     @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
@@ -57,6 +65,15 @@
     void setLaunchCookie(in LaunchCookie launchCookie);
 
     /**
+     * Updates the taskId identifying the task to record.
+     */
+    @EnforcePermission("MANAGE_MEDIA_PROJECTION")
+    @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+            + ".permission.MANAGE_MEDIA_PROJECTION)")
+    void setTaskId(in int taskId);
+
+
+    /**
      * Returns {@code true} if this token is still valid. A token is valid as long as the token
      * hasn't timed out before it was used, and the token is only used once.
      *
diff --git a/media/java/android/media/tv/TvStreamConfig.java b/media/java/android/media/tv/TvStreamConfig.java
index 7ea93b4..1f51c7a 100644
--- a/media/java/android/media/tv/TvStreamConfig.java
+++ b/media/java/android/media/tv/TvStreamConfig.java
@@ -23,6 +23,8 @@
 import android.os.Parcelable;
 import android.util.Log;
 
+import java.util.Objects;
+
 /**
  * @hide
  */
@@ -177,4 +179,9 @@
             && config.mMaxWidth == mMaxWidth
             && config.mMaxHeight == mMaxHeight;
     }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mGeneration, mStreamId, mType, mMaxWidth, mMaxHeight);
+    }
 }
diff --git a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
index 0df36af..6860c0b 100644
--- a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
+++ b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java
@@ -28,6 +28,7 @@
  * outside the test it is implemented by the system server.
  */
 public final class FakeIMediaProjection extends IMediaProjection.Stub {
+    int mTaskId = -1;
     boolean mIsStarted = false;
     LaunchCookie mLaunchCookie = null;
     IMediaProjectionCallback mIMediaProjectionCallback = null;
@@ -87,6 +88,13 @@
 
     @Override
     @EnforcePermission(MANAGE_MEDIA_PROJECTION)
+    public int getTaskId() throws RemoteException {
+        getTaskId_enforcePermission();
+        return mTaskId;
+    }
+
+    @Override
+    @EnforcePermission(MANAGE_MEDIA_PROJECTION)
     public void setLaunchCookie(LaunchCookie launchCookie) throws RemoteException {
         setLaunchCookie_enforcePermission();
         mLaunchCookie = launchCookie;
@@ -94,6 +102,13 @@
 
     @Override
     @EnforcePermission(MANAGE_MEDIA_PROJECTION)
+    public void setTaskId(int taskId) throws RemoteException {
+        setTaskId_enforcePermission();
+        mTaskId = taskId;
+    }
+
+    @Override
+    @EnforcePermission(MANAGE_MEDIA_PROJECTION)
     public boolean isValid() throws RemoteException {
         isValid_enforcePermission();
         return true;
diff --git a/packages/InputDevices/res/raw/keyboard_layout_serbian_latin.kcm b/packages/InputDevices/res/raw/keyboard_layout_serbian_latin.kcm
new file mode 100644
index 0000000..0ff1dea
--- /dev/null
+++ b/packages/InputDevices/res/raw/keyboard_layout_serbian_latin.kcm
@@ -0,0 +1,350 @@
+# Copyright 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+# Serbian (Latin) keyboard layout.
+#
+
+type OVERLAY
+
+map key 12 SLASH
+map key 21 Z
+map key 44 Y
+map key 53 MINUS
+map key 86 PLUS
+
+### ROW 1
+
+key GRAVE {
+    label:                              '\u0327'
+    base:                               '\u0327'
+    shift:                              '~'
+}
+
+key 1 {
+    label:                              '1'
+    base:                               '1'
+    shift:                              '!'
+    ralt:                               '~'
+}
+
+key 2 {
+    label:                              '2'
+    base:                               '2'
+    shift:                              '\u0022'
+    ralt:                               '\u030C'
+}
+
+key 3 {
+    label:                              '3'
+    base:                               '3'
+    shift:                              '#'
+    ralt:                               '\u0302'
+}
+
+key 4 {
+    label:                              '4'
+    base:                               '4'
+    shift:                              '$'
+    ralt:                               '\u0306'
+}
+
+key 5 {
+    label:                              '5'
+    base:                               '5'
+    shift:                              '%'
+    ralt:                               '\u030A'
+}
+
+key 6 {
+    label:                              '6'
+    base:                               '6'
+    shift:                              '&'
+    ralt:                               '\u0328'
+}
+
+key 7 {
+    label:                              '7'
+    base:                               '7'
+    shift:                              '/'
+    ralt:                               '`'
+}
+
+key 8 {
+    label:                              '8'
+    base:                               '8'
+    shift:                              '('
+    ralt:                               '\u0307'
+}
+
+key 9 {
+    label:                              '9'
+    base:                               '9'
+    shift:                              ')'
+    ralt:                               '\u0301'
+}
+
+key 0 {
+    label:                              '0'
+    base:                               '0'
+    shift:                              '='
+    ralt:                               '\u030B'
+}
+
+key SLASH {
+    label:                              '\''
+    base:                               '\''
+    shift:                              '?'
+    ralt:                               '\u0308'
+}
+
+key EQUALS {
+    label:                              '+'
+    base:                               '+'
+    shift:                              '*'
+    ralt:                               '\u0327'
+}
+
+### ROW 2
+
+key Q {
+    label:                              'Q'
+    base, capslock+shift:               'q'
+    shift, capslock:                    'Q'
+    ralt:                               '\\'
+}
+
+key W {
+    label:                              'W'
+    base, capslock+shift:               'w'
+    shift, capslock:                    'W'
+    ralt:                               '|'
+}
+
+key E {
+    label:                              'E'
+    base, capslock+shift:               'e'
+    shift, capslock:                    'E'
+    ralt:                               '\u20ac'
+}
+
+key R {
+    label:                              'R'
+    base, capslock+shift:               'r'
+    shift, capslock:                    'R'
+}
+
+key T {
+    label:                              'T'
+    base, capslock+shift:               't'
+    shift, capslock:                    'T'
+}
+
+key Z {
+    label:                              'Z'
+    base, capslock+shift:               'z'
+    shift, capslock:                    'Z'
+}
+
+key U {
+    label:                              'U'
+    base, capslock+shift:               'u'
+    shift, capslock:                    'U'
+}
+
+key I {
+    label:                              'I'
+    base, capslock+shift:               'i'
+    shift, capslock:                    'I'
+}
+
+key O {
+    label:                              'O'
+    base, capslock+shift:               'o'
+    shift, capslock:                    'O'
+}
+
+key P {
+    label:                              'P'
+    base, capslock+shift:               'p'
+    shift, capslock:                    'P'
+}
+
+key LEFT_BRACKET {
+    label:                              '\u0160'
+    base, capslock+shift:               '\u0161'
+    shift, capslock:                    '\u0160'
+    ralt:                               '\u00f7'
+}
+
+key RIGHT_BRACKET {
+    label:                              '\u0110'
+    base, capslock+shift:               '\u0111'
+    shift, capslock:                    '\u0110'
+    ralt:                               '\u00d7'
+}
+
+### ROW 3
+
+key A {
+    label:                              'A'
+    base, capslock+shift:               'a'
+    shift, capslock:                    'A'
+}
+
+key S {
+    label:                              'S'
+    base, capslock+shift:               's'
+    shift, capslock:                    'S'
+}
+
+key D {
+    label:                              'D'
+    base, capslock+shift:               'd'
+    shift, capslock:                    'D'
+}
+
+key F {
+    label:                              'F'
+    base, capslock+shift:               'f'
+    shift, capslock:                    'F'
+    ralt:                               '['
+}
+
+key G {
+    label:                              'G'
+    base, capslock+shift:               'g'
+    shift, capslock:                    'G'
+    ralt:                               ']'
+}
+
+key H {
+    label:                              'H'
+    base, capslock+shift:               'h'
+    shift, capslock:                    'H'
+}
+
+key J {
+    label:                              'J'
+    base, capslock+shift:               'j'
+    shift, capslock:                    'J'
+}
+
+key K {
+    label:                              'K'
+    base, capslock+shift:               'k'
+    shift, capslock:                    'K'
+    ralt:                               '\u0142'
+}
+
+key L {
+    label:                              'L'
+    base, capslock+shift:               'l'
+    shift, capslock:                    'L'
+    ralt:                               '\u0141'
+}
+
+key SEMICOLON {
+    label:                              '\u010c'
+    base, capslock+shift:               '\u010d'
+    shift, capslock:                    '\u010c'
+}
+
+key APOSTROPHE {
+    label:                              '\u0106'
+    base, capslock+shift:               '\u0107'
+    shift, capslock:                    '\u0106'
+    ralt:                               '\u00df'
+}
+
+key BACKSLASH {
+    label:                              '\u017d'
+    base, capslock+shift:               '\u017e'
+    shift, capslock:                    '\u017d'
+    ralt:                               '\u00a4'
+}
+
+### ROW 4
+
+key PLUS {
+    label:                              '<'
+    base:                               '<'
+    shift:                              '>'
+}
+
+key Y {
+    label:                              'Y'
+    base, capslock+shift:               'y'
+    shift, capslock:                    'Y'
+}
+
+key X {
+    label:                              'X'
+    base, capslock+shift:               'x'
+    shift, capslock:                    'X'
+}
+
+key C {
+    label:                              'C'
+    base, capslock+shift:               'c'
+    shift, capslock:                    'C'
+}
+
+key V {
+    label:                              'V'
+    base, capslock+shift:               'v'
+    shift, capslock:                    'V'
+    ralt:                               '@'
+}
+
+key B {
+    label:                              'B'
+    base, capslock+shift:               'b'
+    shift, capslock:                    'B'
+    ralt:                               '{'
+}
+
+key N {
+    label:                              'N'
+    base, capslock+shift:               'n'
+    shift, capslock:                    'N'
+    ralt:                               '}'
+}
+
+key M {
+    label:                              'M'
+    base, capslock+shift:               'm'
+    shift, capslock:                    'M'
+    ralt:                               '\u00a7'
+}
+
+key COMMA {
+    label:                              ','
+    base:                               ','
+    shift:                              ';'
+    ralt:                               '<'
+}
+
+key PERIOD {
+    label:                              '.'
+    base:                               '.'
+    shift:                              ':'
+    ralt:                               '>'
+}
+
+key MINUS {
+    label:                              '-'
+    base:                               '-'
+    shift:                              '_'
+}
\ No newline at end of file
diff --git a/packages/InputDevices/res/values/strings.xml b/packages/InputDevices/res/values/strings.xml
index e10bd7f..be7d2c1 100644
--- a/packages/InputDevices/res/values/strings.xml
+++ b/packages/InputDevices/res/values/strings.xml
@@ -152,4 +152,10 @@
 
     <!-- Thai (Pattachote variant) keyboard layout label. [CHAR LIMIT=35] -->
     <string name="keyboard_layout_thai_pattachote">Thai (Pattachote)</string>
+
+    <!-- Serbian (Latin) keyboard layout label. [CHAR LIMIT=35] -->
+    <string name="keyboard_layout_serbian_latin">Serbian (Latin)</string>
+
+    <!-- Montenegrin (Latin) keyboard layout label. [CHAR LIMIT=35] -->
+    <string name="keyboard_layout_montenegrin_latin">Montenegrin (Latin)</string>
 </resources>
diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml
index d4f8f7d..84e4b9e 100644
--- a/packages/InputDevices/res/xml/keyboard_layouts.xml
+++ b/packages/InputDevices/res/xml/keyboard_layouts.xml
@@ -332,4 +332,18 @@
         android:keyboardLayout="@raw/keyboard_layout_thai_pattachote"
         android:keyboardLocale="th-Thai"
         android:keyboardLayoutType="extended" />
+
+    <keyboard-layout
+        android:name="keyboard_layout_serbian_latin"
+        android:label="@string/keyboard_layout_serbian_latin"
+        android:keyboardLayout="@raw/keyboard_layout_serbian_latin"
+        android:keyboardLocale="sr-Latn-RS"
+        android:keyboardLayoutType="qwertz" />
+
+    <keyboard-layout
+        android:name="keyboard_layout_montenegrin_latin"
+        android:label="@string/keyboard_layout_montenegrin_latin"
+        android:keyboardLayout="@raw/keyboard_layout_serbian_latin"
+        android:keyboardLocale="cnr-Latn-ME"
+        android:keyboardLayoutType="qwertz" />
 </keyboard-layouts>
diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
index 1040ac6..c5e86b4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerAllowlistBackend.java
@@ -210,7 +210,7 @@
                     mAppContext.getSystemService(ActivityManager.class).noteAppRestrictionEnabled(
                             pkg, uid, ActivityManager.RESTRICTION_LEVEL_EXEMPTED,
                             true, ActivityManager.RESTRICTION_REASON_USER,
-                            "settings", 0);
+                            "settings", ActivityManager.RESTRICTION_SOURCE_USER, 0);
                 }
             }
 
@@ -251,7 +251,7 @@
                     mAppContext.getSystemService(ActivityManager.class).noteAppRestrictionEnabled(
                             pkg, uid, ActivityManager.RESTRICTION_LEVEL_EXEMPTED,
                             false, ActivityManager.RESTRICTION_REASON_USER,
-                            "settings", 0);
+                            "settings", ActivityManager.RESTRICTION_SOURCE_USER, 0L);
                 }
             }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepository.kt b/packages/SettingsLib/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepository.kt
index 5bcb82d..0b71d25 100644
--- a/packages/SettingsLib/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepository.kt
@@ -27,6 +27,7 @@
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
@@ -62,16 +63,18 @@
         captioningChanges
             .filterIsInstance(CaptioningChange.IsSystemAudioCaptioningEnabled::class)
             .map { it.isEnabled }
+            .onStart { emit(captioningManager.isSystemAudioCaptioningEnabled) }
             .stateIn(
                 coroutineScope,
                 SharingStarted.WhileSubscribed(),
-                captioningManager.isSystemAudioCaptioningEnabled
+                captioningManager.isSystemAudioCaptioningEnabled,
             )
 
     override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> =
         captioningChanges
             .filterIsInstance(CaptioningChange.IsSystemUICaptioningEnabled::class)
             .map { it.isEnabled }
+            .onStart { emit(captioningManager.isSystemAudioCaptioningUiEnabled) }
             .stateIn(
                 coroutineScope,
                 SharingStarted.WhileSubscribed(),
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
index 3b84333..8204569 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
@@ -16,9 +16,13 @@
 
 package com.android.settingslib.volume.data.repository
 
+import android.content.ContentResolver
+import android.database.ContentObserver
 import android.media.AudioDeviceInfo
 import android.media.AudioManager
 import android.media.AudioManager.OnCommunicationDeviceChangedListener
+import android.provider.Settings
+import androidx.concurrent.futures.DirectExecutor
 import com.android.internal.util.ConcurrentUtils
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
 import com.android.settingslib.volume.shared.model.AudioManagerEvent
@@ -33,12 +37,13 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
@@ -84,17 +89,31 @@
 class AudioRepositoryImpl(
     private val audioManagerEventsReceiver: AudioManagerEventsReceiver,
     private val audioManager: AudioManager,
+    private val contentResolver: ContentResolver,
     private val backgroundCoroutineContext: CoroutineContext,
     private val coroutineScope: CoroutineScope,
 ) : AudioRepository {
 
+    private val streamSettingNames: Map<AudioStream, String> =
+        mapOf(
+            AudioStream(AudioManager.STREAM_VOICE_CALL) to Settings.System.VOLUME_VOICE,
+            AudioStream(AudioManager.STREAM_SYSTEM) to Settings.System.VOLUME_SYSTEM,
+            AudioStream(AudioManager.STREAM_RING) to Settings.System.VOLUME_RING,
+            AudioStream(AudioManager.STREAM_MUSIC) to Settings.System.VOLUME_MUSIC,
+            AudioStream(AudioManager.STREAM_ALARM) to Settings.System.VOLUME_ALARM,
+            AudioStream(AudioManager.STREAM_NOTIFICATION) to Settings.System.VOLUME_NOTIFICATION,
+            AudioStream(AudioManager.STREAM_BLUETOOTH_SCO) to Settings.System.VOLUME_BLUETOOTH_SCO,
+            AudioStream(AudioManager.STREAM_ACCESSIBILITY) to Settings.System.VOLUME_ACCESSIBILITY,
+            AudioStream(AudioManager.STREAM_ASSISTANT) to Settings.System.VOLUME_ASSISTANT,
+        )
+
     override val mode: StateFlow<Int> =
         callbackFlow {
-                val listener =
-                    AudioManager.OnModeChangedListener { newMode -> launch { send(newMode) } }
+                val listener = AudioManager.OnModeChangedListener { newMode -> trySend(newMode) }
                 audioManager.addOnModeChangedListener(ConcurrentUtils.DIRECT_EXECUTOR, listener)
                 awaitClose { audioManager.removeOnModeChangedListener(listener) }
             }
+            .onStart { emit(audioManager.mode) }
             .flowOn(backgroundCoroutineContext)
             .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode)
 
@@ -102,6 +121,7 @@
         audioManagerEventsReceiver.events
             .filterIsInstance(AudioManagerEvent.InternalRingerModeChanged::class)
             .map { RingerMode(audioManager.ringerModeInternal) }
+            .onStart { emit(RingerMode(audioManager.ringerModeInternal)) }
             .flowOn(backgroundCoroutineContext)
             .stateIn(
                 coroutineScope,
@@ -122,6 +142,7 @@
                 }
                 .filterNotNull()
                 .map { audioManager.communicationDevice }
+                .onStart { emit(audioManager.communicationDevice) }
                 .flowOn(backgroundCoroutineContext)
                 .stateIn(
                     coroutineScope,
@@ -130,17 +151,18 @@
                 )
 
     override fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> {
-        return audioManagerEventsReceiver.events
-            .filter {
-                if (it is StreamAudioManagerEvent) {
-                    it.audioStream == audioStream
-                } else {
-                    true
-                }
-            }
+        return merge(
+                audioManagerEventsReceiver.events.filter {
+                    if (it is StreamAudioManagerEvent) {
+                        it.audioStream == audioStream
+                    } else {
+                        true
+                    }
+                },
+                volumeSettingChanges(audioStream),
+            )
             .map { getCurrentAudioStream(audioStream) }
             .onStart { emit(getCurrentAudioStream(audioStream)) }
-            .conflate()
             .flowOn(backgroundCoroutineContext)
     }
 
@@ -195,4 +217,19 @@
             // return STREAM_VOICE_CALL in getAudioStream
             audioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL)
         }
+
+    private fun volumeSettingChanges(audioStream: AudioStream): Flow<Unit> {
+        val uri = streamSettingNames[audioStream]?.let(Settings.System::getUriFor)
+        uri ?: return emptyFlow()
+        return callbackFlow {
+            val observer =
+                object : ContentObserver(DirectExecutor.INSTANCE, 0) {
+                    override fun onChange(selfChange: Boolean) {
+                        launch { send(Unit) }
+                    }
+                }
+            contentResolver.registerContentObserver(uri, false, observer)
+            awaitClose { contentResolver.unregisterContentObserver(observer) }
+        }
+    }
 }
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
index 8700680..683759d 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.settingslib.volume.data.repository
 
+import android.content.ContentResolver
+import android.database.ContentObserver
 import android.media.AudioDeviceInfo
 import android.media.AudioManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -38,6 +40,7 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Captor
 import org.mockito.Mock
@@ -55,9 +58,11 @@
     @Captor
     private lateinit var communicationDeviceListenerCaptor:
         ArgumentCaptor<AudioManager.OnCommunicationDeviceChangedListener>
+    @Captor private lateinit var contentObserver: ArgumentCaptor<ContentObserver>
 
     @Mock private lateinit var audioManager: AudioManager
     @Mock private lateinit var communicationDevice: AudioDeviceInfo
+    @Mock private lateinit var contentResolver: ContentResolver
 
     private val eventsReceiver = FakeAudioManagerEventsReceiver()
     private val volumeByStream: MutableMap<Int, Int> = mutableMapOf()
@@ -80,6 +85,7 @@
             val streamType = it.arguments[0] as Int
             volumeByStream[it.arguments[0] as Int] = it.arguments[1] as Int
             triggerEvent(AudioManagerEvent.StreamVolumeChanged(AudioStream(streamType)))
+            triggerSettingChange()
         }
         `when`(audioManager.adjustStreamVolume(anyInt(), anyInt(), anyInt())).then {
             val streamType = it.arguments[0] as Int
@@ -100,6 +106,7 @@
             AudioRepositoryImpl(
                 eventsReceiver,
                 audioManager,
+                contentResolver,
                 testScope.testScheduler,
                 testScope.backgroundScope,
             )
@@ -254,6 +261,12 @@
         modeListenerCaptor.value.onModeChanged(mode)
     }
 
+    private fun triggerSettingChange(selfChange: Boolean = false) {
+        verify(contentResolver)
+            .registerContentObserver(any(), anyBoolean(), contentObserver.capture())
+        contentObserver.value.onChange(selfChange)
+    }
+
     private fun triggerEvent(event: AudioManagerEvent) {
         testScope.launch { eventsReceiver.triggerEvent(event) }
     }
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 374240b..04d30ed 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -610,6 +610,8 @@
 
     <!-- Permission required for CTS test - CtsThreadNetworkTestCases -->
     <uses-permission android:name="android.permission.THREAD_NETWORK_PRIVILEGED"/>
+    <!-- Permission required to access Thread network shell commands for testing -->
+    <uses-permission android:name="android.permission.THREAD_NETWORK_TESTING"/>
 
     <!-- Permission required for CTS tests to enable/disable rate limiting toasts. -->
     <uses-permission android:name="android.permission.MANAGE_TOAST_RATE_LIMITING" />
diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
index ee81813..0dd9275 100644
--- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
@@ -36,6 +36,7 @@
 import android.os.UserManager;
 import android.provider.MediaStore;
 import android.provider.Settings;
+import android.text.Html;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.LayoutInflater;
@@ -253,6 +254,9 @@
             } else {
                 p.mTitle = getString(com.android.internal.R.string.ringtone_picker_title);
             }
+        } else {
+            // Make sure intents don't inject HTML elements.
+            p.mTitle = Html.escapeHtml(p.mTitle.toString());
         }
 
         setupAlert();
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index ac680a9..e940674 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -752,6 +752,7 @@
     plugins: [
         "dagger2-compiler",
     ],
+    strict_mode: false,
 }
 
 // in-place tests which use Robolectric in the tests directory
@@ -771,7 +772,6 @@
     ],
     static_libs: [
         "RoboTestLibraries",
-        "mockito-kotlin2",
     ],
     libs: [
         "android.test.runner",
@@ -787,6 +787,7 @@
     plugins: [
         "dagger2-compiler",
     ],
+    strict_mode: false,
 }
 
 android_ravenwood_test {
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 163c849..36bad5e 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -960,10 +960,13 @@
 }
 
 flag {
-  name: "media_controls_user_initiated_dismiss"
+  name: "media_controls_user_initiated_deleteintent"
   namespace: "systemui"
   description: "Only dismiss media notifications when the control was removed by the user."
   bug: "335875159"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
 }
 
 flag {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index f5d01d7..907c39d 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -944,9 +944,26 @@
                 }
 
                 override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
-                    startController.onTransitionAnimationEnd(isExpandingFullyAbove)
-                    endController.onTransitionAnimationEnd(isExpandingFullyAbove)
-                    onLaunchAnimationEnd()
+                    // onLaunchAnimationEnd is called by an Animator at the end of the animation,
+                    // on a Choreographer animation tick. The following calls will move the animated
+                    // content from the dialog overlay back to its original position, and this
+                    // change must be reflected in the next frame given that we then sync the next
+                    // frame of both the content and dialog ViewRoots. However, in case that content
+                    // is rendered by Compose, whose compositions are also scheduled on a
+                    // Choreographer frame, any state change made *right now* won't be reflected in
+                    // the next frame given that a Choreographer frame can't schedule another and
+                    // have it happen in the same frame. So we post the forwarded calls to
+                    // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring
+                    // that the move of the content back to its original window will be reflected in
+                    // the next frame right after [onLaunchAnimationEnd] is called.
+                    //
+                    // TODO(b/330672236): Move this to TransitionAnimator.
+                    dialog.context.mainExecutor.execute {
+                        startController.onTransitionAnimationEnd(isExpandingFullyAbove)
+                        endController.onTransitionAnimationEnd(isExpandingFullyAbove)
+
+                        onLaunchAnimationEnd()
+                    }
                 }
 
                 override fun onTransitionAnimationProgress(
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index cc55df1..8e824e6 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -357,26 +357,13 @@
                         Log.d(TAG, "Animation ended")
                     }
 
-                    // onAnimationEnd is called at the end of the animation, on a Choreographer
-                    // animation tick. During dialog launches, the following calls will move the
-                    // animated content from the dialog overlay back to its original position, and
-                    // this change must be reflected in the next frame given that we then sync the
-                    // next frame of both the content and dialog ViewRoots. During SysUI activity
-                    // launches, we will instantly collapse the shade at the end of the transition.
-                    // However, if those are rendered by Compose, whose compositions are also
-                    // scheduled on a Choreographer frame, any state change made *right now* won't
-                    // be reflected in the next frame given that a Choreographer frame can't
-                    // schedule another and have it happen in the same frame. So we post the
-                    // forwarded calls to [Controller.onLaunchAnimationEnd] in the main executor,
-                    // leaving this Choreographer frame, ensuring that any state change applied by
-                    // onTransitionAnimationEnd() will be reflected in the same frame.
-                    mainExecutor.execute {
-                        controller.onTransitionAnimationEnd(isExpandingFullyAbove)
-                        transitionContainerOverlay.remove(windowBackgroundLayer)
+                    // TODO(b/330672236): Post this to the main thread instead so that it does not
+                    // flicker with Flexiglass enabled.
+                    controller.onTransitionAnimationEnd(isExpandingFullyAbove)
+                    transitionContainerOverlay.remove(windowBackgroundLayer)
 
-                        if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
-                            openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
-                        }
+                    if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
+                        openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
                     }
                 }
             }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
index b057296..536f297 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
@@ -73,3 +73,11 @@
         maxMarginYdp = 8f,
         minScale = 0.9f,
     )
+
+/**
+ * SysUI transitions - Bottomsheet (AT3)
+ * https://carbon.googleplex.com/predictive-back-for-apps/pages/at-3-bottom-sheets
+ */
+fun BackAnimationSpec.Companion.bottomSheetForSysUi(
+    displayMetricsProvider: () -> DisplayMetrics,
+): BackAnimationSpec = BackAnimationSpec.createBottomsheetAnimationSpec(displayMetricsProvider)
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
index 49d1fb4..029f62c 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackTransformation.kt
@@ -26,12 +26,36 @@
     var translateX: Float = Float.NaN,
     var translateY: Float = Float.NaN,
     var scale: Float = Float.NaN,
+    var scalePivotPosition: ScalePivotPosition? = null,
 )
 
+/** Enum that describes the location of the scale pivot position */
+enum class ScalePivotPosition {
+    // more options may be added in the future
+    CENTER,
+    BOTTOM_CENTER;
+
+    fun applyTo(view: View) {
+        val pivotX =
+            when (this) {
+                CENTER -> view.width / 2f
+                BOTTOM_CENTER -> view.width / 2f
+            }
+        val pivotY =
+            when (this) {
+                CENTER -> view.height / 2f
+                BOTTOM_CENTER -> view.height.toFloat()
+            }
+        view.pivotX = pivotX
+        view.pivotY = pivotY
+    }
+}
+
 /** Apply the transformation to the [targetView] */
 fun BackTransformation.applyTo(targetView: View) {
     if (translateX.isFinite()) targetView.translationX = translateX
     if (translateY.isFinite()) targetView.translationY = translateY
+    scalePivotPosition?.applyTo(targetView)
     if (scale.isFinite()) {
         targetView.scaleX = scale
         targetView.scaleY = scale
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BottomsheetBackAnimationSpec.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BottomsheetBackAnimationSpec.kt
new file mode 100644
index 0000000..b1945a1
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BottomsheetBackAnimationSpec.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.animation.back
+
+import android.util.DisplayMetrics
+import android.view.animation.Interpolator
+import com.android.app.animation.Interpolators
+import com.android.systemui.util.dpToPx
+
+private const val MAX_SCALE_DELTA_DP = 48
+
+/** Create a [BackAnimationSpec] from [displayMetrics] and design specs. */
+fun BackAnimationSpec.Companion.createBottomsheetAnimationSpec(
+    displayMetricsProvider: () -> DisplayMetrics,
+    scaleEasing: Interpolator = Interpolators.BACK_GESTURE,
+): BackAnimationSpec {
+    return BackAnimationSpec { backEvent, _, result ->
+        val displayMetrics = displayMetricsProvider()
+        val screenWidthPx = displayMetrics.widthPixels
+        val minScale = 1 - MAX_SCALE_DELTA_DP.dpToPx(displayMetrics) / screenWidthPx
+        val progressX = backEvent.progress
+        val ratioScale = scaleEasing.getInterpolation(progressX)
+        result.apply {
+            scale = 1f - ratioScale * (1f - minScale)
+            scalePivotPosition = ScalePivotPosition.BOTTOM_CENTER
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
index 99f7d0f..887e349 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -28,7 +28,6 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
-import javax.inject.Inject
 
 /**
  * Renders the content of the lockscreen.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index cf2e895..e6132c6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -169,6 +169,7 @@
     viewModel: NotificationsPlaceholderViewModel,
     maxScrimTop: () -> Float,
     shouldPunchHoleBehindScrim: Boolean,
+    shouldFillMaxSize: Boolean = true,
     shadeMode: ShadeMode,
     modifier: Modifier = Modifier,
 ) {
@@ -327,14 +328,14 @@
         }
         Box(
             modifier =
-                Modifier.fillMaxSize()
-                    .graphicsLayer {
+                Modifier.graphicsLayer {
                         alpha =
                             if (shouldPunchHoleBehindScrim) {
                                 (expansionFraction / EXPANSION_FOR_MAX_SCRIM_ALPHA).coerceAtMost(1f)
                             } else 1f
                     }
                     .background(MaterialTheme.colorScheme.surface)
+                    .thenIf(shouldFillMaxSize) { Modifier.fillMaxSize() }
                     .debugBackground(viewModel, DEBUG_BOX_COLOR)
         ) {
             NotificationPlaceholder(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
index e17a146..ae53d56 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
@@ -17,21 +17,31 @@
 package com.android.systemui.notifications.ui.composable
 
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Text
+import androidx.compose.foundation.layout.width
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.animation.scene.UserAction
 import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.ui.composable.LockscreenContent
 import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel
+import com.android.systemui.scene.session.ui.composable.SaveableSession
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
 import com.android.systemui.shade.ui.composable.OverlayShade
 import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
+import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
+import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import com.android.systemui.statusbar.phone.ui.StatusBarIconController
+import com.android.systemui.statusbar.phone.ui.TintedIconManager
 import dagger.Lazy
 import java.util.Optional
 import javax.inject.Inject
@@ -41,36 +51,53 @@
 class NotificationsShadeScene
 @Inject
 constructor(
-    viewModel: NotificationsShadeSceneViewModel,
+    sceneViewModel: NotificationsShadeSceneViewModel,
     private val overlayShadeViewModel: OverlayShadeViewModel,
+    private val shadeHeaderViewModel: ShadeHeaderViewModel,
+    private val notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
+    private val tintedIconManagerFactory: TintedIconManager.Factory,
+    private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
+    private val statusBarIconController: StatusBarIconController,
+    private val shadeSession: SaveableSession,
+    private val stackScrollView: Lazy<NotificationScrollView>,
     private val lockscreenContent: Lazy<Optional<LockscreenContent>>,
 ) : ComposableScene {
 
     override val key = Scenes.NotificationsShade
 
     override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> =
-        viewModel.destinationScenes
+        sceneViewModel.destinationScenes
 
     @Composable
     override fun SceneScope.Content(
         modifier: Modifier,
     ) {
         OverlayShade(
-            viewModel = overlayShadeViewModel,
             modifier = modifier,
+            viewModel = overlayShadeViewModel,
             horizontalArrangement = Arrangement.Start,
             lockscreenContent = lockscreenContent,
         ) {
-            Text(
-                text = "Notifications list",
-                modifier = Modifier.padding(NotificationsShade.Dimensions.Padding)
-            )
-        }
-    }
-}
+            Column {
+                ExpandedShadeHeader(
+                    viewModel = shadeHeaderViewModel,
+                    createTintedIconManager = tintedIconManagerFactory::create,
+                    createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
+                    statusBarIconController = statusBarIconController,
+                    modifier = Modifier.padding(horizontal = 16.dp),
+                )
 
-object NotificationsShade {
-    object Dimensions {
-        val Padding = 16.dp
+                NotificationScrollingStack(
+                    shadeSession = shadeSession,
+                    stackScrollView = stackScrollView.get(),
+                    viewModel = notificationsPlaceholderViewModel,
+                    maxScrimTop = { 0f },
+                    shouldPunchHoleBehindScrim = false,
+                    shouldFillMaxSize = false,
+                    shadeMode = ShadeMode.Dual,
+                    modifier = Modifier.width(416.dp),
+                )
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index 54a98dd..8195df3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.qs.ui.composable
 
+import android.view.ViewGroup
+import android.widget.FrameLayout
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -24,6 +26,7 @@
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.viewinterop.AndroidView
@@ -164,11 +167,8 @@
     state: () -> QSSceneAdapter.State,
     modifier: Modifier = Modifier,
 ) {
-    val qsView by qsSceneAdapter.qsView.collectAsStateWithLifecycle(null)
-    val isCustomizing by
-        qsSceneAdapter.isCustomizerShowing.collectAsStateWithLifecycle(
-            qsSceneAdapter.isCustomizerShowing.value
-        )
+    val qsView by qsSceneAdapter.qsView.collectAsStateWithLifecycle()
+    val isCustomizing by qsSceneAdapter.isCustomizerShowing.collectAsStateWithLifecycle()
     QuickSettingsTheme {
         val context = LocalContext.current
 
@@ -180,15 +180,34 @@
         qsView?.let { view ->
             Box(
                 modifier =
-                    modifier.fillMaxWidth().thenIf(isCustomizing) { Modifier.fillMaxHeight() }
+                    modifier
+                        .fillMaxWidth()
+                        .thenIf(isCustomizing) { Modifier.fillMaxHeight() }
+                        .drawWithContent {
+                            qsSceneAdapter.applyLatestExpansionAndSquishiness()
+                            drawContent()
+                        }
             ) {
                 AndroidView(
                     modifier = Modifier.fillMaxWidth(),
-                    factory = { _ ->
+                    factory = { context ->
                         qsSceneAdapter.setState(state())
-                        view
+                        FrameLayout(context).apply {
+                            (view.parent as? ViewGroup)?.removeView(view)
+                            addView(view)
+                        }
                     },
-                    update = { qsSceneAdapter.setState(state()) }
+                    // When the view changes (e.g. due to a theme change), this will be recomposed
+                    // if needed and the new view will be attached to the FrameLayout here.
+                    update = {
+                        qsSceneAdapter.setState(state())
+                        if (view.parent != it) {
+                            it.removeAllViews()
+                            (view.parent as? ViewGroup)?.removeView(view)
+                            it.addView(view)
+                        }
+                    },
+                    onRelease = { it.removeAllViews() }
                 )
             }
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
index 04f76f5..4d946bf 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
@@ -33,9 +33,9 @@
 import com.android.systemui.shade.ui.composable.OverlayShade
 import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
 import dagger.Lazy
+import java.util.Optional
 import javax.inject.Inject
 import kotlinx.coroutines.flow.StateFlow
-import java.util.Optional
 
 @SysUISingleton
 class QuickSettingsShadeScene
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index 92b2b4e..18e65508 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -30,9 +30,6 @@
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.motionEventSpy
-import androidx.compose.ui.input.pointer.pointerInput
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
 import com.android.compose.animation.scene.SceneKey
@@ -94,21 +91,7 @@
     Box(
         modifier = Modifier.fillMaxSize(),
     ) {
-        SceneTransitionLayout(
-            state = state,
-            modifier =
-                modifier
-                    .fillMaxSize()
-                    .motionEventSpy { event -> viewModel.onMotionEvent(event) }
-                    .pointerInput(Unit) {
-                        awaitPointerEventScope {
-                            while (true) {
-                                awaitPointerEvent(PointerEventPass.Final)
-                                viewModel.onMotionEventComplete()
-                            }
-                        }
-                    }
-        ) {
+        SceneTransitionLayout(state = state, modifier = modifier.fillMaxSize()) {
             sceneByKey.forEach { (sceneKey, composableScene) ->
                 scene(
                     key = sceneKey,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index cbaa894..f5a0ef2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -8,12 +8,16 @@
 import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
 import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
 import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransition
+import com.android.systemui.scene.ui.composable.transitions.goneToNotificationsShadeTransition
+import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition
 import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.goneToSplitShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToBouncerTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToCommunalTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToGoneTransition
+import com.android.systemui.scene.ui.composable.transitions.lockscreenToNotificationsShadeTransition
+import com.android.systemui.scene.ui.composable.transitions.lockscreenToQuickSettingsShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToQuickSettingsTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToShadeTransition
 import com.android.systemui.scene.ui.composable.transitions.lockscreenToSplitShadeTransition
@@ -37,6 +41,7 @@
     // Scene transitions
 
     from(Scenes.Bouncer, to = Scenes.Gone) { bouncerToGoneTransition() }
+    from(Scenes.Gone, to = Scenes.NotificationsShade) { goneToNotificationsShadeTransition() }
     from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
     from(
         Scenes.Gone,
@@ -53,8 +58,15 @@
         goneToShadeTransition(durationScale = 0.9)
     }
     from(Scenes.Gone, to = Scenes.QuickSettings) { goneToQuickSettingsTransition() }
+    from(Scenes.Gone, to = Scenes.QuickSettingsShade) { goneToQuickSettingsShadeTransition() }
     from(Scenes.Lockscreen, to = Scenes.Bouncer) { lockscreenToBouncerTransition() }
     from(Scenes.Lockscreen, to = Scenes.Communal) { lockscreenToCommunalTransition() }
+    from(Scenes.Lockscreen, to = Scenes.NotificationsShade) {
+        lockscreenToNotificationsShadeTransition()
+    }
+    from(Scenes.Lockscreen, to = Scenes.QuickSettingsShade) {
+        lockscreenToQuickSettingsShadeTransition()
+    }
     from(Scenes.Lockscreen, to = Scenes.Shade) { lockscreenToShadeTransition() }
     from(
         Scenes.Lockscreen,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToNotificationsShadeTransition.kt
new file mode 100644
index 0000000..48ec198
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToNotificationsShadeTransition.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.composable.transitions
+
+import com.android.compose.animation.scene.TransitionBuilder
+
+fun TransitionBuilder.goneToNotificationsShadeTransition(
+    durationScale: Double = 1.0,
+) {
+    toNotificationsShadeTransition(durationScale)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt
new file mode 100644
index 0000000..225ca4e
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.composable.transitions
+
+import com.android.compose.animation.scene.TransitionBuilder
+
+fun TransitionBuilder.goneToQuickSettingsShadeTransition(
+    durationScale: Double = 1.0,
+) {
+    toQuickSettingsShadeTransition(durationScale)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToNotificationsShadeTransition.kt
new file mode 100644
index 0000000..372e4a1a
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToNotificationsShadeTransition.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.composable.transitions
+
+import com.android.compose.animation.scene.TransitionBuilder
+
+fun TransitionBuilder.lockscreenToNotificationsShadeTransition(
+    durationScale: Double = 1.0,
+) {
+    toNotificationsShadeTransition(durationScale)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt
new file mode 100644
index 0000000..ce24f5e
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.composable.transitions
+
+import com.android.compose.animation.scene.TransitionBuilder
+
+fun TransitionBuilder.lockscreenToQuickSettingsShadeTransition(
+    durationScale: Double = 1.0,
+) {
+    toQuickSettingsShadeTransition(durationScale)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt
new file mode 100644
index 0000000..a6b268d
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.UserActionDistance
+import com.android.compose.animation.scene.UserActionDistanceScope
+import com.android.systemui.notifications.ui.composable.Notifications
+import com.android.systemui.shade.ui.composable.OverlayShade
+import com.android.systemui.shade.ui.composable.Shade
+import com.android.systemui.shade.ui.composable.ShadeHeader
+import kotlin.time.Duration.Companion.milliseconds
+
+fun TransitionBuilder.toNotificationsShadeTransition(
+    durationScale: Double = 1.0,
+) {
+    spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
+    swipeSpec =
+        spring(
+            stiffness = Spring.StiffnessMediumLow,
+            visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
+        )
+    distance =
+        object : UserActionDistance {
+            override fun UserActionDistanceScope.absoluteDistance(
+                fromSceneSize: IntSize,
+                orientation: Orientation,
+            ): Float {
+                return fromSceneSize.height.toFloat() * 2 / 3f
+            }
+        }
+
+    translate(OverlayShade.Elements.PanelBackground, Edge.Top)
+    translate(Notifications.Elements.NotificationScrim, Edge.Top)
+
+    fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) }
+
+    fractionRange(start = .5f) {
+        fade(ShadeHeader.Elements.Clock)
+        fade(ShadeHeader.Elements.ExpandedContent)
+        fade(ShadeHeader.Elements.PrivacyChip)
+        fade(Notifications.Elements.NotificationScrim)
+    }
+}
+
+private val DefaultDuration = 300.milliseconds
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
new file mode 100644
index 0000000..2baaecf
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.UserActionDistance
+import com.android.compose.animation.scene.UserActionDistanceScope
+import com.android.systemui.shade.ui.composable.OverlayShade
+import com.android.systemui.shade.ui.composable.Shade
+import kotlin.time.Duration.Companion.milliseconds
+
+fun TransitionBuilder.toQuickSettingsShadeTransition(
+    durationScale: Double = 1.0,
+) {
+    spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
+    swipeSpec =
+        spring(
+            stiffness = Spring.StiffnessMediumLow,
+            visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
+        )
+    distance =
+        object : UserActionDistance {
+            override fun UserActionDistanceScope.absoluteDistance(
+                fromSceneSize: IntSize,
+                orientation: Orientation,
+            ): Float {
+                return fromSceneSize.height.toFloat() * 2 / 3f
+            }
+        }
+
+    translate(OverlayShade.Elements.PanelBackground, Edge.Top)
+
+    fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) }
+}
+
+private val DefaultDuration = 300.milliseconds
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index 2f11085..34cc676 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -23,9 +23,12 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.getValue
@@ -37,6 +40,8 @@
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.LowestZIndexScenePicker
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.thenIf
+import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.keyguard.ui.composable.LockscreenContent
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
@@ -54,6 +59,8 @@
     content: @Composable () -> Unit,
 ) {
     val backgroundScene by viewModel.backgroundScene.collectAsStateWithLifecycle()
+    val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass
+    val isPanelFullWidth = widthSizeClass == WindowWidthSizeClass.Compact
 
     Box(modifier) {
         if (backgroundScene == Scenes.Lockscreen) {
@@ -66,10 +73,13 @@
         Scrim(onClicked = viewModel::onScrimClicked)
 
         Row(
-            modifier = Modifier.fillMaxSize().padding(OverlayShade.Dimensions.ScrimContentPadding),
+            modifier =
+                Modifier.fillMaxSize().thenIf(!isPanelFullWidth) {
+                    Modifier.padding(OverlayShade.Dimensions.ScrimContentPadding)
+                },
             horizontalArrangement = horizontalArrangement,
         ) {
-            Panel(content = content)
+            Panel(modifier = Modifier.panelSize(), content = content)
         }
     }
 }
@@ -111,6 +121,20 @@
     }
 }
 
+@Composable
+private fun Modifier.panelSize(): Modifier {
+    val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass
+
+    return this.then(
+        when (widthSizeClass) {
+            WindowWidthSizeClass.Compact -> Modifier.fillMaxWidth()
+            WindowWidthSizeClass.Medium -> Modifier.width(OverlayShade.Dimensions.PanelWidthMedium)
+            WindowWidthSizeClass.Expanded -> Modifier.width(OverlayShade.Dimensions.PanelWidthLarge)
+            else -> error("Unsupported WindowWidthSizeClass \"$widthSizeClass\"")
+        }
+    )
+}
+
 object OverlayShade {
     object Elements {
         val Scrim = ElementKey("OverlayShadeScrim", scenePicker = LowestZIndexScenePicker)
@@ -127,6 +151,8 @@
     object Dimensions {
         val ScrimContentPadding = 16.dp
         val PanelCornerRadius = 46.dp
+        val PanelWidthMedium = 390.dp
+        val PanelWidthLarge = 474.dp
     }
 
     object Shapes {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 9d689fc..33a630c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.clipScrollableContainer
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
@@ -42,6 +43,7 @@
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
@@ -67,6 +69,7 @@
 import com.android.compose.animation.scene.animateSceneFloatAsState
 import com.android.compose.modifiers.padding
 import com.android.compose.modifiers.thenIf
+import com.android.compose.windowsizeclass.LocalWindowSizeClass
 import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
 import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
@@ -235,6 +238,10 @@
     val shouldPunchHoleBehindScrim =
         layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) ||
             layoutState.isTransitioningBetween(Scenes.Lockscreen, Scenes.Shade)
+    // Media is visible and we are in landscape on a small height screen
+    val mediaInRow =
+        isMediaVisible &&
+            LocalWindowSizeClass.current.heightSizeClass == WindowHeightSizeClass.Compact
 
     Box(
         modifier =
@@ -274,22 +281,39 @@
                                 createBatteryMeterViewController = createBatteryMeterViewController,
                                 statusBarIconController = statusBarIconController,
                             )
-                            Box(Modifier.element(QuickSettings.Elements.QuickQuickSettings)) {
-                                QuickSettings(
-                                    viewModel.qsSceneAdapter,
-                                    { viewModel.qsSceneAdapter.qqsHeight },
-                                    isSplitShade = false,
-                                    squishiness = { tileSquishiness },
+
+                            val content: @Composable (Modifier) -> Unit = { modifier ->
+                                Box(
+                                    Modifier.element(QuickSettings.Elements.QuickQuickSettings)
+                                        .then(modifier)
+                                ) {
+                                    QuickSettings(
+                                        viewModel.qsSceneAdapter,
+                                        { viewModel.qsSceneAdapter.qqsHeight },
+                                        isSplitShade = false,
+                                        squishiness = { tileSquishiness },
+                                    )
+                                }
+
+                                MediaCarousel(
+                                    isVisible = isMediaVisible,
+                                    mediaHost = mediaHost,
+                                    modifier = Modifier.fillMaxWidth().then(modifier),
+                                    carouselController = mediaCarouselController,
                                 )
                             }
 
-                            MediaCarousel(
-                                isVisible = isMediaVisible,
-                                mediaHost = mediaHost,
-                                modifier = Modifier.fillMaxWidth(),
-                                carouselController = mediaCarouselController,
-                            )
-
+                            if (!mediaInRow) {
+                                content(Modifier)
+                            } else {
+                                Row(
+                                    modifier = Modifier.fillMaxWidth(),
+                                    horizontalArrangement = spacedBy(16.dp),
+                                    verticalAlignment = Alignment.CenterVertically,
+                                ) {
+                                    content(Modifier.weight(1f))
+                                }
+                            }
                             Spacer(modifier = Modifier.height(16.dp))
                         }
                     },
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/EdgeToEdgeDialogDelegate.kt b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/EdgeToEdgeDialogDelegate.kt
index 55dfed4..5fc78c0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/EdgeToEdgeDialogDelegate.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/EdgeToEdgeDialogDelegate.kt
@@ -17,8 +17,11 @@
 package com.android.systemui.statusbar.phone
 
 import android.os.Bundle
+import android.util.DisplayMetrics
 import android.view.Gravity
 import android.view.WindowManager
+import com.android.systemui.animation.back.BackAnimationSpec
+import com.android.systemui.animation.back.bottomSheetForSysUi
 
 /** [DialogDelegate] that configures a dialog to be an edge-to-edge one. */
 class EdgeToEdgeDialogDelegate : DialogDelegate<SystemUIDialog> {
@@ -40,4 +43,10 @@
     override fun getWidth(dialog: SystemUIDialog): Int = WindowManager.LayoutParams.MATCH_PARENT
 
     override fun getHeight(dialog: SystemUIDialog): Int = WindowManager.LayoutParams.MATCH_PARENT
+
+    override fun getBackAnimationSpec(
+        displayMetricsProvider: () -> DisplayMetrics
+    ): BackAnimationSpec {
+        return BackAnimationSpec.bottomSheetForSysUi(displayMetricsProvider)
+    }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
index 83b8158..ab14911 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
@@ -23,7 +23,6 @@
 import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -43,16 +42,7 @@
 fun VolumePanelRoot(
     viewModel: VolumePanelViewModel,
     modifier: Modifier = Modifier,
-    onDismiss: () -> Unit
 ) {
-    LaunchedEffect(viewModel) {
-        viewModel.volumePanelState.collect {
-            if (!it.isVisible) {
-                onDismiss()
-            }
-        }
-    }
-
     val accessibilityTitle = stringResource(R.string.accessibility_volume_settings)
     val state: VolumePanelState by viewModel.volumePanelState.collectAsStateWithLifecycle()
     val components by viewModel.componentsLayout.collectAsStateWithLifecycle(null)
diff --git a/packages/SystemUI/docs/demo_mode.md b/packages/SystemUI/docs/demo_mode.md
index b2424f4..ade5171 100644
--- a/packages/SystemUI/docs/demo_mode.md
+++ b/packages/SystemUI/docs/demo_mode.md
@@ -22,42 +22,45 @@
 <br/>
 Commands are sent as string extras with key ```command``` (required). Possible values are:
 
-| Command              | Subcommand                 | Argument       | Description
-| ---                  | ---                        | ---            | ---
-| ```enter```          |                            |                | Enters demo mode, bar state allowed to be modified (for convenience, any of the other non-exit commands will automatically flip demo mode on, no need to call this explicitly in practice)
-| ```exit```           |                            |                | Exits demo mode, bars back to their system-driven state
-| ```battery```        |                            |                | Control the battery display
-|                      | ```level```                |                | Sets the battery level (0 - 100)
-|                      | ```plugged```              |                | Sets charging state (```true```, ```false```)
-|                      | ```powersave```            |                | Sets power save mode (```true```, ```anything else```)
-| ```network```        |                            |                | Control the RSSI display
-|                      | ```airplane```             |                | ```show``` to show icon, any other value to hide
-|                      | ```fully```                |                | Sets MCS state to fully connected (```true```, ```false```)
-|                      | ```wifi```                 |                | ```show``` to show icon, any other value to hide
-|                      |                            | ```level```    | Sets wifi level (null or 0-4)
-|                      | ```mobile```               |                | ```show``` to show icon, any other value to hide
-|                      |                            | ```datatype``` | Values: ```1x```, ```3g```, ```4g```, ```e```, ```g```, ```h```, ```lte```, ```roam```, any other value to hide
-|                      |                            | ```level```    | Sets mobile signal strength level (null or 0-4)
-|                      | ```carriernetworkchange``` |                | Sets mobile signal icon to carrier network change UX when disconnected (```show``` to show icon, any other value to hide)
-|                      | ```sims```                 |                | Sets the number of sims (1-8)
-|                      | ```nosim```                |                | ```show``` to show icon, any other value to hide
-| ```bars```           |                            |                | Control the visual style of the bars (opaque, translucent, etc)
-|                      | ```mode```                 |                | Sets the bars visual style (opaque, translucent, semi-transparent)
-| ```status```         |                            |                | Control the system status icons
-|                      | ```volume```               |                | Sets the icon in the volume slot (```silent```, ```vibrate```, any other value to hide)
-|                      | ```bluetooth```            |                | Sets the icon in the bluetooth slot (```connected```, ```disconnected```, any other value to hide)
-|                      | ```location```             |                | Sets the icon in the location slot (```show```, any other value to hide)
-|                      | ```alarm```                |                | Sets the icon in the alarm_clock slot (```show```, any other value to hide)
-|                      | ```sync```                 |                | Sets the icon in the sync_active slot (```show```, any other value to hide)
-|                      | ```tty```                  |                | Sets the icon in the tty slot (```show```, any other value to hide)
-|                      | ```eri```                  |                | Sets the icon in the cdma_eri slot (```show```, any other value to hide)
-|                      | ```mute```                 |                | Sets the icon in the mute slot (```show```, any other value to hide)
-|                      | ```speakerphone```         |                | Sets the icon in the speakerphone slot (```show```, any other value to hide)
-| ```notifications```  |                            |                | Control the notification icons
-|                      | ```visible```              |                | ```false``` to hide the notification icons, any other value to show
-| ```clock```          |                            |                | Control the clock display
-|                      | ```millis```               |                | Sets the time in millis
-|                      | ```hhmm```                 |                | Sets the time in hh:mm
+| Command              | Subcommand                 | Argument         | Description
+| ---                  |----------------------------|------------------| ---
+| ```enter```          |                            |                  | Enters demo mode, bar state allowed to be modified (for convenience, any of the other non-exit commands will automatically flip demo mode on, no need to call this explicitly in practice)
+| ```exit```           |                            |                  | Exits demo mode, bars back to their system-driven state
+| ```battery```        |                            |                  | Control the battery display
+|                      | ```level```                |                  | Sets the battery level (0 - 100)
+|                      | ```plugged```              |                  | Sets charging state (```true```, ```false```)
+|                      | ```powersave```            |                  | Sets power save mode (```true```, ```anything else```)
+| ```network```        |                            |                  | Control the RSSI display
+|                      | ```airplane```             |                  | ```show``` to show icon, any other value to hide
+|                      | ```fully```                |                  | Sets MCS state to fully connected (```true```, ```false```)
+|                      | ```wifi```                 |                  | ```show``` to show icon, any other value to hide
+|                      |                            | ```level```      | Sets wifi level (null or 0-4)
+|                      | ```mobile```               |                  | ```show``` to show icon, any other value to hide
+|                      |                            | ```datatype```   | Values: ```1x```, ```3g```, ```4g```, ```e```, ```g```, ```h```, ```lte```, ```roam```, any other value to hide
+|                      |                            | ```level```      | Sets mobile signal strength level (null or 0-4)
+|                      | ```satellite```            |                  | ```show``` to show icon, any other value to hide
+|                      |                            | ```connection``` | ```connected```, ```off```, ```on```, or ```unknown``` (matches SatelliteConnectionState enum)
+|                      |                            | ```level```      | Sets satellite signal strength level (0-4)
+|                      | ```carriernetworkchange``` |                  | Sets mobile signal icon to carrier network change UX when disconnected (```show``` to show icon, any other value to hide)
+|                      | ```sims```                 |                  | Sets the number of sims (1-8)
+|                      | ```nosim```                |                  | ```show``` to show icon, any other value to hide
+| ```bars```           |                            |                  | Control the visual style of the bars (opaque, translucent, etc)
+|                      | ```mode```                 |                  | Sets the bars visual style (opaque, translucent, semi-transparent)
+| ```status```         |                            |                  | Control the system status icons
+|                      | ```volume```               |                  | Sets the icon in the volume slot (```silent```, ```vibrate```, any other value to hide)
+|                      | ```bluetooth```            |                  | Sets the icon in the bluetooth slot (```connected```, ```disconnected```, any other value to hide)
+|                      | ```location```             |                  | Sets the icon in the location slot (```show```, any other value to hide)
+|                      | ```alarm```                |                  | Sets the icon in the alarm_clock slot (```show```, any other value to hide)
+|                      | ```sync```                 |                  | Sets the icon in the sync_active slot (```show```, any other value to hide)
+|                      | ```tty```                  |                  | Sets the icon in the tty slot (```show```, any other value to hide)
+|                      | ```eri```                  |                  | Sets the icon in the cdma_eri slot (```show```, any other value to hide)
+|                      | ```mute```                 |                  | Sets the icon in the mute slot (```show```, any other value to hide)
+|                      | ```speakerphone```         |                  | Sets the icon in the speakerphone slot (```show```, any other value to hide)
+| ```notifications```  |                            |                  | Control the notification icons
+|                      | ```visible```              |                  | ```false``` to hide the notification icons, any other value to show
+| ```clock```          |                            |                  | Control the clock display
+|                      | ```millis```               |                  | Sets the time in millis
+|                      | ```hhmm```                 |                  | Sets the time in hh:mm
 
 ## Examples
 Enter demo mode
@@ -90,6 +93,15 @@
 ```
 
 
+Show the satellite icon
+
+```
+# Sets mobile to be out-of-service, which is required for satellite to show
+adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e level 0
+# Sets satellite to be connected
+adb shell am broadcast -a com.android.systemui.demo -e command network -e satellite show -e level 4 -e connection connected
+```
+
 Show the silent volume icon
 
 ```
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
index d88260f..3035481 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt
@@ -16,90 +16,93 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.keyguardUpdateMonitor
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
-import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository
-import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepositoryImpl
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
-import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.FakeSystemClock
-import com.android.systemui.util.time.SystemClock
-import dagger.Lazy
-import kotlinx.coroutines.test.TestScope
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.statusbar.policy.keyguardStateController
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.mock
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class AlternateBouncerInteractorTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+
     private lateinit var underTest: AlternateBouncerInteractor
-    private lateinit var bouncerRepository: KeyguardBouncerRepository
-    private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository
-    private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository
-    @Mock private lateinit var statusBarStateController: StatusBarStateController
-    @Mock private lateinit var keyguardStateController: KeyguardStateController
-    @Mock private lateinit var systemClock: SystemClock
-    @Mock private lateinit var bouncerLogger: TableLogBuffer
-    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
 
     @Before
     fun setup() {
-        MockitoAnnotations.initMocks(this)
-        bouncerRepository =
-            KeyguardBouncerRepositoryImpl(
-                FakeSystemClock(),
-                TestScope().backgroundScope,
-                bouncerLogger,
-            )
-        biometricSettingsRepository = FakeBiometricSettingsRepository()
-        fingerprintPropertyRepository = FakeFingerprintPropertyRepository()
-
-        mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
-        initializeUnderTest()
-    }
-
-    private fun initializeUnderTest() {
-        // Set any feature flags before creating the alternateBouncerInteractor
-        underTest =
-            AlternateBouncerInteractor(
-                statusBarStateController,
-                keyguardStateController,
-                bouncerRepository,
-                fingerprintPropertyRepository,
-                biometricSettingsRepository,
-                systemClock,
-                keyguardUpdateMonitor,
-                Lazy { mock(DeviceEntryFingerprintAuthInteractor::class.java) },
-                Lazy { mock(KeyguardInteractor::class.java) },
-                Lazy { mock(KeyguardTransitionInteractor::class.java) },
-                TestScope().backgroundScope,
-            )
+        underTest = kosmos.alternateBouncerInteractor
     }
 
     @Test(expected = IllegalStateException::class)
+    @EnableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
     fun enableUdfpsRefactor_deprecatedShowMethod_throwsIllegalStateException() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
         underTest.show()
     }
 
     @Test
+    @DisableSceneContainer
+    fun canShowAlternateBouncer_false_dueToTransitionState() =
+        kosmos.testScope.runTest {
+            givenAlternateBouncerSupported()
+            val canShowAlternateBouncer by collectLastValue(underTest.canShowAlternateBouncer)
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionStep(
+                from = KeyguardState.AOD,
+                to = KeyguardState.GONE,
+                validateStep = false,
+            )
+            assertFalse(canShowAlternateBouncer!!)
+        }
+
+    @Test
+    @EnableSceneContainer
+    fun canShowAlternateBouncer_false_dueToTransitionState_scene_container() =
+        kosmos.testScope.runTest {
+            givenAlternateBouncerSupported()
+            val canShowAlternateBouncer by collectLastValue(underTest.canShowAlternateBouncer)
+            val isDeviceUnlocked by
+                collectLastValue(
+                    kosmos.deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked }
+                )
+            assertThat(isDeviceUnlocked).isFalse()
+
+            kosmos.authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)
+            assertThat(isDeviceUnlocked).isTrue()
+            kosmos.sceneInteractor.changeScene(Scenes.Gone, "")
+
+            assertThat(canShowAlternateBouncer).isFalse()
+        }
+
+    @Test
+    @DisableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
     fun canShowAlternateBouncerForFingerprint_givenCanShow() {
         givenCanShowAlternateBouncer()
         assertTrue(underTest.canShowAlternateBouncerForFingerprint())
@@ -108,7 +111,7 @@
     @Test
     fun canShowAlternateBouncerForFingerprint_alternateBouncerUIUnavailable() {
         givenCanShowAlternateBouncer()
-        bouncerRepository.setAlternateBouncerUIAvailable(false)
+        kosmos.keyguardBouncerRepository.setAlternateBouncerUIAvailable(false)
 
         assertFalse(underTest.canShowAlternateBouncerForFingerprint())
     }
@@ -116,7 +119,7 @@
     @Test
     fun canShowAlternateBouncerForFingerprint_ifFingerprintIsNotUsuallyAllowed() {
         givenCanShowAlternateBouncer()
-        biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+        kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
 
         assertFalse(underTest.canShowAlternateBouncerForFingerprint())
     }
@@ -124,7 +127,7 @@
     @Test
     fun canShowAlternateBouncerForFingerprint_strongBiometricNotAllowed() {
         givenCanShowAlternateBouncer()
-        biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false)
+        kosmos.biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false)
 
         assertFalse(underTest.canShowAlternateBouncerForFingerprint())
     }
@@ -132,23 +135,24 @@
     @Test
     fun canShowAlternateBouncerForFingerprint_fingerprintLockedOut() {
         givenCanShowAlternateBouncer()
-        whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(true)
+        whenever(kosmos.keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(true)
 
         assertFalse(underTest.canShowAlternateBouncerForFingerprint())
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
     fun show_whenCanShow() {
         givenCanShowAlternateBouncer()
 
         assertTrue(underTest.show())
-        assertTrue(bouncerRepository.alternateBouncerVisible.value)
+        assertTrue(kosmos.keyguardBouncerRepository.alternateBouncerVisible.value)
     }
 
     @Test
     fun canShowAlternateBouncerForFingerprint_butCanDismissLockScreen() {
         givenCanShowAlternateBouncer()
-        whenever(keyguardStateController.isUnlocked).thenReturn(true)
+        whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(true)
 
         assertFalse(underTest.canShowAlternateBouncerForFingerprint())
     }
@@ -156,82 +160,86 @@
     @Test
     fun canShowAlternateBouncerForFingerprint_primaryBouncerShowing() {
         givenCanShowAlternateBouncer()
-        bouncerRepository.setPrimaryShow(true)
+        kosmos.keyguardBouncerRepository.setPrimaryShow(true)
 
         assertFalse(underTest.canShowAlternateBouncerForFingerprint())
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
     fun show_whenCannotShow() {
         givenCannotShowAlternateBouncer()
 
         assertFalse(underTest.show())
-        assertFalse(bouncerRepository.alternateBouncerVisible.value)
+        assertFalse(kosmos.keyguardBouncerRepository.alternateBouncerVisible.value)
     }
 
     @Test
     fun hide_wasPreviouslyShowing() {
-        bouncerRepository.setAlternateVisible(true)
+        kosmos.keyguardBouncerRepository.setAlternateVisible(true)
 
         assertTrue(underTest.hide())
-        assertFalse(bouncerRepository.alternateBouncerVisible.value)
+        assertFalse(kosmos.keyguardBouncerRepository.alternateBouncerVisible.value)
     }
 
     @Test
     fun hide_wasNotPreviouslyShowing() {
-        bouncerRepository.setAlternateVisible(false)
+        kosmos.keyguardBouncerRepository.setAlternateVisible(false)
 
         assertFalse(underTest.hide())
-        assertFalse(bouncerRepository.alternateBouncerVisible.value)
+        assertFalse(kosmos.keyguardBouncerRepository.alternateBouncerVisible.value)
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
     fun canShowAlternateBouncerForFingerprint_rearFps() {
-        mSetFlagsRule.enableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
-        initializeUnderTest()
         givenCanShowAlternateBouncer()
-        fingerprintPropertyRepository.supportsRearFps() // does not support alternate bouncer
+        kosmos.fingerprintPropertyRepository.supportsRearFps() // does not support alternate bouncer
 
         assertFalse(underTest.canShowAlternateBouncerForFingerprint())
     }
 
     @Test
+    @DisableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR)
     fun alternateBouncerUiAvailable_fromMultipleSources() {
-        initializeUnderTest()
-        assertFalse(bouncerRepository.alternateBouncerUIAvailable.value)
+        assertFalse(kosmos.keyguardBouncerRepository.alternateBouncerUIAvailable.value)
 
         // GIVEN there are two different sources indicating the alternate bouncer is available
         underTest.setAlternateBouncerUIAvailable(true, "source1")
         underTest.setAlternateBouncerUIAvailable(true, "source2")
-        assertTrue(bouncerRepository.alternateBouncerUIAvailable.value)
+        assertTrue(kosmos.keyguardBouncerRepository.alternateBouncerUIAvailable.value)
 
         // WHEN one of the sources no longer says the UI is available
         underTest.setAlternateBouncerUIAvailable(false, "source1")
 
         // THEN alternate bouncer UI is still available (from the other source)
-        assertTrue(bouncerRepository.alternateBouncerUIAvailable.value)
+        assertTrue(kosmos.keyguardBouncerRepository.alternateBouncerUIAvailable.value)
 
         // WHEN all sources say the UI is not available
         underTest.setAlternateBouncerUIAvailable(false, "source2")
 
         // THEN alternate boucer UI is not available
-        assertFalse(bouncerRepository.alternateBouncerUIAvailable.value)
+        assertFalse(kosmos.keyguardBouncerRepository.alternateBouncerUIAvailable.value)
+    }
+
+    private fun givenAlternateBouncerSupported() {
+        if (DeviceEntryUdfpsRefactor.isEnabled) {
+            kosmos.fingerprintPropertyRepository.supportsUdfps()
+        } else {
+            kosmos.keyguardBouncerRepository.setAlternateBouncerUIAvailable(true)
+        }
     }
 
     private fun givenCanShowAlternateBouncer() {
-        if (DeviceEntryUdfpsRefactor.isEnabled) {
-            fingerprintPropertyRepository.supportsUdfps()
-        } else {
-            bouncerRepository.setAlternateBouncerUIAvailable(true)
-        }
-        bouncerRepository.setPrimaryShow(false)
-        biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
-        biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
-        whenever(keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false)
-        whenever(keyguardStateController.isUnlocked).thenReturn(false)
+        givenAlternateBouncerSupported()
+        kosmos.keyguardBouncerRepository.setPrimaryShow(false)
+        kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+        kosmos.biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+        whenever(kosmos.keyguardUpdateMonitor.isFingerprintLockedOut).thenReturn(false)
+        whenever(kosmos.keyguardStateController.isUnlocked).thenReturn(false)
     }
 
     private fun givenCannotShowAlternateBouncer() {
-        biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+        kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
     }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
index 2d78a9b..45e7d8a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
@@ -39,7 +39,7 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val underTest by lazy {
-        CommunalRepositoryImpl(
+        CommunalSceneRepositoryImpl(
             kosmos.applicationCoroutineScope,
             kosmos.sceneDataSource,
         )
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
index 5a7cbf6..6b896c6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
@@ -21,7 +21,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
 import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
 import com.android.systemui.communal.data.repository.fakeCommunalRepository
 import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
@@ -48,7 +48,7 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
 
-    private lateinit var communalRepository: FakeCommunalRepository
+    private lateinit var communalRepository: FakeCommunalSceneRepository
     private lateinit var widgetRepository: FakeCommunalWidgetRepository
     private lateinit var keyguardRepository: FakeKeyguardRepository
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 83227e1..12389a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -39,7 +39,7 @@
 import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl
 import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
 import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
 import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
 import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
@@ -48,6 +48,7 @@
 import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
 import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.domain.model.CommunalTransitionProgressModel
 import com.android.systemui.communal.shared.model.CommunalContentSize
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
@@ -111,7 +112,7 @@
     private val testScope = kosmos.testScope
 
     private lateinit var tutorialRepository: FakeCommunalTutorialRepository
-    private lateinit var communalRepository: FakeCommunalRepository
+    private lateinit var communalRepository: FakeCommunalSceneRepository
     private lateinit var mediaRepository: FakeCommunalMediaRepository
     private lateinit var widgetRepository: FakeCommunalWidgetRepository
     private lateinit var smartspaceRepository: FakeSmartspaceRepository
@@ -508,30 +509,6 @@
         }
 
     @Test
-    fun desiredScene_communalNotAvailable_returnsBlank() =
-        testScope.runTest {
-            kosmos.setCommunalAvailable(true)
-            runCurrent()
-
-            val desiredScene by collectLastValue(underTest.desiredScene)
-
-            underTest.changeScene(CommunalScenes.Communal)
-            assertThat(desiredScene).isEqualTo(CommunalScenes.Communal)
-
-            kosmos.setCommunalAvailable(false)
-            runCurrent()
-
-            // Scene returns blank when communal is not available.
-            assertThat(desiredScene).isEqualTo(CommunalScenes.Blank)
-
-            kosmos.setCommunalAvailable(true)
-            runCurrent()
-
-            // After re-enabling, scene goes back to Communal.
-            assertThat(desiredScene).isEqualTo(CommunalScenes.Communal)
-        }
-
-    @Test
     fun transitionProgress_onTargetScene_fullProgress() =
         testScope.runTest {
             val targetScene = CommunalScenes.Blank
@@ -545,7 +522,8 @@
             underTest.setTransitionState(transitionState)
 
             // We're on the target scene.
-            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
+            assertThat(transitionProgress)
+                .isEqualTo(CommunalTransitionProgressModel.Idle(targetScene))
         }
 
     @Test
@@ -563,7 +541,8 @@
             underTest.setTransitionState(transitionState)
 
             // Transition progress is still idle, but we're not on the target scene.
-            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))
+            assertThat(transitionProgress)
+                .isEqualTo(CommunalTransitionProgressModel.Idle(currentScene))
         }
 
     @Test
@@ -581,7 +560,8 @@
             underTest.setTransitionState(transitionState)
 
             // Progress starts at 0.
-            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))
+            assertThat(transitionProgress)
+                .isEqualTo(CommunalTransitionProgressModel.Idle(currentScene))
 
             val progress = MutableStateFlow(0f)
             transitionState =
@@ -599,16 +579,18 @@
 
             // Partially transition.
             progress.value = .4f
-            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Transition(.4f))
+            assertThat(transitionProgress)
+                .isEqualTo(CommunalTransitionProgressModel.Transition(.4f))
 
             // Transition is at full progress.
             progress.value = 1f
-            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Transition(1f))
+            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgressModel.Transition(1f))
 
             // Transition finishes.
             transitionState = MutableStateFlow(ObservableTransitionState.Idle(targetScene))
             underTest.setTransitionState(transitionState)
-            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
+            assertThat(transitionProgress)
+                .isEqualTo(CommunalTransitionProgressModel.Idle(targetScene))
         }
 
     @Test
@@ -626,7 +608,8 @@
             underTest.setTransitionState(transitionState)
 
             // Progress starts at 0.
-            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(currentScene))
+            assertThat(transitionProgress)
+                .isEqualTo(CommunalTransitionProgressModel.Idle(currentScene))
 
             val progress = MutableStateFlow(0f)
             transitionState =
@@ -646,16 +629,19 @@
             progress.value = .4f
 
             // This is a transition we don't care about the progress of.
-            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.OtherTransition)
+            assertThat(transitionProgress)
+                .isEqualTo(CommunalTransitionProgressModel.OtherTransition)
 
             // Transition is at full progress.
             progress.value = 1f
-            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.OtherTransition)
+            assertThat(transitionProgress)
+                .isEqualTo(CommunalTransitionProgressModel.OtherTransition)
 
             // Transition finishes.
             transitionState = MutableStateFlow(ObservableTransitionState.Idle(targetScene))
             underTest.setTransitionState(transitionState)
-            assertThat(transitionProgress).isEqualTo(CommunalTransitionProgress.Idle(targetScene))
+            assertThat(transitionProgress)
+                .isEqualTo(CommunalTransitionProgressModel.Idle(targetScene))
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index be44339..6b80775 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -28,7 +28,7 @@
 import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
 import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
 import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
 import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
@@ -106,7 +106,7 @@
     private lateinit var userRepository: FakeUserRepository
     private lateinit var shadeTestUtil: ShadeTestUtil
     private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
-    private lateinit var communalRepository: FakeCommunalRepository
+    private lateinit var communalRepository: FakeCommunalSceneRepository
 
     private lateinit var underTest: CommunalViewModel
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index eef2337..933f095 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -45,7 +45,7 @@
 import com.android.systemui.ambient.touch.scrim.ScrimManager
 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
 import com.android.systemui.communal.data.repository.fakeCommunalRepository
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.communalInteractor
@@ -79,7 +79,6 @@
 import org.mockito.Mockito
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.isNull
-import org.mockito.Mockito.spy
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -156,7 +155,7 @@
     @Mock lateinit var mDreamOverlayCallbackController: DreamOverlayCallbackController
 
     private lateinit var bouncerRepository: FakeKeyguardBouncerRepository
-    private lateinit var communalRepository: FakeCommunalRepository
+    private lateinit var communalRepository: FakeCommunalSceneRepository
 
     @Captor var mViewCaptor: ArgumentCaptor<View>? = null
     private lateinit var mService: DreamOverlayService
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModelTest.kt
new file mode 100644
index 0000000..460a1fc
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModelTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.graphics.Color
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DeviceEntryForegroundViewModelTest : SysuiTestCase() {
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val underTest: DeviceEntryForegroundViewModel =
+        kosmos.deviceEntryForegroundIconViewModel
+
+    @Test
+    fun aodIconColorWhite() =
+        testScope.runTest {
+            val viewModel by collectLastValue(underTest.viewModel)
+
+            givenUdfpsEnrolledAndEnabled()
+            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
+                from = KeyguardState.LOCKSCREEN,
+                to = KeyguardState.AOD,
+                testScope = testScope,
+            )
+
+            assertThat(viewModel?.useAodVariant).isEqualTo(true)
+            assertThat(viewModel?.tint).isEqualTo(Color.WHITE)
+        }
+
+    private fun givenUdfpsEnrolledAndEnabled() {
+        kosmos.fakeFingerprintPropertyRepository.supportsUdfps()
+        kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 20ffa33..33e2cac 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -26,7 +26,7 @@
 import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.communal.data.repository.communalRepository
+import com.android.systemui.communal.data.repository.communalSceneRepository
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
@@ -71,7 +71,7 @@
     private val testScope = kosmos.testScope
     private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
     private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
-    private val communalRepository by lazy { kosmos.communalRepository }
+    private val communalRepository by lazy { kosmos.communalSceneRepository }
     private val screenOffAnimationController by lazy { kosmos.screenOffAnimationController }
     private val deviceEntryRepository by lazy { kosmos.fakeDeviceEntryRepository }
     private val notificationsKeyguardInteractor by lazy { kosmos.notificationsKeyguardInteractor }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
index f1cd0c8..79e4fef 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
@@ -179,6 +179,7 @@
         val label = context.getString(R.string.status_bar_alarm)
         return QSTileState(
             { Icon.Loaded(context.getDrawable(R.drawable.ic_alarm)!!, null) },
+            R.drawable.ic_alarm,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt
index 6e9db2c..a0d26c2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt
@@ -254,6 +254,7 @@
         val label = context.getString(R.string.battery_detail_switch_title)
         return QSTileState(
             { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            iconRes,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt
index d05e98f..ea7b7c5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt
@@ -78,6 +78,7 @@
         val label = context.getString(R.string.quick_settings_color_correction_label)
         return QSTileState(
             { Icon.Loaded(context.getDrawable(R.drawable.ic_qs_color_correction)!!, null) },
+            R.drawable.ic_qs_color_correction,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt
index 3972938..b4ff565 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt
@@ -245,6 +245,7 @@
     ): QSTileState {
         return QSTileState(
             { icon?.let { com.android.systemui.common.shared.model.Icon.Loaded(icon, null) } },
+            null,
             "test label",
             activationState,
             "test subtitle",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt
index b7b3fdb..f8e01be 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt
@@ -66,6 +66,7 @@
                     null
                 )
             },
+            R.drawable.ic_qs_font_scaling,
             context.getString(R.string.quick_settings_font_scaling_label),
             QSTileState.ActivationState.ACTIVE,
             null,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
index 39755bf..c44836a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt
@@ -70,6 +70,7 @@
                 QSTileState.ActivationState.ACTIVE,
                 context.getString(R.string.quick_settings_networks_available),
                 Icon.Loaded(context.getDrawable(wifiRes)!!, contentDescription = null),
+                wifiRes,
                 context.getString(R.string.quick_settings_internet_label)
             )
         QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
@@ -96,6 +97,7 @@
                     context.getDrawable(R.drawable.ic_qs_no_internet_unavailable)!!,
                     contentDescription = null
                 ),
+                R.drawable.ic_qs_no_internet_unavailable,
                 context.getString(R.string.quick_settings_networks_unavailable)
             )
         QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
@@ -105,11 +107,13 @@
         activationState: QSTileState.ActivationState,
         secondaryLabel: String,
         icon: Icon,
+        iconRes: Int,
         contentDescription: String,
     ): QSTileState {
         val label = context.getString(R.string.quick_settings_internet_label)
         return QSTileState(
             { icon },
+            iconRes,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt
index ccd7ed9..a7bd697 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt
@@ -39,9 +39,7 @@
     private val colorInversionTileConfig = kosmos.qsColorInversionTileConfig
     private val subtitleArrayId =
         SubtitleArrayMapping.getSubtitleId(colorInversionTileConfig.tileSpec.spec)
-    private val subtitleArray by lazy {
-        context.resources.getStringArray(subtitleArrayId)
-    }
+    private val subtitleArray by lazy { context.resources.getStringArray(subtitleArrayId) }
     // Using lazy (versus =) to make sure we override the right context -- see b/311612168
     private val mapper by lazy {
         ColorInversionTileMapper(
@@ -93,6 +91,7 @@
         val label = context.getString(R.string.quick_settings_inversion_label)
         return QSTileState(
             { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            iconRes,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt
index 5d2e701..75273f2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt
@@ -281,21 +281,16 @@
         secondaryLabel: String?
     ): QSTileState {
         val label = context.getString(R.string.quick_settings_night_display_label)
-
+        val iconRes =
+            if (activationState == QSTileState.ActivationState.ACTIVE)
+                R.drawable.qs_nightlight_icon_on
+            else R.drawable.qs_nightlight_icon_off
         val contentDescription =
             if (TextUtils.isEmpty(secondaryLabel)) label
             else TextUtils.concat(label, ", ", secondaryLabel)
         return QSTileState(
-            {
-                Icon.Loaded(
-                    context.getDrawable(
-                        if (activationState == QSTileState.ActivationState.ACTIVE)
-                            R.drawable.qs_nightlight_icon_on
-                        else R.drawable.qs_nightlight_icon_off
-                    )!!,
-                    null
-                )
-            },
+            { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            iconRes,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt
index 7ef020d..3189a9e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt
@@ -97,6 +97,7 @@
         val label = context.getString(R.string.quick_settings_onehanded_label)
         return QSTileState(
             { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            iconRes,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt
index d26a213..08e5cbe 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt
@@ -100,6 +100,7 @@
                     null
                 )
             },
+            com.android.systemui.res.R.drawable.ic_qr_code_scanner,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt
index 10e9bd6..ca30e9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt
@@ -83,17 +83,13 @@
     ): QSTileState {
         val label =
             context.getString(com.android.internal.R.string.reduce_bright_colors_feature_name)
+        val iconRes =
+            if (activationState == QSTileState.ActivationState.ACTIVE)
+                R.drawable.qs_extra_dim_icon_on
+            else R.drawable.qs_extra_dim_icon_off
         return QSTileState(
-            {
-                Icon.Loaded(
-                    context.getDrawable(
-                        if (activationState == QSTileState.ActivationState.ACTIVE)
-                            R.drawable.qs_extra_dim_icon_on
-                        else R.drawable.qs_extra_dim_icon_off
-                    )!!,
-                    null
-                )
-            },
+            { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            iconRes,
             label,
             activationState,
             context.resources
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt
index 60c69f4..04ca38f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt
@@ -172,6 +172,7 @@
         val label = context.getString(R.string.quick_settings_rotation_unlocked_label)
         return QSTileState(
             { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            iconRes,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt
index d162c77..9bb6141 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt
@@ -92,6 +92,7 @@
 
         return QSTileState(
             { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            iconRes,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt
index 31ae9c5..336b566 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt
@@ -111,6 +111,7 @@
 
         return QSTileState(
             { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            iconRes,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt
index 5e7aadc..b08f39b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt
@@ -147,6 +147,7 @@
 
         return QSTileState(
             { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            iconRes,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt
index a977606..c021caa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt
@@ -70,6 +70,7 @@
     ): QSTileState {
         return QSTileState(
             { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            iconRes,
             label,
             activationState,
             secondaryLabel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index afe7b8f..7388d51 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -54,9 +54,11 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyFloat
 import org.mockito.Mockito.anyInt
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 
 @SmallTest
@@ -162,16 +164,25 @@
 
             with(qsImpl!!) {
                 verify(this).setQsVisible(false)
-                verify(this)
+                verify(this, never())
                     .setQsExpansion(
-                        /* expansion= */ 0f,
-                        /* panelExpansionFraction= */ 1f,
-                        /* proposedTranslation= */ 0f,
-                        /* squishinessFraction= */ 1f,
+                        /* expansion= */ anyFloat(),
+                        /* panelExpansionFraction= */ anyFloat(),
+                        /* proposedTranslation= */ anyFloat(),
+                        /* squishinessFraction= */ anyFloat(),
                     )
                 verify(this).setListening(false)
                 verify(this).setExpanded(false)
             }
+
+            underTest.applyLatestExpansionAndSquishiness()
+            verify(qsImpl!!)
+                .setQsExpansion(
+                    /* expansion= */ 0f,
+                    /* panelExpansionFraction= */ 1f,
+                    /* proposedTranslation= */ 0f,
+                    /* squishinessFraction= */ 1f,
+                )
         }
 
     @Test
@@ -186,16 +197,25 @@
             underTest.setState(QSSceneAdapter.State.QQS)
             with(qsImpl!!) {
                 verify(this).setQsVisible(true)
-                verify(this)
+                verify(this, never())
                     .setQsExpansion(
-                        /* expansion= */ 0f,
-                        /* panelExpansionFraction= */ 1f,
-                        /* proposedTranslation= */ 0f,
-                        /* squishinessFraction= */ 1f,
+                        /* expansion= */ anyFloat(),
+                        /* panelExpansionFraction= */ anyFloat(),
+                        /* proposedTranslation= */ anyFloat(),
+                        /* squishinessFraction= */ anyFloat(),
                     )
                 verify(this).setListening(true)
                 verify(this).setExpanded(false)
             }
+
+            underTest.applyLatestExpansionAndSquishiness()
+            verify(qsImpl!!)
+                .setQsExpansion(
+                    /* expansion= */ 0f,
+                    /* panelExpansionFraction= */ 1f,
+                    /* proposedTranslation= */ 0f,
+                    /* squishinessFraction= */ 1f,
+                )
         }
 
     @Test
@@ -210,16 +230,25 @@
             underTest.setState(QSSceneAdapter.State.QS)
             with(qsImpl!!) {
                 verify(this).setQsVisible(true)
-                verify(this)
+                verify(this, never())
                     .setQsExpansion(
-                        /* expansion= */ 1f,
-                        /* panelExpansionFraction= */ 1f,
-                        /* proposedTranslation= */ 0f,
-                        /* squishinessFraction= */ 1f,
+                        /* expansion= */ anyFloat(),
+                        /* panelExpansionFraction= */ anyFloat(),
+                        /* proposedTranslation= */ anyFloat(),
+                        /* squishinessFraction= */ anyFloat(),
                     )
                 verify(this).setListening(true)
                 verify(this).setExpanded(true)
             }
+
+            underTest.applyLatestExpansionAndSquishiness()
+            verify(qsImpl!!)
+                .setQsExpansion(
+                    /* expansion= */ 1f,
+                    /* panelExpansionFraction= */ 1f,
+                    /* proposedTranslation= */ 0f,
+                    /* squishinessFraction= */ 1f,
+                )
         }
 
     @Test
@@ -235,16 +264,25 @@
             underTest.setState(QSSceneAdapter.State.Expanding(progress))
             with(qsImpl!!) {
                 verify(this).setQsVisible(true)
-                verify(this)
+                verify(this, never())
                     .setQsExpansion(
-                        /* expansion= */ progress,
-                        /* panelExpansionFraction= */ 1f,
-                        /* proposedTranslation= */ 0f,
-                        /* squishinessFraction= */ 1f,
+                        /* expansion= */ anyFloat(),
+                        /* panelExpansionFraction= */ anyFloat(),
+                        /* proposedTranslation= */ anyFloat(),
+                        /* squishinessFraction= */ anyFloat(),
                     )
                 verify(this).setListening(true)
                 verify(this).setExpanded(true)
             }
+
+            underTest.applyLatestExpansionAndSquishiness()
+            verify(qsImpl!!)
+                .setQsExpansion(
+                    /* expansion= */ progress,
+                    /* panelExpansionFraction= */ 1f,
+                    /* proposedTranslation= */ 0f,
+                    /* squishinessFraction= */ 1f,
+                )
         }
 
     @Test
@@ -260,16 +298,25 @@
             underTest.setState(QSSceneAdapter.State.UnsquishingQQS { squishiness })
             with(qsImpl!!) {
                 verify(this).setQsVisible(true)
-                verify(this)
+                verify(this, never())
                     .setQsExpansion(
-                        /* expansion= */ 0f,
-                        /* panelExpansionFraction= */ 1f,
-                        /* proposedTranslation= */ 0f,
-                        /* squishinessFraction= */ squishiness,
+                        /* expansion= */ anyFloat(),
+                        /* panelExpansionFraction= */ anyFloat(),
+                        /* proposedTranslation= */ anyFloat(),
+                        /* squishinessFraction= */ anyFloat(),
                     )
                 verify(this).setListening(true)
                 verify(this).setExpanded(false)
             }
+
+            underTest.applyLatestExpansionAndSquishiness()
+            verify(qsImpl!!)
+                .setQsExpansion(
+                    /* expansion= */ 0f,
+                    /* panelExpansionFraction= */ 1f,
+                    /* proposedTranslation= */ 0f,
+                    /* squishinessFraction= */ squishiness,
+                )
         }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt
index 8cb811d..a67a8ab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt
@@ -78,7 +78,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DISMISS)
+    @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT)
     fun mediaDataRemoved_userInitiated_dismissNotif() {
         val notifEntryCaptor = argumentCaptor<NotificationEntry>()
         val notifEntry = mock<NotificationEntry>()
@@ -93,7 +93,7 @@
     }
 
     @Test
-    @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DISMISS)
+    @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT)
     fun mediaDataRemoved_notUserInitiated_doesNotDismissNotif() {
         listenerCaptor.lastValue.onMediaDataRemoved(KEY, false)
 
@@ -101,7 +101,7 @@
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DISMISS)
+    @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_USER_INITIATED_DELETEINTENT)
     fun mediaDataRemoved_notUserInitiated_flagOff_dismissNotif() {
         val notifEntryCaptor = argumentCaptor<NotificationEntry>()
         val notifEntry = mock<NotificationEntry>()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
index 288c083..db5921d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -118,8 +118,6 @@
         underTest.setGroupExpanded(summary1, false)
 
         // Expanding again should throw.
-        // TODO(b/320238410): Remove this check when robolectric supports wtf assertions.
-        Assume.assumeFalse(Build.FINGERPRINT.contains("robolectric"))
         assertLogsWtf { underTest.setGroupExpanded(summary1, true) }
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
index 1cdf8dc..79b5cc3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIDialogTest.java
@@ -21,8 +21,10 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.BroadcastReceiver;
 import android.content.Intent;
@@ -41,6 +43,7 @@
 import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogTransitionAnimator;
+import com.android.systemui.animation.back.BackAnimationSpec;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.model.SysUiState;
 
@@ -78,6 +81,8 @@
         MockitoAnnotations.initMocks(this);
 
         mDependency.injectTestDependency(BroadcastDispatcher.class, mBroadcastDispatcher);
+        when(mDelegate.getBackAnimationSpec(ArgumentMatchers.any()))
+                .thenReturn(mock(BackAnimationSpec.class));
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
index fdea5a4..254a967 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
@@ -24,13 +24,13 @@
 import com.android.internal.logging.uiEventLogger
 import com.android.internal.logging.uiEventLoggerFake
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.testKosmos
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
+import com.android.systemui.volume.panel.data.repository.volumePanelGlobalStateRepository
 import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
 import com.android.systemui.volume.panel.ui.viewmodel.volumePanelViewModel
 import com.google.common.truth.Truth.assertThat
@@ -56,7 +56,10 @@
 
     @Captor private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback>
 
-    private val kosmos = testKosmos()
+    private val kosmos =
+        testKosmos().apply {
+            volumePanelGlobalStateRepository.updateVolumePanelState { it.copy(isVisible = true) }
+        }
 
     private lateinit var underTest: BottomBarViewModel
 
@@ -75,8 +78,7 @@
                 underTest.onDoneClicked()
                 runCurrent()
 
-                val volumePanelState by collectLastValue(volumePanelViewModel.volumePanelState)
-                assertThat(volumePanelState!!.isVisible).isFalse()
+                assertThat(volumePanelGlobalStateRepository.globalState.value.isVisible).isFalse()
             }
         }
     }
@@ -106,8 +108,7 @@
                     .isEqualTo(VolumePanelUiEvent.VOLUME_PANEL_SOUND_SETTINGS_CLICKED.id)
 
                 activityStartedCaptor.value.onActivityStarted(ActivityManager.START_SUCCESS)
-                val volumePanelState by collectLastValue(volumePanelViewModel.volumePanelState)
-                assertThat(volumePanelState!!.isVisible).isFalse()
+                assertThat(volumePanelGlobalStateRepository.globalState.value.isVisible).isFalse()
             }
         }
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/VolumePanelGlobalStateInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/VolumePanelGlobalStateInteractorTest.kt
new file mode 100644
index 0000000..d44724f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/VolumePanelGlobalStateInteractorTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class VolumePanelGlobalStateInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+
+    private val underTest = kosmos.volumePanelGlobalStateInteractor
+
+    @Test
+    fun changeVisibility_changesVisibility() =
+        with(kosmos) {
+            testScope.runTest {
+                underTest.setVisible(false)
+                assertThat(underTest.globalState.value.isVisible).isFalse()
+
+                underTest.setVisible(true)
+                assertThat(underTest.globalState.value.isVisible).isTrue()
+            }
+        }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt
index b37184d..d712afe 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManagerTest.kt
@@ -51,7 +51,7 @@
         val component5 = ComponentState(COMPONENT_5, kosmos.mockVolumePanelUiComponent, false)
         val layout =
             underTest.layout(
-                VolumePanelState(0, false, false),
+                VolumePanelState(orientation = 0, isLargeScreen = false),
                 setOf(
                     bottomBarComponentState,
                     component1,
@@ -79,7 +79,7 @@
         val component1State = ComponentState(COMPONENT_1, kosmos.mockVolumePanelUiComponent, false)
         val component2State = ComponentState(COMPONENT_2, kosmos.mockVolumePanelUiComponent, false)
         underTest.layout(
-            VolumePanelState(0, false, false),
+            VolumePanelState(orientation = 0, isLargeScreen = false),
             setOf(
                 component1State,
                 component2State,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt
index f6ada4c16..420b955 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.statusbar.policy.fakeConfigurationController
 import com.android.systemui.testKosmos
+import com.android.systemui.volume.panel.data.repository.volumePanelGlobalStateRepository
 import com.android.systemui.volume.panel.domain.interactor.criteriaByKey
 import com.android.systemui.volume.panel.domain.unavailableCriteria
 import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
@@ -49,7 +50,10 @@
 class VolumePanelViewModelTest : SysuiTestCase() {
 
     private val kosmos =
-        testKosmos().apply { componentsLayoutManager = DefaultComponentsLayoutManager(BOTTOM_BAR) }
+        testKosmos().apply {
+            componentsLayoutManager = DefaultComponentsLayoutManager(BOTTOM_BAR)
+            volumePanelGlobalStateRepository.updateVolumePanelState { it.copy(isVisible = true) }
+        }
 
     private val testableResources = context.orCreateTestableResources
 
@@ -58,12 +62,10 @@
     @Test
     fun dismissingPanel_changesVisibility() = test {
         testScope.runTest {
-            assertThat(underTest.volumePanelState.value.isVisible).isTrue()
-
             underTest.dismissPanel()
             runCurrent()
 
-            assertThat(underTest.volumePanelState.value.isVisible).isFalse()
+            assertThat(volumePanelGlobalStateRepository.globalState.value.isVisible).isFalse()
         }
     }
 
@@ -114,11 +116,10 @@
     @Test
     fun dismissPanel_dismissesPanel() = test {
         testScope.runTest {
-            val volumePanelState by collectLastValue(underTest.volumePanelState)
             underTest.dismissPanel()
             runCurrent()
 
-            assertThat(volumePanelState!!.isVisible).isFalse()
+            assertThat(volumePanelGlobalStateRepository.globalState.value.isVisible).isFalse()
         }
     }
 
@@ -126,14 +127,13 @@
     fun dismissBroadcast_dismissesPanel() = test {
         testScope.runTest {
             runCurrent() // run the flows to let allow the receiver to be registered
-            val volumePanelState by collectLastValue(underTest.volumePanelState)
             broadcastDispatcher.sendIntentToMatchingReceiversOnly(
                 applicationContext,
                 Intent(DISMISS_ACTION),
             )
             runCurrent()
 
-            assertThat(volumePanelState!!.isVisible).isFalse()
+            assertThat(volumePanelGlobalStateRepository.globalState.value.isVisible).isFalse()
         }
     }
 
diff --git a/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_background.xml b/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_background.xml
index 6e6e032..c83b6d3 100644
--- a/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_background.xml
+++ b/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_background.xml
@@ -30,8 +30,8 @@
         android:end="20dp"
         android:gravity="end|center_vertical">
         <vector
-            android:width="@dimen/screenrecord_spinner_arrow_size"
-            android:height="@dimen/screenrecord_spinner_arrow_size"
+            android:width="@dimen/hearing_devices_preset_spinner_arrow_size"
+            android:height="@dimen/hearing_devices_preset_spinner_arrow_size"
             android:viewportWidth="24"
             android:viewportHeight="24"
             android:tint="?androidprv:attr/colorControlNormal">
diff --git a/packages/SystemUI/res/layout/hearing_devices_preset_dropdown_item.xml b/packages/SystemUI/res/layout/hearing_devices_preset_dropdown_item.xml
new file mode 100644
index 0000000..1d9307b
--- /dev/null
+++ b/packages/SystemUI/res/layout/hearing_devices_preset_dropdown_item.xml
@@ -0,0 +1,25 @@
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/hearing_devices_preset_option_text"
+    style="?android:attr/spinnerDropDownItemStyle"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="@dimen/hearing_devices_preset_spinner_height"
+    android:paddingStart="@dimen/hearing_devices_preset_spinner_text_padding_start"
+    android:gravity="center_vertical"
+    android:ellipsize="end" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/hearing_devices_preset_spinner_selected.xml b/packages/SystemUI/res/layout/hearing_devices_preset_spinner_selected.xml
new file mode 100644
index 0000000..77172ca
--- /dev/null
+++ b/packages/SystemUI/res/layout/hearing_devices_preset_spinner_selected.xml
@@ -0,0 +1,46 @@
+<!--
+    Copyright (C) 2024 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="@dimen/hearing_devices_preset_spinner_height"
+    android:paddingStart="@dimen/hearing_devices_preset_spinner_text_padding_start"
+    android:paddingTop="@dimen/hearing_devices_preset_spinner_text_padding_vertical"
+    android:paddingBottom="@dimen/hearing_devices_preset_spinner_text_padding_vertical"
+    android:orientation="vertical">
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:textAppearance="@style/TextAppearance.Dialog.Title"
+        android:lineSpacingExtra="6dp"
+        android:text="@string/hearing_devices_preset_label"
+        android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+        android:textSize="14sp"
+        android:gravity="center_vertical"
+        android:layout_weight="1" />
+    <TextView
+        android:id="@+id/hearing_devices_preset_option_text"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:textAppearance="@style/TextAppearance.Dialog.Body"
+        android:lineSpacingExtra="6dp"
+        android:gravity="center_vertical"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:layout_weight="1" />
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
index 8e1d0a5..2bf6f80 100644
--- a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
@@ -17,7 +17,6 @@
 <androidx.constraintlayout.widget.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/root"
     style="@style/Widget.SliceView.Panel"
     android:layout_width="wrap_content"
@@ -36,16 +35,22 @@
         android:id="@+id/preset_spinner"
         style="@style/BluetoothTileDialog.Device"
         android:layout_width="match_parent"
-        android:layout_height="@dimen/hearing_devices_preset_spinner_height"
-        android:layout_marginTop="@dimen/hearing_devices_preset_spinner_margin"
-        android:layout_marginBottom="@dimen/hearing_devices_preset_spinner_margin"
+        android:layout_height="wrap_content"
+        android:minHeight="@dimen/hearing_devices_preset_spinner_height"
+        android:layout_marginTop="@dimen/hearing_devices_layout_margin"
+        android:layout_marginBottom="@dimen/hearing_devices_layout_margin"
         android:gravity="center_vertical"
         android:background="@drawable/hearing_devices_preset_spinner_background"
         android:popupBackground="@drawable/hearing_devices_preset_spinner_popup_background"
+        android:dropDownVerticalOffset="@dimen/hearing_devices_preset_spinner_height"
+        android:dropDownWidth="match_parent"
+        android:paddingStart="0dp"
+        android:paddingEnd="0dp"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintTop_toBottomOf="@id/device_list"
         app:layout_constraintBottom_toTopOf="@id/pair_new_device_button"
+        android:longClickable="false"
         android:visibility="gone"/>
 
     <androidx.constraintlayout.widget.Barrier
diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
index 704cf0b..12e226a 100644
--- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml
+++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
@@ -125,6 +125,7 @@
             android:ellipsize="marquee"
             android:text="@string/accessibility_allow_diagonal_scrolling"
             android:textAppearance="@style/TextAppearance.MagnificationSetting.Title"
+            android:labelFor="@id/magnifier_horizontal_lock_switch"
             android:layout_gravity="center_vertical" />
 
         <Switch
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 02b74ce..7d7a5d4 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1772,10 +1772,10 @@
     <dimen name="bluetooth_dialog_scroll_view_min_height_with_auto_on">350dp</dimen>
 
     <!-- Hearing devices dialog related dimensions -->
+    <dimen name="hearing_devices_layout_margin">12dp</dimen>
     <dimen name="hearing_devices_preset_spinner_height">72dp</dimen>
-    <dimen name="hearing_devices_preset_spinner_margin">24dp</dimen>
     <dimen name="hearing_devices_preset_spinner_text_padding_start">20dp</dimen>
-    <dimen name="hearing_devices_preset_spinner_text_padding_end">80dp</dimen>
+    <dimen name="hearing_devices_preset_spinner_text_padding_vertical">15dp</dimen>
     <dimen name="hearing_devices_preset_spinner_arrow_size">24dp</dimen>
     <dimen name="hearing_devices_preset_spinner_background_radius">28dp</dimen>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index abfdc2a..6f2806d 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -913,8 +913,10 @@
     <string name="quick_settings_pair_hearing_devices">Pair new device</string>
     <!-- QuickSettings: Content description of the hearing devices dialog pair new device [CHAR LIMIT=NONE] -->
     <string name="accessibility_hearing_device_pair_new_device">Click to pair new device</string>
-    <!-- Message when selecting hearing aids presets failed. [CHAR LIMIT=NONE] -->
+    <!-- QuickSettings: Message when selecting hearing aids presets failed. [CHAR LIMIT=NONE] -->
     <string name="hearing_devices_presets_error">Couldn\'t update preset</string>
+    <!-- QuickSettings: Title for hearing aids presets. Preset is a set of hearing aid settings. User can apply different settings in different environments (e.g. Outdoor, Restaurant, Home) [CHAR LIMIT=40]-->
+    <string name="hearing_devices_preset_label">Preset</string>
 
     <!--- Title of dialog triggered if the microphone is disabled but an app tried to access it. [CHAR LIMIT=150] -->
     <string name="sensor_privacy_start_use_mic_dialog_title">Unblock device microphone?</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
index 3f34df7..3bf3fb3 100644
--- a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java
@@ -300,6 +300,7 @@
                 });
                 mTelephonyListenerManager.addActiveDataSubscriptionIdListener(mPhoneStateListener);
                 cancelSatelliteCollectionJob(/* reason= */ "Starting new job");
+                mLogger.logStartListeningForSatelliteCarrierText();
                 mSatelliteConnectionJob =
                     mJavaAdapter.alwaysCollectFlow(
                         mDeviceBasedSatelliteViewModel.getCarrierText(),
@@ -316,7 +317,7 @@
                 mWakefulnessLifecycle.removeObserver(mWakefulnessObserver);
             });
             mTelephonyListenerManager.removeActiveDataSubscriptionIdListener(mPhoneStateListener);
-            cancelSatelliteCollectionJob(/* reason= */ "Stopping listening");
+            cancelSatelliteCollectionJob(/* reason= */ "#handleSetListening has null callback");
         }
     }
 
@@ -336,6 +337,7 @@
 
     private void onSatelliteCarrierTextChanged(@Nullable String text) {
         mLogger.logUpdateCarrierTextForReason(REASON_SATELLITE_CHANGED);
+        mLogger.logNewSatelliteCarrierText(text);
         mSatelliteCarrierText = text;
         updateCarrierText();
     }
@@ -654,6 +656,7 @@
     private void cancelSatelliteCollectionJob(String reason) {
         Job job = mSatelliteConnectionJob;
         if (job != null) {
+            mLogger.logStopListeningForSatelliteCarrierText(reason);
             job.cancel(new CancellationException(reason));
         }
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
index 48fea55..7d0c491 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt
@@ -38,8 +38,11 @@
         buffer.log(
             TAG,
             LogLevel.VERBOSE,
-            { int1 = numSubs },
-            { "updateCarrierText: location=${location ?: "(unknown)"} numSubs=$int1" },
+            {
+                int1 = numSubs
+                str1 = location
+            },
+            { "updateCarrierText: location=${str1 ?: "(unknown)"} numSubs=$int1" },
         )
     }
 
@@ -77,6 +80,15 @@
         )
     }
 
+    fun logNewSatelliteCarrierText(newSatelliteText: String?) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            { str1 = newSatelliteText },
+            { "New satellite text = $str1" },
+        )
+    }
+
     fun logUsingSatelliteText(satelliteText: String) {
         buffer.log(
             TAG,
@@ -125,10 +137,37 @@
         buffer.log(
             TAG,
             LogLevel.DEBUG,
-            { int1 = reason },
+            {
+                int1 = reason
+                str1 = location
+            },
             {
                 "refreshing carrier info for reason: ${reason.reasonMessage()}" +
-                    " location=${location ?: "(unknown)"}"
+                    " location=${str1 ?: "(unknown)"}"
+            }
+        )
+    }
+
+    fun logStartListeningForSatelliteCarrierText() {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            { str1 = location },
+            { "Start listening for satellite carrier text. Location=${str1 ?: "(unknown)"}" }
+        )
+    }
+
+    fun logStopListeningForSatelliteCarrierText(reason: String) {
+        buffer.log(
+            TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = location
+                str2 = reason
+            },
+            {
+                "Stop listening for satellite carrier text. " +
+                    "Location=${str1 ?: "(unknown)"} Reason=$str2"
             }
         )
     }
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index ce4032a..bebfd85 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -16,7 +16,6 @@
 
 package com.android.keyguard.logging
 
-import android.hardware.biometrics.BiometricSourceType
 import com.android.systemui.biometrics.AuthRippleController
 import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController
 import com.android.systemui.log.LogBuffer
@@ -81,6 +80,23 @@
         )
     }
 
+    fun delayShowingTrustAgentError(
+        msg: CharSequence,
+        fpEngaged: Boolean,
+        faceRunning: Boolean,
+    ) {
+        buffer.log(
+            BIO_TAG,
+            LogLevel.DEBUG,
+            {
+                str1 = msg.toString()
+                bool1 = fpEngaged
+                bool2 = faceRunning
+            },
+            { "Delay showing trustAgentError:$str1. fpEngaged:$bool1 faceRunning:$bool2 " }
+        )
+    }
+
     fun logUpdateDeviceEntryIndication(
         animate: Boolean,
         visible: Boolean,
@@ -118,10 +134,9 @@
         )
     }
 
-    fun logDropNonFingerprintMessage(
+    fun logDropFaceMessage(
         message: CharSequence,
         followUpMessage: CharSequence?,
-        biometricSourceType: BiometricSourceType?,
     ) {
         buffer.log(
             KeyguardIndicationController.TAG,
@@ -129,12 +144,8 @@
             {
                 str1 = message.toString()
                 str2 = followUpMessage?.toString()
-                str3 = biometricSourceType?.name
             },
-            {
-                "droppingNonFingerprintMessage message=$str1 " +
-                    "followUpMessage:$str2 biometricSourceType:$str3"
-            }
+            { "droppingFaceMessage message=$str1 followUpMessage:$str2" }
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 7b5a09c..28dd233 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -32,6 +32,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.Visibility;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.AdapterView;
 import android.widget.ArrayAdapter;
 import android.widget.Button;
@@ -208,6 +209,10 @@
         }
         mMainHandler.post(() -> {
             mDeviceListAdapter.refreshDeviceItemList(mHearingDeviceItemList);
+            final List<BluetoothHapPresetInfo> presetInfos =
+                    mPresetsController.getAllPresetInfo();
+            final int activePresetIndex = mPresetsController.getActivePresetIndex();
+            refreshPresetInfoAdapter(presetInfos, activePresetIndex);
             mPresetSpinner.setVisibility(
                     (activeHearingDevice != null && !mPresetInfoAdapter.isEmpty()) ? VISIBLE
                             : GONE);
@@ -295,10 +300,23 @@
                 mHearingDeviceItemList);
         mPresetsController.setActiveHearingDevice(activeHearingDevice);
 
-        mPresetInfoAdapter = new ArrayAdapter<>(dialog.getContext(),
-                android.R.layout.simple_spinner_dropdown_item);
-        mPresetInfoAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        mPresetInfoAdapter = new ArrayAdapter<String>(dialog.getContext(),
+                R.layout.hearing_devices_preset_spinner_selected,
+                R.id.hearing_devices_preset_option_text);
+        mPresetInfoAdapter.setDropDownViewResource(
+                R.layout.hearing_devices_preset_dropdown_item);
         mPresetSpinner.setAdapter(mPresetInfoAdapter);
+
+        // disable redundant Touch & Hold accessibility action for Switch Access
+        mPresetSpinner.setAccessibilityDelegate(new View.AccessibilityDelegate() {
+            @Override
+            public void onInitializeAccessibilityNodeInfo(@NonNull View host,
+                    @NonNull AccessibilityNodeInfo info) {
+                info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK);
+                super.onInitializeAccessibilityNodeInfo(host, info);
+            }
+        });
+
         mPresetSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
             @Override
             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt
index 2e29c3b..7503a8b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/UdfpsTouchOverlayBinder.kt
@@ -46,15 +46,22 @@
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.CREATED) {
                 launch {
-                    viewModel.shouldHandleTouches.collect { shouldHandleTouches ->
+                        viewModel.shouldHandleTouches.collect { shouldHandleTouches ->
+                            Log.d(
+                                "UdfpsTouchOverlayBinder",
+                                "[$view]: update shouldHandleTouches=$shouldHandleTouches"
+                            )
+                            view.isInvisible = !shouldHandleTouches
+                            udfpsOverlayInteractor.setHandleTouches(shouldHandleTouches)
+                        }
+                    }
+                    .invokeOnCompletion {
                         Log.d(
                             "UdfpsTouchOverlayBinder",
-                            "[$view]: update shouldHandleTouches=$shouldHandleTouches"
+                            "[$view-detached]: update shouldHandleTouches=false"
                         )
-                        view.isInvisible = !shouldHandleTouches
-                        udfpsOverlayInteractor.setHandleTouches(shouldHandleTouches)
+                        udfpsOverlayInteractor.setHandleTouches(false)
                     }
-                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
index 49d0847..51b2280 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
@@ -155,7 +155,7 @@
     }
 }
 
-internal class AvailableHearingDeviceItemFactory : ActiveMediaDeviceItemFactory() {
+internal class AvailableHearingDeviceItemFactory : AvailableMediaDeviceItemFactory() {
     override fun isFilterMatched(
         context: Context,
         cachedDevice: CachedBluetoothDevice,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
index e0334a0..1d11dfb 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt
@@ -28,6 +28,9 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
 import com.android.systemui.util.time.SystemClock
@@ -60,6 +63,7 @@
     private val deviceEntryFingerprintAuthInteractor: Lazy<DeviceEntryFingerprintAuthInteractor>,
     private val keyguardInteractor: Lazy<KeyguardInteractor>,
     keyguardTransitionInteractor: Lazy<KeyguardTransitionInteractor>,
+    sceneInteractor: Lazy<SceneInteractor>,
     @Application scope: CoroutineScope,
 ) {
     var receivedDownTouch = false
@@ -96,30 +100,42 @@
         alternateBouncerSupported
             .flatMapLatest { alternateBouncerSupported ->
                 if (alternateBouncerSupported) {
-                    keyguardTransitionInteractor.get().currentKeyguardState.flatMapLatest {
-                        currentKeyguardState ->
-                        if (currentKeyguardState == KeyguardState.GONE) {
-                            flowOf(false)
-                        } else {
-                            combine(
-                                deviceEntryFingerprintAuthInteractor
-                                    .get()
-                                    .isFingerprintAuthCurrentlyAllowed,
-                                keyguardInteractor.get().isKeyguardDismissible,
-                                bouncerRepository.primaryBouncerShow,
-                                isDozingOrAod
+                    combine(
+                            keyguardTransitionInteractor.get().currentKeyguardState,
+                            if (SceneContainerFlag.isEnabled) {
+                                sceneInteractor.get().currentScene
+                            } else {
+                                flowOf(Scenes.Lockscreen)
+                            },
+                            ::Pair
+                        )
+                        .flatMapLatest { (currentKeyguardState, transitionState) ->
+                            if (currentKeyguardState == KeyguardState.GONE) {
+                                flowOf(false)
+                            } else if (
+                                SceneContainerFlag.isEnabled && transitionState == Scenes.Gone
                             ) {
-                                fingerprintAllowed,
-                                keyguardDismissible,
-                                primaryBouncerShowing,
-                                dozing ->
-                                fingerprintAllowed &&
-                                    !keyguardDismissible &&
-                                    !primaryBouncerShowing &&
-                                    !dozing
+                                flowOf(false)
+                            } else {
+                                combine(
+                                    deviceEntryFingerprintAuthInteractor
+                                        .get()
+                                        .isFingerprintAuthCurrentlyAllowed,
+                                    keyguardInteractor.get().isKeyguardDismissible,
+                                    bouncerRepository.primaryBouncerShow,
+                                    isDozingOrAod
+                                ) {
+                                    fingerprintAllowed,
+                                    keyguardDismissible,
+                                    primaryBouncerShowing,
+                                    dozing ->
+                                    fingerprintAllowed &&
+                                        !keyguardDismissible &&
+                                        !primaryBouncerShowing &&
+                                        !dozing
+                                }
                             }
                         }
-                    }
                 } else {
                     flowOf(false)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
index 1de3459..7f137f3 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepositoryModule.kt
@@ -21,5 +21,5 @@
 
 @Module
 interface CommunalRepositoryModule {
-    @Binds fun communalRepository(impl: CommunalRepositoryImpl): CommunalRepository
+    @Binds fun communalRepository(impl: CommunalSceneRepositoryImpl): CommunalSceneRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
rename to packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
index 8bfd8d9..d6d08b4 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
@@ -36,7 +36,7 @@
 import kotlinx.coroutines.flow.stateIn
 
 /** Encapsulates the state of communal mode. */
-interface CommunalRepository {
+interface CommunalSceneRepository {
     /**
      * Target scene as requested by the underlying [SceneTransitionLayout] or through [changeScene].
      */
@@ -48,6 +48,9 @@
     /** Updates the requested scene. */
     fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null)
 
+    /** Immediately snaps to the desired scene. */
+    fun snapToScene(toScene: SceneKey)
+
     /**
      * Updates the transition state of the hub [SceneTransitionLayout].
      *
@@ -58,12 +61,12 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
-class CommunalRepositoryImpl
+class CommunalSceneRepositoryImpl
 @Inject
 constructor(
     @Background backgroundScope: CoroutineScope,
     @Communal private val sceneDataSource: SceneDataSource,
-) : CommunalRepository {
+) : CommunalSceneRepository {
 
     override val currentScene: StateFlow<SceneKey> = sceneDataSource.currentScene
 
@@ -82,6 +85,10 @@
         sceneDataSource.changeScene(toScene, transitionKey)
     }
 
+    override fun snapToScene(toScene: SceneKey) {
+        sceneDataSource.snapToScene(toScene)
+    }
+
     /**
      * Updates the transition state of the hub [SceneTransitionLayout].
      *
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 9599a88..2be28ca 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -29,7 +29,6 @@
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.communal.data.repository.CommunalMediaRepository
 import com.android.systemui.communal.data.repository.CommunalPrefsRepository
-import com.android.systemui.communal.data.repository.CommunalRepository
 import com.android.systemui.communal.data.repository.CommunalWidgetRepository
 import com.android.systemui.communal.domain.model.CommunalContentModel
 import com.android.systemui.communal.domain.model.CommunalContentModel.WidgetContent
@@ -97,7 +96,6 @@
     @Application val applicationScope: CoroutineScope,
     @Background val bgDispatcher: CoroutineDispatcher,
     broadcastDispatcher: BroadcastDispatcher,
-    private val communalRepository: CommunalRepository,
     private val widgetRepository: CommunalWidgetRepository,
     private val communalPrefsRepository: CommunalPrefsRepository,
     mediaRepository: CommunalMediaRepository,
@@ -110,6 +108,7 @@
     private val userTracker: UserTracker,
     private val activityStarter: ActivityStarter,
     private val userManager: UserManager,
+    private val communalSceneInteractor: CommunalSceneInteractor,
     sceneInteractor: SceneInteractor,
     @CommunalLog logBuffer: LogBuffer,
     @CommunalTableLog tableLogBuffer: TableLogBuffer,
@@ -174,15 +173,19 @@
      *
      * If [isCommunalAvailable] is false, will return [CommunalScenes.Blank]
      */
-    val desiredScene: Flow<SceneKey> =
-        communalRepository.currentScene.combine(isCommunalAvailable) { scene, available ->
-            if (available) scene else CommunalScenes.Blank
-        }
+    @Deprecated(
+        "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
+    )
+    val desiredScene: Flow<SceneKey> = communalSceneInteractor.currentScene
 
     /** Transition state of the hub mode. */
-    val transitionState: StateFlow<ObservableTransitionState> = communalRepository.transitionState
+    @Deprecated(
+        "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
+    )
+    val transitionState: StateFlow<ObservableTransitionState> =
+        communalSceneInteractor.transitionState
 
-    val _userActivity: MutableSharedFlow<Unit> =
+    private val _userActivity: MutableSharedFlow<Unit> =
         MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
     val userActivity: Flow<Unit> = _userActivity.asSharedFlow()
 
@@ -212,32 +215,18 @@
      *
      * Note that you must call is with `null` when the UI is done or risk a memory leak.
      */
-    fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
-        communalRepository.setTransitionState(transitionState)
-    }
+    @Deprecated(
+        "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
+    )
+    fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) =
+        communalSceneInteractor.setTransitionState(transitionState)
 
     /** Returns a flow that tracks the progress of transitions to the given scene from 0-1. */
+    @Deprecated(
+        "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
+    )
     fun transitionProgressToScene(targetScene: SceneKey) =
-        transitionState
-            .flatMapLatest { state ->
-                when (state) {
-                    is ObservableTransitionState.Idle ->
-                        flowOf(CommunalTransitionProgress.Idle(state.currentScene))
-                    is ObservableTransitionState.Transition ->
-                        if (state.toScene == targetScene) {
-                            state.progress.map {
-                                CommunalTransitionProgress.Transition(
-                                    // Clamp the progress values between 0 and 1 as actual progress
-                                    // values can be higher than 0 or lower than 1 due to a fling.
-                                    progress = it.coerceIn(0.0f, 1.0f)
-                                )
-                            }
-                        } else {
-                            flowOf(CommunalTransitionProgress.OtherTransition)
-                        }
-                }
-            }
-            .distinctUntilChanged()
+        communalSceneInteractor.transitionProgressToScene(targetScene)
 
     /**
      * Flow that emits a boolean if the communal UI is the target scene, ie. the [desiredScene] is
@@ -283,34 +272,30 @@
      * This will not be true while transitioning to the hub and will turn false immediately when a
      * swipe to exit the hub starts.
      */
-    val isIdleOnCommunal: StateFlow<Boolean> =
-        communalRepository.transitionState
-            .map {
-                it is ObservableTransitionState.Idle && it.currentScene == CommunalScenes.Communal
-            }
-            .stateIn(
-                scope = applicationScope,
-                started = SharingStarted.Eagerly,
-                initialValue = false,
-            )
+    @Deprecated(
+        "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
+    )
+    val isIdleOnCommunal: StateFlow<Boolean> = communalSceneInteractor.isIdleOnCommunal
 
     /**
      * Flow that emits a boolean if any portion of the communal UI is visible at all.
      *
      * This flow will be true during any transition and when idle on the communal scene.
      */
-    val isCommunalVisible: Flow<Boolean> =
-        communalRepository.transitionState.map {
-            !(it is ObservableTransitionState.Idle && it.currentScene == CommunalScenes.Blank)
-        }
+    @Deprecated(
+        "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
+    )
+    val isCommunalVisible: Flow<Boolean> = communalSceneInteractor.isCommunalVisible
 
     /**
      * Asks for an asynchronous scene witch to [newScene], which will use the corresponding
      * installed transition or the one specified by [transitionKey], if provided.
      */
-    fun changeScene(newScene: SceneKey, transitionKey: TransitionKey? = null) {
-        communalRepository.changeScene(newScene, transitionKey)
-    }
+    @Deprecated(
+        "Use com.android.systemui.communal.domain.interactor.CommunalSceneInteractor instead"
+    )
+    fun changeScene(newScene: SceneKey, transitionKey: TransitionKey? = null) =
+        communalSceneInteractor.changeScene(newScene, transitionKey)
 
     fun setEditModeOpen(isOpen: Boolean) {
         _editModeOpen.value = isOpen
@@ -579,17 +564,3 @@
         }
     }
 }
-
-/** Simplified transition progress data class for tracking a single transition between scenes. */
-sealed class CommunalTransitionProgress {
-    /** No transition/animation is currently running. */
-    data class Idle(val scene: SceneKey) : CommunalTransitionProgress()
-
-    /** There is a transition animating to the expected scene. */
-    data class Transition(
-        val progress: Float,
-    ) : CommunalTransitionProgress()
-
-    /** There is a transition animating to a scene other than the expected scene. */
-    data object OtherTransition : CommunalTransitionProgress()
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
new file mode 100644
index 0000000..5cfe979
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.domain.interactor
+
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionKey
+import com.android.systemui.communal.data.repository.CommunalSceneRepository
+import com.android.systemui.communal.domain.model.CommunalTransitionProgressModel
+import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class CommunalSceneInteractor
+@Inject
+constructor(
+    @Application private val applicationScope: CoroutineScope,
+    private val communalSceneRepository: CommunalSceneRepository,
+) {
+    /**
+     * Asks for an asynchronous scene witch to [newScene], which will use the corresponding
+     * installed transition or the one specified by [transitionKey], if provided.
+     */
+    fun changeScene(newScene: SceneKey, transitionKey: TransitionKey? = null) {
+        communalSceneRepository.changeScene(newScene, transitionKey)
+    }
+
+    /** Immediately snaps to the new scene. */
+    fun snapToScene(newScene: SceneKey) {
+        communalSceneRepository.snapToScene(newScene)
+    }
+
+    /**
+     * Target scene as requested by the underlying [SceneTransitionLayout] or through [changeScene].
+     */
+    val currentScene: Flow<SceneKey> = communalSceneRepository.currentScene
+
+    /** Transition state of the hub mode. */
+    val transitionState: StateFlow<ObservableTransitionState> =
+        communalSceneRepository.transitionState
+
+    /**
+     * Updates the transition state of the hub [SceneTransitionLayout].
+     *
+     * Note that you must call is with `null` when the UI is done or risk a memory leak.
+     */
+    fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
+        communalSceneRepository.setTransitionState(transitionState)
+    }
+
+    /** Returns a flow that tracks the progress of transitions to the given scene from 0-1. */
+    fun transitionProgressToScene(targetScene: SceneKey) =
+        transitionState
+            .flatMapLatest { state ->
+                when (state) {
+                    is ObservableTransitionState.Idle ->
+                        flowOf(CommunalTransitionProgressModel.Idle(state.currentScene))
+                    is ObservableTransitionState.Transition ->
+                        if (state.toScene == targetScene) {
+                            state.progress.map {
+                                CommunalTransitionProgressModel.Transition(
+                                    // Clamp the progress values between 0 and 1 as actual progress
+                                    // values can be higher than 0 or lower than 1 due to a fling.
+                                    progress = it.coerceIn(0.0f, 1.0f)
+                                )
+                            }
+                        } else {
+                            flowOf(CommunalTransitionProgressModel.OtherTransition)
+                        }
+                }
+            }
+            .distinctUntilChanged()
+
+    /**
+     * Flow that emits a boolean if the communal UI is fully visible and not in transition.
+     *
+     * This will not be true while transitioning to the hub and will turn false immediately when a
+     * swipe to exit the hub starts.
+     */
+    val isIdleOnCommunal: StateFlow<Boolean> =
+        transitionState
+            .map {
+                it is ObservableTransitionState.Idle && it.currentScene == CommunalScenes.Communal
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = false,
+            )
+
+    /**
+     * Flow that emits a boolean if any portion of the communal UI is visible at all.
+     *
+     * This flow will be true during any transition and when idle on the communal scene.
+     */
+    val isCommunalVisible: Flow<Boolean> =
+        transitionState.map {
+            !(it is ObservableTransitionState.Idle && it.currentScene == CommunalScenes.Blank)
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalTransitionProgressModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalTransitionProgressModel.kt
new file mode 100644
index 0000000..e3187c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalTransitionProgressModel.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.domain.model
+
+import com.android.compose.animation.scene.SceneKey
+
+/** Simplified transition progress data class for tracking a single transition between scenes. */
+sealed interface CommunalTransitionProgressModel {
+    /** No transition/animation is currently running. */
+    data class Idle(val scene: SceneKey) : CommunalTransitionProgressModel
+
+    /** There is a transition animating to the expected scene. */
+    data class Transition(
+        val progress: Float,
+    ) : CommunalTransitionProgressModel
+
+    /** There is a transition animating to a scene other than the expected scene. */
+    data object OtherTransition : CommunalTransitionProgressModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
index a3c61a4..73cfb52 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalTransitionKeys.kt
@@ -26,4 +26,6 @@
 object CommunalTransitionKeys {
     /** Fades the glanceable hub without any translation */
     val SimpleFade = TransitionKey("SimpleFade")
+    /** Immediately transitions without any delay */
+    val Immediately = TransitionKey("Immediately")
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
index b7e8205..058ca4d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
@@ -83,7 +83,9 @@
     override fun allocateAppWidgetId(): Int {
         return super.allocateAppWidgetId().also { appWidgetId ->
             backgroundScope.launch {
-                observers.forEach { observer -> observer.onAllocateAppWidgetId(appWidgetId) }
+                synchronized(observers) {
+                    observers.forEach { observer -> observer.onAllocateAppWidgetId(appWidgetId) }
+                }
             }
         }
     }
@@ -91,18 +93,28 @@
     override fun deleteAppWidgetId(appWidgetId: Int) {
         super.deleteAppWidgetId(appWidgetId)
         backgroundScope.launch {
-            observers.forEach { observer -> observer.onDeleteAppWidgetId(appWidgetId) }
+            synchronized(observers) {
+                observers.forEach { observer -> observer.onDeleteAppWidgetId(appWidgetId) }
+            }
         }
     }
 
     override fun startListening() {
         super.startListening()
-        backgroundScope.launch { observers.forEach { observer -> observer.onHostStartListening() } }
+        backgroundScope.launch {
+            synchronized(observers) {
+                observers.forEach { observer -> observer.onHostStartListening() }
+            }
+        }
     }
 
     override fun stopListening() {
         super.stopListening()
-        backgroundScope.launch { observers.forEach { observer -> observer.onHostStopListening() } }
+        backgroundScope.launch {
+            synchronized(observers) {
+                observers.forEach { observer -> observer.onHostStopListening() }
+            }
+        }
     }
 
     fun addObserver(observer: Observer) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 93f3793..339e8f0 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -75,7 +75,8 @@
 import com.android.systemui.log.dagger.LogModule;
 import com.android.systemui.log.dagger.MonitorLog;
 import com.android.systemui.log.table.TableLogBuffer;
-import com.android.systemui.mediaprojection.appselector.MediaProjectionModule;
+import com.android.systemui.mediaprojection.MediaProjectionModule;
+import com.android.systemui.mediaprojection.appselector.MediaProjectionActivitiesModule;
 import com.android.systemui.mediaprojection.taskswitcher.MediaProjectionTaskSwitcherModule;
 import com.android.systemui.model.SceneContainerPlugin;
 import com.android.systemui.model.SysUiState;
@@ -225,6 +226,7 @@
         KeyguardSectionsModule.class,
         LetterboxModule.class,
         LogModule.class,
+        MediaProjectionActivitiesModule.class,
         MediaProjectionModule.class,
         MediaProjectionTaskSwitcherModule.class,
         MotionToolModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
index ec574d2..a5eafa9 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
@@ -28,6 +28,7 @@
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.flatMapLatest
@@ -43,9 +44,15 @@
     biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor,
     fingerprintPropertyRepository: FingerprintPropertyRepository,
 ) {
-    /** Whether fingerprint authentication is currently running or not */
+    /**
+     * Whether fingerprint authentication is currently running or not. This does not mean the user
+     * [isEngaged] with the fingerprint.
+     */
     val isRunning: Flow<Boolean> = repository.isRunning
 
+    /** Whether the user is actively engaging with the fingerprint sensor */
+    val isEngaged: StateFlow<Boolean> = repository.isEngaged
+
     /** Provide the current status of fingerprint authentication. */
     val authenticationStatus: Flow<FingerprintAuthenticationStatus> =
         repository.authenticationStatus
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 9cdba58..f2a544e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -77,7 +77,9 @@
 import com.android.keyguard.mediator.ScreenOnCoordinator;
 import com.android.systemui.SystemUIApplication;
 import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor;
 import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier;
 import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindViewBinder;
 import com.android.systemui.keyguard.ui.binder.WindowManagerLockscreenVisibilityViewBinder;
@@ -101,6 +103,7 @@
 import java.util.ArrayList;
 import java.util.Map;
 import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
@@ -116,6 +119,7 @@
     private final DisplayTracker mDisplayTracker;
     private final PowerInteractor mPowerInteractor;
     private final Lazy<SceneInteractor> mSceneInteractorLazy;
+    private final Executor mMainExecutor;
 
     private static RemoteAnimationTarget[] wrap(TransitionInfo info, boolean wallpapers,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap,
@@ -308,6 +312,7 @@
     }
 
     private final WindowManagerOcclusionManager mWmOcclusionManager;
+    private final KeyguardEnabledInteractor mKeyguardEnabledInteractor;
 
     private final Lazy<FoldGracePeriodProvider> mFoldGracePeriodProvider = new Lazy<>() {
         @Override
@@ -331,7 +336,9 @@
             FeatureFlags featureFlags,
             PowerInteractor powerInteractor,
             WindowManagerOcclusionManager windowManagerOcclusionManager,
-            Lazy<SceneInteractor> sceneInteractorLazy) {
+            Lazy<SceneInteractor> sceneInteractorLazy,
+            @Main Executor mainExecutor,
+            KeyguardEnabledInteractor keyguardEnabledInteractor) {
         super();
         mKeyguardViewMediator = keyguardViewMediator;
         mKeyguardLifecyclesDispatcher = keyguardLifecyclesDispatcher;
@@ -341,6 +348,7 @@
         mFlags = featureFlags;
         mPowerInteractor = powerInteractor;
         mSceneInteractorLazy = sceneInteractorLazy;
+        mMainExecutor = mainExecutor;
 
         if (KeyguardWmStateRefactor.isEnabled()) {
             WindowManagerLockscreenVisibilityViewBinder.bind(
@@ -355,6 +363,7 @@
         }
 
         mWmOcclusionManager = windowManagerOcclusionManager;
+        mKeyguardEnabledInteractor = keyguardEnabledInteractor;
     }
 
     @Override
@@ -593,6 +602,7 @@
         public void setKeyguardEnabled(boolean enabled) {
             trace("setKeyguardEnabled enabled" + enabled);
             checkPermission();
+            mKeyguardEnabledInteractor.notifyKeyguardEnabled(enabled);
             mKeyguardViewMediator.setKeyguardEnabled(enabled);
         }
 
@@ -619,8 +629,8 @@
             mKeyguardViewMediator.showDismissibleKeyguard();
 
             if (SceneContainerFlag.isEnabled() && mFoldGracePeriodProvider.get().isEnabled()) {
-                mSceneInteractorLazy.get().changeScene(
-                        Scenes.Lockscreen, "KeyguardService.showDismissibleKeyguard");
+                mMainExecutor.execute(() -> mSceneInteractorLazy.get().changeScene(
+                        Scenes.Lockscreen, "KeyguardService.showDismissibleKeyguard"));
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
index 0ebc92e..b1589da 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt
@@ -40,9 +40,13 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
 
@@ -57,6 +61,9 @@
      */
     val isRunning: Flow<Boolean>
 
+    /** Whether the fingerprint sensor is actively authenticating. */
+    val isEngaged: StateFlow<Boolean>
+
     /**
      * Fingerprint sensor type present on the device, null if fingerprint sensor is not available.
      */
@@ -176,6 +183,17 @@
                     mainDispatcher
                 ) // keyguardUpdateMonitor requires registration on main thread.
 
+    override val isEngaged: StateFlow<Boolean> =
+        authenticationStatus
+            .map { it.isEngaged }
+            .filterNotNull()
+            .map { it }
+            .stateIn(
+                scope = scope,
+                started = WhileSubscribed(),
+                initialValue = false,
+            )
+
     // TODO(b/322555228) Remove after consolidating device entry auth messages with BP auth messages
     //  in BiometricStatusRepository
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 8a53dd1..a2bbcad 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -109,6 +109,19 @@
     )
     val isKeyguardGoingAway: Flow<Boolean>
 
+    /**
+     * Whether the keyguard is enabled, per [KeyguardService]. If the keyguard is not enabled, the
+     * lockscreen cannot be shown and the device will go from AOD/DOZING directly to GONE.
+     *
+     * Keyguard can be disabled by selecting Security: "None" in settings, or by apps that hold
+     * permission to do so (such as Phone).
+     *
+     * If the keyguard is disabled while we're locked, we will transition to GONE unless we're in
+     * lockdown mode. If the keyguard is re-enabled, we'll transition back to LOCKSCREEN if we were
+     * locked when it was disabled.
+     */
+    val isKeyguardEnabled: StateFlow<Boolean>
+
     /** Is the always-on display available to be used? */
     val isAodAvailable: StateFlow<Boolean>
 
@@ -269,6 +282,9 @@
             "'keyguardDoneAnimationsFinished' is when the GONE transition is finished."
     )
     fun keyguardDoneAnimationsFinished()
+
+    /** Sets whether the keyguard is enabled (see [isKeyguardEnabled]). */
+    fun setKeyguardEnabled(enabled: Boolean)
 }
 
 /** Encapsulates application state for the keyguard. */
@@ -439,6 +455,9 @@
         awaitClose { keyguardStateController.removeCallback(callback) }
     }
 
+    private val _isKeyguardEnabled = MutableStateFlow(true)
+    override val isKeyguardEnabled: StateFlow<Boolean> = _isKeyguardEnabled.asStateFlow()
+
     private val _isDozing = MutableStateFlow(statusBarStateController.isDozing)
     override val isDozing: StateFlow<Boolean> = _isDozing.asStateFlow()
 
@@ -664,6 +683,10 @@
         _clockShouldBeCentered.value = shouldBeCentered
     }
 
+    override fun setKeyguardEnabled(enabled: Boolean) {
+        _isKeyguardEnabled.value = enabled
+    }
+
     private fun statusBarStateIntToObject(value: Int): StatusBarState {
         return when (value) {
             0 -> StatusBarState.SHADE
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index a306954..01109af 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
 import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock
@@ -50,6 +51,7 @@
     private val keyguardInteractor: KeyguardInteractor,
     powerInteractor: PowerInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
+    val deviceEntryRepository: DeviceEntryRepository,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.AOD,
@@ -125,7 +127,12 @@
                         val shouldTransitionToOccluded =
                             !KeyguardWmStateRefactor.isEnabled && isKeyguardOccludedLegacy
 
-                        if (canDismissLockscreen) {
+                        val shouldTransitionToGone =
+                            (!KeyguardWmStateRefactor.isEnabled && canDismissLockscreen) ||
+                                (KeyguardWmStateRefactor.isEnabled &&
+                                    !deviceEntryRepository.isLockscreenEnabled())
+
+                        if (shouldTransitionToGone) {
                             startTransitionTo(
                                 toState = KeyguardState.GONE,
                             )
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 115fc36..7d3de30 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
 import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockMode.Companion.isWakeAndUnlock
@@ -50,6 +51,7 @@
     powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
+    val deviceEntryRepository: DeviceEntryRepository,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DOZING,
@@ -99,7 +101,9 @@
                         canTransitionToGoneOnWake,
                         primaryBouncerShowing) ->
                     startTransitionTo(
-                        if (isWakeAndUnlock(biometricUnlockState.mode)) {
+                        if (!deviceEntryRepository.isLockscreenEnabled()) {
+                            KeyguardState.GONE
+                        } else if (isWakeAndUnlock(biometricUnlockState.mode)) {
                             KeyguardState.GONE
                         } else if (canTransitionToGoneOnWake) {
                             KeyguardState.GONE
@@ -145,7 +149,12 @@
                             !isWakeAndUnlock(biometricUnlockState.mode)
                     ) {
                         startTransitionTo(
-                            if (canDismissLockscreen) {
+                            if (!KeyguardWmStateRefactor.isEnabled && canDismissLockscreen) {
+                                KeyguardState.GONE
+                            } else if (
+                                KeyguardWmStateRefactor.isEnabled &&
+                                    !deviceEntryRepository.isLockscreenEnabled()
+                            ) {
                                 KeyguardState.GONE
                             } else if (primaryBouncerShowing) {
                                 KeyguardState.PRIMARY_BOUNCER
@@ -153,7 +162,8 @@
                                 KeyguardState.GLANCEABLE_HUB
                             } else {
                                 KeyguardState.LOCKSCREEN
-                            }
+                            },
+                            ownerReason = "waking from dozing"
                         )
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index 2b3732f..8ca29c8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.KeyguardWmStateRefactor
 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
@@ -36,6 +37,7 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
 
 @SysUISingleton
@@ -52,6 +54,8 @@
     private val communalInteractor: CommunalInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
     private val biometricSettingsRepository: BiometricSettingsRepository,
+    private val keyguardRepository: KeyguardRepository,
+    private val keyguardEnabledInteractor: KeyguardEnabledInteractor,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.GONE,
@@ -93,6 +97,21 @@
                         startTransitionTo(to, ownerReason = "User initiated lockdown")
                     }
             }
+
+            scope.launch {
+                keyguardRepository.isKeyguardEnabled
+                    .filterRelevantKeyguardStateAnd { enabled -> enabled }
+                    .sample(keyguardEnabledInteractor.showKeyguardWhenReenabled)
+                    .filter { reshow -> reshow }
+                    .collect {
+                        startTransitionTo(
+                            KeyguardState.LOCKSCREEN,
+                            ownerReason =
+                                "Keyguard was re-enabled, and we weren't GONE when it " +
+                                    "was originally disabled"
+                        )
+                    }
+            }
         } else {
             scope.launch("$TAG#listenForGoneToLockscreenOrHub") {
                 keyguardInteractor.isKeyguardShowing
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
index fcf67d5..af1ce2b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
@@ -19,7 +19,7 @@
 import android.animation.ValueAnimator
 import com.android.app.animation.Interpolators
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.domain.interactor.CommunalTransitionProgress
+import com.android.systemui.communal.domain.model.CommunalTransitionProgressModel
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -71,7 +71,7 @@
                 if (id == null) {
                     // No transition started.
                     if (
-                        transitionProgress is CommunalTransitionProgress.Transition &&
+                        transitionProgress is CommunalTransitionProgressModel.Transition &&
                             lastStartedState == fromState
                     ) {
                         transitionId =
@@ -93,7 +93,7 @@
                     val nextState: TransitionState
                     val progressFraction: Float
                     when (transitionProgress) {
-                        is CommunalTransitionProgress.Idle -> {
+                        is CommunalTransitionProgressModel.Idle -> {
                             if (transitionProgress.scene == toScene) {
                                 nextState = TransitionState.FINISHED
                                 progressFraction = 1f
@@ -102,11 +102,11 @@
                                 progressFraction = 0f
                             }
                         }
-                        is CommunalTransitionProgress.Transition -> {
+                        is CommunalTransitionProgressModel.Transition -> {
                             nextState = TransitionState.RUNNING
                             progressFraction = transitionProgress.progress
                         }
-                        is CommunalTransitionProgress.OtherTransition -> {
+                        is CommunalTransitionProgressModel.OtherTransition -> {
                             // Shouldn't happen but if another transition starts during the
                             // current one, mark the current one as canceled.
                             nextState = TransitionState.CANCELED
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt
new file mode 100644
index 0000000..8dede01
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractor.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/**
+ * Logic around the keyguard being enabled/disabled, per [KeyguardService]. If the keyguard is not
+ * enabled, the lockscreen cannot be shown and the device will go from AOD/DOZING directly to GONE.
+ *
+ * Keyguard can be disabled by selecting Security: "None" in settings, or by apps that hold
+ * permission to do so (such as Phone). Some CTS tests also disable keyguard in onCreate or onStart
+ * rather than simply dismissing the keyguard or setting up the device to have Security: None, for
+ * reasons unknown.
+ */
+@SysUISingleton
+class KeyguardEnabledInteractor
+@Inject
+constructor(
+    @Application scope: CoroutineScope,
+    val repository: KeyguardRepository,
+    val biometricSettingsRepository: BiometricSettingsRepository,
+    transitionInteractor: KeyguardTransitionInteractor,
+) {
+
+    init {
+        /**
+         * Whenever keyguard is disabled, transition to GONE unless we're in lockdown or already
+         * GONE.
+         */
+        scope.launch {
+            repository.isKeyguardEnabled
+                .filter { enabled -> !enabled }
+                .sampleCombine(
+                    biometricSettingsRepository.isCurrentUserInLockdown,
+                    transitionInteractor.currentTransitionInfoInternal,
+                )
+                .collect { (_, inLockdown, currentTransitionInfo) ->
+                    if (currentTransitionInfo.to != KeyguardState.GONE && !inLockdown) {
+                        transitionInteractor.startDismissKeyguardTransition("keyguard disabled")
+                    }
+                }
+        }
+    }
+
+    /**
+     * Whether we need to show the keyguard when the keyguard is re-enabled, since we hid it when it
+     * became disabled.
+     */
+    val showKeyguardWhenReenabled: Flow<Boolean> =
+        repository.isKeyguardEnabled
+            // Whenever the keyguard is disabled...
+            .filter { enabled -> !enabled }
+            .sampleCombine(
+                transitionInteractor.currentTransitionInfoInternal,
+                biometricSettingsRepository.isCurrentUserInLockdown
+            )
+            .map { (_, transitionInfo, inLockdown) ->
+                // ...we hide the keyguard, if it's showing and we're not in lockdown. In that case,
+                // we want to remember that and re-show it when keyguard is enabled again.
+                transitionInfo.to != KeyguardState.GONE && !inLockdown
+            }
+
+    fun notifyKeyguardEnabled(enabled: Boolean) {
+        repository.setKeyguardEnabled(enabled)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
index 8ba09bd..fb65a6d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
@@ -176,7 +176,8 @@
 
                     if (!returningToGoneAfterCancellation) {
                         // By default, apply the lockscreen visibility of the current state.
-                        KeyguardState.lockscreenVisibleInState(currentState)
+                        deviceEntryInteractor.get().isLockscreenEnabled() &&
+                            KeyguardState.lockscreenVisibleInState(currentState)
                     } else {
                         // If we're transitioning to GONE after a prior canceled transition from
                         // GONE, then this is the camera launch transition from an asleep state back
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
index d8b7b4a..e92dec0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FingerprintAuthenticationModels.kt
@@ -19,6 +19,7 @@
 import android.hardware.biometrics.BiometricFingerprintConstants
 import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_GOOD
 import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
+import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_UNKNOWN
 import android.hardware.fingerprint.FingerprintManager
 import android.os.SystemClock.elapsedRealtime
 import com.android.systemui.biometrics.shared.model.AuthenticationReason
@@ -26,26 +27,43 @@
 /**
  * Fingerprint authentication status provided by
  * [com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository]
+ *
+ * @isEngaged whether fingerprint is actively engaged by the user. This is distinct from fingerprint
+ * running on the device. Can be null if the status does not have an associated isEngaged state.
  */
-sealed class FingerprintAuthenticationStatus
+sealed class FingerprintAuthenticationStatus(val isEngaged: Boolean?)
 
 /** Fingerprint authentication success status. */
 data class SuccessFingerprintAuthenticationStatus(
     val userId: Int,
     val isStrongBiometric: Boolean,
-) : FingerprintAuthenticationStatus()
+) : FingerprintAuthenticationStatus(isEngaged = false)
 
 /** Fingerprint authentication help message. */
 data class HelpFingerprintAuthenticationStatus(
     val msgId: Int,
     val msg: String?,
-) : FingerprintAuthenticationStatus()
+) : FingerprintAuthenticationStatus(isEngaged = null)
 
 /** Fingerprint acquired message. */
 data class AcquiredFingerprintAuthenticationStatus(
     val authenticationReason: AuthenticationReason,
     val acquiredInfo: Int
-) : FingerprintAuthenticationStatus() {
+) :
+    FingerprintAuthenticationStatus(
+        isEngaged =
+            if (acquiredInfo == FINGERPRINT_ACQUIRED_START) {
+                true
+            } else if (
+                acquiredInfo == FINGERPRINT_ACQUIRED_UNKNOWN ||
+                    acquiredInfo == FINGERPRINT_ACQUIRED_GOOD
+            ) {
+                null
+            } else {
+                // soft errors that indicate fingerprint activity ended
+                false
+            }
+    ) {
 
     val fingerprintCaptureStarted: Boolean = acquiredInfo == FINGERPRINT_ACQUIRED_START
 
@@ -53,7 +71,8 @@
 }
 
 /** Fingerprint authentication failed message. */
-data object FailFingerprintAuthenticationStatus : FingerprintAuthenticationStatus()
+data object FailFingerprintAuthenticationStatus :
+    FingerprintAuthenticationStatus(isEngaged = false)
 
 /** Fingerprint authentication error message */
 data class ErrorFingerprintAuthenticationStatus(
@@ -61,7 +80,7 @@
     val msg: String? = null,
     // present to break equality check if the same error occurs repeatedly.
     val createdAt: Long = elapsedRealtime(),
-) : FingerprintAuthenticationStatus() {
+) : FingerprintAuthenticationStatus(isEngaged = false) {
     fun isCancellationError(): Boolean =
         msgId == BiometricFingerprintConstants.FINGERPRINT_ERROR_CANCELED ||
             msgId == BiometricFingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
index 200d30c..7a2e610 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
@@ -88,6 +88,12 @@
             }
     }
 
+    /**
+     * Setups different icon states.
+     * - All lottie views will require a LottieOnCompositionLoadedListener to update
+     *   LottieProperties (like color) of the view.
+     * - Drawable properties can be updated using ImageView properties like imageTintList.
+     */
     private fun setupIconStates() {
         // Lockscreen States
         // LOCK
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
index 0aa6d12..0f1f5c1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt
@@ -64,13 +64,6 @@
         }
     }
 
-    private val color: Flow<Int> =
-        deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBgProtection ->
-            configurationInteractor.onAnyConfigurationChange
-                .map { getColor(useBgProtection) }
-                .onStart { emit(getColor(useBgProtection)) }
-        }
-
     // While dozing, the display can show the AOD UI; show the AOD udfps when dozing
     private val useAodIconVariant: Flow<Boolean> =
         deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest { udfspEnrolled ->
@@ -81,6 +74,22 @@
             }
         }
 
+    private val color: Flow<Int> =
+        useAodIconVariant
+            .flatMapLatest { useAodVariant ->
+                if (useAodVariant) {
+                    flowOf(android.graphics.Color.WHITE)
+                } else {
+                    deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBgProtection
+                        ->
+                        configurationInteractor.onAnyConfigurationChange
+                            .map { getColor(useBgProtection) }
+                            .onStart { emit(getColor(useBgProtection)) }
+                    }
+                }
+            }
+            .distinctUntilChanged()
+
     private val padding: Flow<Int> =
         deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { udfpsSupported ->
             if (udfpsSupported) {
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index 5babc8b..14890d7 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -458,7 +458,7 @@
     @SysUISingleton
     @CarrierTextManagerLog
     public static LogBuffer provideCarrierTextManagerLog(LogBufferFactory factory) {
-        return factory.create("CarrierTextManagerLog", 100);
+        return factory.create("CarrierTextManagerLog", 400);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt
index a618490..de56c84 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt
@@ -21,15 +21,17 @@
 import android.os.Parcelable
 
 /**
- * Class that represents an area that should be captured. Currently it has only a launch cookie that
- * represents a task but we potentially could add more identifiers e.g. for a pair of tasks.
+ * Class that represents an area that should be captured. Currently it has only a launch cookie and
+ * id that represents a task but we potentially could add more identifiers e.g. for a pair of tasks.
  */
-data class MediaProjectionCaptureTarget(val launchCookie: LaunchCookie?) : Parcelable {
+data class MediaProjectionCaptureTarget(val launchCookie: LaunchCookie?, val taskId: Int) :
+    Parcelable {
 
-    constructor(parcel: Parcel) : this(LaunchCookie.readFromParcel(parcel))
+    constructor(parcel: Parcel) : this(LaunchCookie.readFromParcel(parcel), parcel.readInt())
 
     override fun writeToParcel(dest: Parcel, flags: Int) {
         LaunchCookie.writeToParcel(launchCookie, dest)
+        dest.writeInt(taskId)
     }
 
     override fun describeContents(): Int = 0
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt
new file mode 100644
index 0000000..3489459
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionModule.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.mediaprojection
+
+import com.android.systemui.mediaprojection.data.repository.MediaProjectionManagerRepository
+import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface MediaProjectionModule {
+    @Binds fun mediaRepository(impl: MediaProjectionManagerRepository): MediaProjectionRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
index 4685c5a..d6affd2 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt
@@ -174,7 +174,7 @@
         // is created and ready to be captured.
         val activityStarted =
             activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
-                returnSelectedApp(launchCookie)
+                returnSelectedApp(launchCookie, taskId = -1)
             }
 
         // Rely on the ActivityManager to pop up a dialog regarding app suspension
@@ -232,7 +232,7 @@
         }
     }
 
-    override fun returnSelectedApp(launchCookie: LaunchCookie) {
+    override fun returnSelectedApp(launchCookie: LaunchCookie, taskId: Int) {
         taskSelected = true
         if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
             // The client requested to return the result in the result receiver instead of
@@ -242,7 +242,7 @@
                     EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
                     ResultReceiver::class.java
                 ) as ResultReceiver
-            val captureRegion = MediaProjectionCaptureTarget(launchCookie)
+            val captureRegion = MediaProjectionCaptureTarget(launchCookie, taskId)
             val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) }
             resultReceiver.send(RESULT_OK, data)
             // TODO(b/279175710): Ensure consent result is always set here. Skipping this for now
@@ -255,6 +255,7 @@
             val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
 
             projection.setLaunchCookie(launchCookie)
+            projection.setTaskId(taskId)
 
             val intent = Intent()
             intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
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 f08bc17..9b1ca1e 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
@@ -65,7 +65,7 @@
     subcomponents = [MediaProjectionAppSelectorComponent::class],
     includes = [MediaProjectionDevicePolicyModule::class]
 )
-interface MediaProjectionModule {
+interface MediaProjectionActivitiesModule {
     @Binds
     @IntoMap
     @ClassKey(MediaProjectionAppSelectorActivity::class)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt
index f204b3e..6857000 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt
@@ -9,7 +9,10 @@
 interface MediaProjectionAppSelectorResultHandler {
     /**
      * Return selected app to the original caller of the media projection app picker.
-     * @param launchCookie launch cookie of the launched activity of the target app
+     * @param launchCookie launch cookie of the launched activity of the target app, always set
+     *     regardless of launching a new task or a recent task
+     * @param taskId id of the launched task of the target app, only set to a positive int when
+     *     launching a recent task, otherwise set to -1 by default
      */
-    fun returnSelectedApp(launchCookie: LaunchCookie)
+    fun returnSelectedApp(launchCookie: LaunchCookie, taskId: Int)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
index 9549ab1..46aa064 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
@@ -144,10 +144,9 @@
         activityOptions.launchDisplayId = task.displayId
         activityOptions.setLaunchCookie(launchCookie)
 
-        val handleResult: () -> Unit = { resultHandler.returnSelectedApp(launchCookie)}
-
         val taskId = task.taskId
         val splitBounds = task.splitBounds
+        val handleResult: () -> Unit = { resultHandler.returnSelectedApp(launchCookie, taskId)}
 
         if (pssAppSelectorRecentsSplitScreen() &&
             task.isLaunchingInSplitScreen() &&
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
similarity index 87%
rename from packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt
rename to packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
index cfbcaf9..1d5f6f5 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/model/MediaProjectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.mediaprojection.taskswitcher.data.model
+package com.android.systemui.mediaprojection.data.model
 
 import android.app.ActivityManager.RunningTaskInfo
 
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
similarity index 88%
rename from packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt
rename to packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
index 74d1992..3ce0a1e0 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.mediaprojection.taskswitcher.data.repository
+package com.android.systemui.mediaprojection.data.repository
 
 import android.app.ActivityManager.RunningTaskInfo
 import android.media.projection.MediaProjectionInfo
@@ -24,20 +24,21 @@
 import android.view.ContentRecordingSession
 import android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.mediaprojection.MediaProjectionServiceHelper
-import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
@@ -88,7 +89,11 @@
                 mediaProjectionManager.addCallback(callback, handler)
                 awaitClose { mediaProjectionManager.removeCallback(callback) }
             }
-            .shareIn(scope = applicationScope, started = SharingStarted.Lazily, replay = 1)
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Lazily,
+                initialValue = MediaProjectionState.NotProjecting,
+            )
 
     private suspend fun stateForSession(session: ContentRecordingSession?): MediaProjectionState {
         if (session == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt
similarity index 86%
rename from packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt
rename to packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt
index e495466..21300db 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.mediaprojection.taskswitcher.data.repository
+package com.android.systemui.mediaprojection.data.repository
 
 import android.app.ActivityManager.RunningTaskInfo
-import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import kotlinx.coroutines.flow.Flow
 
 /** Represents a repository to retrieve and change data related to media projection. */
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherModule.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherModule.kt
index 22ad07e..eb38958 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/MediaProjectionTaskSwitcherModule.kt
@@ -17,16 +17,11 @@
 package com.android.systemui.mediaprojection.taskswitcher
 
 import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionRepository
 import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository
 import dagger.Binds
 import dagger.Module
 
 @Module
 interface MediaProjectionTaskSwitcherModule {
-
-    @Binds fun mediaRepository(impl: MediaProjectionManagerRepository): MediaProjectionRepository
-
     @Binds fun tasksRepository(impl: ActivityTaskManagerTasksRepository): TasksRepository
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
index eb9e6a5..c232d4d 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
@@ -21,8 +21,8 @@
 import android.content.Intent
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionRepository
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
+import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository
 import com.android.systemui.mediaprojection.taskswitcher.data.repository.TasksRepository
 import com.android.systemui.mediaprojection.taskswitcher.domain.model.TaskSwitchState
 import javax.inject.Inject
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 9487085..d0f8412 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -15,6 +15,7 @@
  */
 package com.android.systemui.navigationbar.gestural;
 
+import static android.content.pm.ActivityInfo.CONFIG_FONT_SCALE;
 import static android.view.InputDevice.SOURCE_MOUSE;
 import static android.view.InputDevice.SOURCE_TOUCHPAD;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION;
@@ -24,10 +25,13 @@
 import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
 import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe;
 
+import static java.util.stream.Collectors.joining;
+
 import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Configuration;
@@ -47,6 +51,7 @@
 import android.os.SystemProperties;
 import android.os.Trace;
 import android.provider.DeviceConfig;
+import android.util.ArraySet;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.TypedValue;
@@ -102,6 +107,7 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
@@ -255,7 +261,7 @@
 
     private boolean mIsAttached;
     private boolean mIsGestureHandlingEnabled;
-    private boolean mIsTrackpadConnected;
+    private final Set<Integer> mTrackpadsConnected = new ArraySet<>();
     private boolean mInGestureNavMode;
     private boolean mUsingThreeButtonNav;
     private boolean mIsEnabled;
@@ -358,16 +364,14 @@
 
     private final InputManager.InputDeviceListener mInputDeviceListener =
             new InputManager.InputDeviceListener() {
-
-        // Only one trackpad can be connected to a device at a time, since it takes over the
-        // only USB port.
-        private int mTrackpadDeviceId;
-
         @Override
         public void onInputDeviceAdded(int deviceId) {
             if (isTrackpadDevice(deviceId)) {
-                mTrackpadDeviceId = deviceId;
-                update(true /* isTrackpadConnected */);
+                boolean wasEmpty = mTrackpadsConnected.isEmpty();
+                mTrackpadsConnected.add(deviceId);
+                if (wasEmpty) {
+                    update();
+                }
             }
         }
 
@@ -376,18 +380,29 @@
 
         @Override
         public void onInputDeviceRemoved(int deviceId) {
-            if (mTrackpadDeviceId == deviceId) {
-                update(false /* isTrackpadConnected */);
+            mTrackpadsConnected.remove(deviceId);
+            if (mTrackpadsConnected.isEmpty()) {
+                update();
             }
         }
 
-        private void update(boolean isTrackpadConnected) {
-            boolean isPreviouslyTrackpadConnected = mIsTrackpadConnected;
-            mIsTrackpadConnected = isTrackpadConnected;
-            if (isPreviouslyTrackpadConnected != mIsTrackpadConnected) {
-                updateIsEnabled();
-                updateCurrentUserResources();
+        private void update() {
+            if (mIsEnabled && !mTrackpadsConnected.isEmpty()) {
+                // Don't reinitialize gesture handling due to trackpad connecting when it's
+                // already set up.
+                return;
             }
+            updateIsEnabled();
+            updateCurrentUserResources();
+        }
+
+        private boolean isTrackpadDevice(int deviceId) {
+            InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
+            if (inputDevice == null) {
+                return false;
+            }
+            return inputDevice.getSources() == (InputDevice.SOURCE_MOUSE
+                    | InputDevice.SOURCE_TOUCHPAD);
         }
     };
 
@@ -566,6 +581,7 @@
         mOverviewProxyService.removeCallback(mQuickSwitchListener);
         mSysUiState.removeCallback(mSysUiStateCallback);
         mInputManager.unregisterInputDeviceListener(mInputDeviceListener);
+        mTrackpadsConnected.clear();
         updateIsEnabled();
         mUserTracker.removeCallback(mUserChangedCallback);
     }
@@ -605,7 +621,7 @@
             Trace.beginSection("EdgeBackGestureHandler#updateIsEnabled");
 
             mIsGestureHandlingEnabled = mInGestureNavMode || (mUsingThreeButtonNav
-                    && mIsTrackpadConnected);
+                    && !mTrackpadsConnected.isEmpty());
             boolean isEnabled = mIsAttached && mIsGestureHandlingEnabled;
             if (isEnabled == mIsEnabled) {
                 return;
@@ -867,15 +883,6 @@
                 mDisplaySize.y - insets.bottom);
     }
 
-    private boolean isTrackpadDevice(int deviceId) {
-        InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
-        if (inputDevice == null) {
-            return false;
-        }
-        return inputDevice.getSources() == (InputDevice.SOURCE_MOUSE
-                | InputDevice.SOURCE_TOUCHPAD);
-    }
-
     private boolean desktopExcludeRegionContains(int x, int y) {
         return mDesktopModeExcludeRegion.contains(x, y);
     }
@@ -1175,6 +1182,10 @@
         // TODO(b/332635834): Disable this logging once b/332635834 is fixed.
         Log.i(DEBUG_MISSING_GESTURE_TAG, "Config changed: newConfig=" + newConfig
                 + " lastReportedConfig=" + mLastReportedConfig);
+        final int diff = newConfig.diff(mLastReportedConfig);
+        if ((diff & CONFIG_FONT_SCALE) != 0 || (diff & ActivityInfo.CONFIG_DENSITY) != 0) {
+            updateCurrentUserResources();
+        }
         mLastReportedConfig.updateFrom(newConfig);
         updateDisplaySize();
     }
@@ -1251,7 +1262,8 @@
         pw.println("  mPredictionLog=" + String.join("\n", mPredictionLog));
         pw.println("  mGestureLogInsideInsets=" + String.join("\n", mGestureLogInsideInsets));
         pw.println("  mGestureLogOutsideInsets=" + String.join("\n", mGestureLogOutsideInsets));
-        pw.println("  mIsTrackpadConnected=" + mIsTrackpadConnected);
+        pw.println("  mTrackpadsConnected=" + mTrackpadsConnected.stream().map(
+                String::valueOf).collect(joining()));
         pw.println("  mUsingThreeButtonNav=" + mUsingThreeButtonNav);
         pw.println("  mEdgeBackPlugin=" + mEdgeBackPlugin);
         if (mEdgeBackPlugin != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index b34b370..e77bd03 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -485,6 +485,11 @@
     }
 
     @Override
+    public int getMinRows() {
+        return mMinRows;
+    }
+
+    @Override
     public boolean setMaxColumns(int maxColumns) {
         mMaxColumns = maxColumns;
         boolean changed = false;
@@ -497,6 +502,11 @@
         return changed;
     }
 
+    @Override
+    public int getMaxColumns() {
+        return mMaxColumns;
+    }
+
     /**
      * Set the amount of excess space that we gave this view compared to the actual available
      * height. This is because this view is in a scrollview.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 00757b7..9c8c17b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -42,6 +42,7 @@
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.settings.brightness.BrightnessSliderController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
@@ -441,7 +442,7 @@
     }
 
     private boolean needsDynamicRowsAndColumns() {
-        return true;
+        return !SceneContainerFlag.isEnabled();
     }
 
     private void switchAllContentToParent(ViewGroup parent, QSTileLayout newLayout) {
@@ -634,8 +635,7 @@
             switchAllContentToParent(newParent, mTileLayout);
             reAttachMediaHost(mediaHostView, horizontal);
             if (needsDynamicRowsAndColumns()) {
-                mTileLayout.setMinRows(horizontal ? 2 : 1);
-                mTileLayout.setMaxColumns(horizontal ? 2 : 4);
+                setColumnRowLayout(horizontal);
             }
             updateMargins(mediaHostView);
             if (mHorizontalLinearLayout != null) {
@@ -644,6 +644,11 @@
         }
     }
 
+    void setColumnRowLayout(boolean withMedia) {
+        mTileLayout.setMinRows(withMedia ? 2 : 1);
+        mTileLayout.setMaxColumns(withMedia ? 2 : 4);
+    }
+
     private void updateMargins(ViewGroup mediaHostView) {
         updateMediaHostContentMargins(mediaHostView);
         updateHorizontalLinearLayoutMargins();
@@ -736,6 +741,8 @@
             return false;
         }
 
+        int getMinRows();
+
         /**
          * Sets the max number of columns to show
          *
@@ -747,6 +754,8 @@
             return false;
         }
 
+        int getMaxColumns();
+
         /**
          * Sets the expansion value and proposedTranslation to panel.
          */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index e24caf1..f76183e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -30,6 +30,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.haptics.qs.QSLongPressEffect;
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor;
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager;
 import com.android.systemui.media.controls.ui.view.MediaHost;
 import com.android.systemui.media.controls.ui.view.MediaHostState;
@@ -46,10 +47,13 @@
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
 import com.android.systemui.tuner.TunerService;
 
+import kotlinx.coroutines.flow.StateFlow;
+
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Provider;
 
+
 /**
  * Controller for {@link QSPanel}.
  */
@@ -72,6 +76,8 @@
     private final BrightnessSliderController.Factory mBrightnessSliderControllerFactory;
     private final BrightnessController.Factory mBrightnessControllerFactory;
 
+    protected final MediaCarouselInteractor mMediaCarouselInteractor;
+
     private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() {
         @Override
         public boolean onTouch(View v, MotionEvent event) {
@@ -94,7 +100,8 @@
             FalsingManager falsingManager,
             StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             SplitShadeStateController splitShadeStateController,
-            Provider<QSLongPressEffect> longPRessEffectProvider) {
+            Provider<QSLongPressEffect> longPRessEffectProvider,
+            MediaCarouselInteractor mediaCarouselInteractor) {
         super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost,
                 metricsLogger, uiEventLogger, qsLogger, dumpManager, splitShadeStateController,
                 longPRessEffectProvider);
@@ -113,6 +120,7 @@
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mLastDensity = view.getResources().getConfiguration().densityDpi;
         mSceneContainerEnabled = SceneContainerFlag.isEnabled();
+        mMediaCarouselInteractor = mediaCarouselInteractor;
     }
 
     @Override
@@ -126,6 +134,11 @@
     }
 
     @Override
+    StateFlow<Boolean> getMediaVisibleFlow() {
+        return mMediaCarouselInteractor.getHasAnyMediaOrRecommendation();
+    }
+
+    @Override
     protected void onViewAttached() {
         super.onViewAttached();
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 583cfb9..3b5cc61 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -41,13 +41,17 @@
 import com.android.systemui.qs.external.CustomTile;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileViewImpl;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.animation.DisappearParameters;
+import com.android.systemui.util.kotlin.JavaAdapterKt;
 
 import kotlin.Unit;
 import kotlin.jvm.functions.Function1;
 
+import kotlinx.coroutines.flow.StateFlow;
+
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -58,6 +62,7 @@
 
 import javax.inject.Provider;
 
+
 /**
  * Controller for QSPanel views.
  *
@@ -93,6 +98,15 @@
 
     private final Provider<QSLongPressEffect> mLongPressEffectProvider;
 
+    private boolean mDestroyed = false;
+
+    private boolean mMediaVisibleFromInteractor;
+
+    private final Consumer<Boolean> mMediaOrRecommendationVisibleConsumer = mediaVisible -> {
+        mMediaVisibleFromInteractor = mediaVisible;
+        setLayoutForMediaInScene();
+    };
+
     @VisibleForTesting
     protected final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener =
             new QSPanel.OnConfigurationChangedListener() {
@@ -115,7 +129,11 @@
                         /* newScreenLayout= */ mLastScreenLayout,
                         /* containerName= */ mView.getDumpableTag());
 
-                    switchTileLayoutIfNeeded();
+                    if (SceneContainerFlag.isEnabled()) {
+                        setLayoutForMediaInScene();
+                    } else {
+                        switchTileLayoutIfNeeded();
+                    }
                     onConfigurationChanged();
                     if (previousSplitShadeState != mShouldUseSplitNotificationShade) {
                         onSplitShadeChanged(mShouldUseSplitNotificationShade);
@@ -173,6 +191,9 @@
         mView.initialize(mQSLogger, mUsingMediaPlayer);
         mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), "");
         mHost.addCallback(mQSHostCallback);
+        if (SceneContainerFlag.isEnabled()) {
+            registerForMediaInteractorChanges();
+        }
     }
 
     /**
@@ -192,7 +213,7 @@
         // will remove the attach listener. We don't need to do that, because once this object is
         // detached from the graph, it will be gc.
         mHost.removeCallback(mQSHostCallback);
-
+        mDestroyed = true;
         for (TileRecord record : mRecords) {
             record.tile.removeCallback(record.callback);
             mView.removeTile(record);
@@ -207,17 +228,32 @@
             mQsTileRevealController.setExpansion(mRevealExpansion);
         }
 
-        mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener);
+        if (!SceneContainerFlag.isEnabled()) {
+            mMediaHost.addVisibilityChangeListener(mMediaHostVisibilityListener);
+        }
         mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
         setTiles();
         mLastOrientation = getResources().getConfiguration().orientation;
         mLastScreenLayout = getResources().getConfiguration().screenLayout;
         mQSLogger.logOnViewAttached(mLastOrientation, mView.getDumpableTag());
+        if (SceneContainerFlag.isEnabled()) {
+            setLayoutForMediaInScene();
+        }
         switchTileLayout(true);
 
         mDumpManager.registerDumpable(mView.getDumpableTag(), this);
     }
 
+    private void registerForMediaInteractorChanges() {
+        JavaAdapterKt.collectFlow(
+                mView,
+                getMediaVisibleFlow(),
+                mMediaOrRecommendationVisibleConsumer
+        );
+    }
+
+    abstract StateFlow<Boolean> getMediaVisibleFlow();
+
     @Override
     protected void onViewDetached() {
         mQSLogger.logOnViewDetached(mLastOrientation, mView.getDumpableTag());
@@ -242,6 +278,7 @@
 
     /** */
     public void setTiles(Collection<QSTile> tiles, boolean collapsedView) {
+        if (mDestroyed) return;
         // TODO(b/168904199): move this logic into QSPanelController.
         if (!collapsedView && mQsTileRevealController != null) {
             mQsTileRevealController.updateRevealedTiles(tiles);
@@ -433,6 +470,11 @@
         return false;
     }
 
+    void setLayoutForMediaInScene() {
+        boolean withMedia = shouldUseHorizontalInScene();
+        mView.setColumnRowLayout(withMedia);
+    }
+
     /**
      * Update the way the media disappears based on if we're using the horizontal layout
      */
@@ -473,6 +515,16 @@
                 == Configuration.SCREENLAYOUT_LONG_YES;
     }
 
+    boolean shouldUseHorizontalInScene() {
+        if (mShouldUseSplitNotificationShade) {
+            return false;
+        }
+        return mMediaVisibleFromInteractor
+                && mLastOrientation == Configuration.ORIENTATION_LANDSCAPE
+                && (mLastScreenLayout & Configuration.SCREENLAYOUT_LONG_MASK)
+                == Configuration.SCREENLAYOUT_LONG_YES;
+    }
+
     private void logTiles() {
         for (int i = 0; i < mRecords.size(); i++) {
             QSTile tile = mRecords.get(i).tile;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index 6cda740..f207b1d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -26,6 +26,7 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.haptics.qs.QSLongPressEffect;
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor;
 import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager;
 import com.android.systemui.media.controls.ui.view.MediaHost;
 import com.android.systemui.plugins.qs.QSTile;
@@ -36,6 +37,8 @@
 import com.android.systemui.statusbar.policy.SplitShadeStateController;
 import com.android.systemui.util.leak.RotationUtils;
 
+import kotlinx.coroutines.flow.StateFlow;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -43,12 +46,15 @@
 import javax.inject.Named;
 import javax.inject.Provider;
 
+
 /** Controller for {@link QuickQSPanel}. */
 @QSScope
 public class QuickQSPanelController extends QSPanelControllerBase<QuickQSPanel> {
 
     private final Provider<Boolean> mUsingCollapsedLandscapeMediaProvider;
 
+    private final MediaCarouselInteractor mMediaCarouselInteractor;
+
     @Inject
     QuickQSPanelController(QuickQSPanel view, QSHost qsHost,
             QSCustomizerController qsCustomizerController,
@@ -58,12 +64,14 @@
                     Provider<Boolean> usingCollapsedLandscapeMediaProvider,
             MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
             DumpManager dumpManager, SplitShadeStateController splitShadeStateController,
-            Provider<QSLongPressEffect> longPressEffectProvider
+            Provider<QSLongPressEffect> longPressEffectProvider,
+            MediaCarouselInteractor mediaCarouselInteractor
     ) {
         super(view, qsHost, qsCustomizerController, usingMediaPlayer, mediaHost, metricsLogger,
                 uiEventLogger, qsLogger, dumpManager, splitShadeStateController,
                 longPressEffectProvider);
         mUsingCollapsedLandscapeMediaProvider = usingCollapsedLandscapeMediaProvider;
+        mMediaCarouselInteractor = mediaCarouselInteractor;
     }
 
     @Override
@@ -74,6 +82,11 @@
         mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
     }
 
+    @Override
+    StateFlow<Boolean> getMediaVisibleFlow() {
+        return mMediaCarouselInteractor.getHasActiveMediaOrRecommendation();
+    }
+
     private void updateMediaExpansion() {
         int rotation = getRotation();
         boolean isLandscape = rotation == RotationUtils.ROTATION_LANDSCAPE
diff --git a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
index dcb9288..ef44e5f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
@@ -12,6 +12,7 @@
 import android.widget.TextView;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.FontSizeUtils;
@@ -97,12 +98,24 @@
         return false;
     }
 
+    @VisibleForTesting
+    @Override
+    public int getMinRows() {
+        return mMinRows;
+    }
+
     @Override
     public boolean setMaxColumns(int maxColumns) {
         mMaxColumns = maxColumns;
         return updateColumns();
     }
 
+    @VisibleForTesting
+    @Override
+    public int getMaxColumns() {
+        return mMaxColumns;
+    }
+
     public void addTile(TileRecord tile) {
         mRecords.add(tile);
         tile.tile.setListening(this, mListening);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
index d161c6b..7b67993 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.qs.panels.domain.interactor.NoopGridConsistencyInteractor
 import com.android.systemui.qs.panels.shared.model.GridConsistencyLog
 import com.android.systemui.qs.panels.shared.model.GridLayoutType
+import com.android.systemui.qs.panels.shared.model.IconLabelVisibilityLog
 import com.android.systemui.qs.panels.shared.model.InfiniteGridLayoutType
 import com.android.systemui.qs.panels.shared.model.PartitionedGridLayoutType
 import com.android.systemui.qs.panels.shared.model.StretchedGridLayoutType
@@ -35,6 +36,12 @@
 import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
 import com.android.systemui.qs.panels.ui.compose.PartitionedGridLayout
 import com.android.systemui.qs.panels.ui.compose.StretchedGridLayout
+import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModelImpl
+import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModelImpl
+import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModelImpl
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -53,6 +60,15 @@
         impl: NoopGridConsistencyInteractor
     ): GridTypeConsistencyInteractor
 
+    @Binds fun bindIconTilesViewModel(impl: IconTilesViewModelImpl): IconTilesViewModel
+
+    @Binds fun bindGridSizeViewModel(impl: InfiniteGridSizeViewModelImpl): InfiniteGridSizeViewModel
+
+    @Binds
+    fun bindIconLabelVisibilityViewModel(
+        impl: IconLabelVisibilityViewModelImpl
+    ): IconLabelVisibilityViewModel
+
     @Binds @Named("Default") fun bindDefaultGridLayout(impl: PartitionedGridLayout): GridLayout
 
     companion object {
@@ -64,6 +80,13 @@
         }
 
         @Provides
+        @SysUISingleton
+        @IconLabelVisibilityLog
+        fun providesIconTileLabelVisibilityLog(factory: LogBufferFactory): LogBuffer {
+            return factory.create("IconLabelVisibilityLog", 50)
+        }
+
+        @Provides
         @IntoSet
         fun provideGridLayout(gridLayout: InfiniteGridLayout): Pair<GridLayoutType, GridLayout> {
             return Pair(InfiniteGridLayoutType, gridLayout)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconLabelVisibilityRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconLabelVisibilityRepository.kt
new file mode 100644
index 0000000..686e5f4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconLabelVisibilityRepository.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Repository for whether to show the labels of icon tiles. */
+@SysUISingleton
+class IconLabelVisibilityRepository @Inject constructor() {
+    // TODO(b/341735914): Persist and back up showLabels
+    private val _showLabels = MutableStateFlow(false)
+    val showLabels: StateFlow<Boolean> = _showLabels.asStateFlow()
+
+    fun setShowLabels(showLabels: Boolean) {
+        _showLabels.value = showLabels
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconLabelVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconLabelVisibilityInteractor.kt
new file mode 100644
index 0000000..a871531
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconLabelVisibilityInteractor.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.qs.panels.data.repository.IconLabelVisibilityRepository
+import com.android.systemui.qs.panels.shared.model.IconLabelVisibilityLog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class IconLabelVisibilityInteractor
+@Inject
+constructor(
+    private val repo: IconLabelVisibilityRepository,
+    @IconLabelVisibilityLog private val logBuffer: LogBuffer,
+    @Application scope: CoroutineScope,
+) {
+    val showLabels: StateFlow<Boolean> =
+        repo.showLabels
+            .onEach { logChange(it) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), repo.showLabels.value)
+
+    fun setShowLabels(showLabels: Boolean) {
+        repo.setShowLabels(showLabels)
+    }
+
+    private fun logChange(showLabels: Boolean) {
+        logBuffer.log(
+            LOG_BUFFER_ICON_TILE_LABEL_VISIBILITY_CHANGE_TAG,
+            LogLevel.DEBUG,
+            { bool1 = showLabels },
+            { "Icon tile label visibility changed: $bool1" }
+        )
+    }
+
+    private companion object {
+        const val LOG_BUFFER_ICON_TILE_LABEL_VISIBILITY_CHANGE_TAG = "IconLabelVisibilityChange"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/IconLabelVisibilityLog.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/IconLabelVisibilityLog.kt
new file mode 100644
index 0000000..c92234c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/IconLabelVisibilityLog.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.shared.model
+
+import javax.inject.Qualifier
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class IconLabelVisibilityLog()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
index f5ee720..4aeaa7d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
@@ -26,9 +26,9 @@
 import androidx.compose.ui.res.dimensionResource
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor
-import com.android.systemui.qs.panels.domain.interactor.InfiniteGridSizeInteractor
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.res.R
@@ -38,8 +38,8 @@
 class InfiniteGridLayout
 @Inject
 constructor(
-    private val iconTilesInteractor: IconTilesInteractor,
-    private val gridSizeInteractor: InfiniteGridSizeInteractor
+    private val iconTilesViewModel: IconTilesViewModel,
+    private val gridSizeViewModel: InfiniteGridSizeViewModel,
 ) : GridLayout {
 
     @Composable
@@ -52,8 +52,8 @@
             tiles.forEach { it.startListening(token) }
             onDispose { tiles.forEach { it.stopListening(token) } }
         }
-        val iconTilesSpecs by iconTilesInteractor.iconTilesSpecs.collectAsStateWithLifecycle()
-        val columns by gridSizeInteractor.columns.collectAsStateWithLifecycle()
+        val iconTilesSpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle()
+        val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
 
         TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
             items(
@@ -68,9 +68,9 @@
                 }
             ) { index ->
                 Tile(
-                    tiles[index],
-                    iconTilesSpecs.contains(tiles[index].spec),
-                    Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
+                    tile = tiles[index],
+                    iconOnly = iconTilesSpecs.contains(tiles[index].spec),
+                    modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
                 )
             }
         }
@@ -83,8 +83,8 @@
         onAddTile: (TileSpec, Int) -> Unit,
         onRemoveTile: (TileSpec) -> Unit,
     ) {
-        val iconOnlySpecs by iconTilesInteractor.iconTilesSpecs.collectAsStateWithLifecycle()
-        val columns by gridSizeInteractor.columns.collectAsStateWithLifecycle()
+        val iconOnlySpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle()
+        val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
 
         DefaultEditTileGrid(
             tiles = tiles,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
index 8d0b386..708ef0d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -32,6 +33,8 @@
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.getValue
@@ -39,14 +42,14 @@
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.modifiers.background
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor
-import com.android.systemui.qs.panels.domain.interactor.InfiniteGridSizeInteractor
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.PartitionedGridViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
 import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -54,12 +57,8 @@
 import javax.inject.Inject
 
 @SysUISingleton
-class PartitionedGridLayout
-@Inject
-constructor(
-    private val iconTilesInteractor: IconTilesInteractor,
-    private val gridSizeInteractor: InfiniteGridSizeInteractor,
-) : GridLayout {
+class PartitionedGridLayout @Inject constructor(private val viewModel: PartitionedGridViewModel) :
+    GridLayout {
     @Composable
     override fun TileGrid(tiles: List<TileViewModel>, modifier: Modifier) {
         DisposableEffect(tiles) {
@@ -67,9 +66,11 @@
             tiles.forEach { it.startListening(token) }
             onDispose { tiles.forEach { it.stopListening(token) } }
         }
-        val iconTilesSpecs by iconTilesInteractor.iconTilesSpecs.collectAsStateWithLifecycle()
-        val columns by gridSizeInteractor.columns.collectAsStateWithLifecycle()
-        val tileHeight = dimensionResource(id = R.dimen.qs_tile_height)
+        val iconTilesSpecs by viewModel.iconTilesSpecs.collectAsStateWithLifecycle()
+        val columns by viewModel.columns.collectAsStateWithLifecycle()
+        val showLabels by viewModel.showLabels.collectAsStateWithLifecycle()
+        val largeTileHeight = tileHeight()
+        val iconTileHeight = tileHeight(showLabels)
         val (smallTiles, largeTiles) = tiles.partition { iconTilesSpecs.contains(it.spec) }
 
         TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
@@ -78,7 +79,7 @@
                 Tile(
                     tile = largeTiles[index],
                     iconOnly = false,
-                    modifier = Modifier.height(tileHeight)
+                    modifier = Modifier.height(largeTileHeight)
                 )
             }
             fillUpRow(nTiles = largeTiles.size, columns = columns / 2)
@@ -88,7 +89,8 @@
                 Tile(
                     tile = smallTiles[index],
                     iconOnly = true,
-                    modifier = Modifier.height(tileHeight)
+                    showLabels = showLabels,
+                    modifier = Modifier.height(iconTileHeight)
                 )
             }
         }
@@ -101,8 +103,9 @@
         onAddTile: (TileSpec, Int) -> Unit,
         onRemoveTile: (TileSpec) -> Unit
     ) {
-        val iconOnlySpecs by iconTilesInteractor.iconTilesSpecs.collectAsStateWithLifecycle()
-        val columns by gridSizeInteractor.columns.collectAsStateWithLifecycle()
+        val iconOnlySpecs by viewModel.iconTilesSpecs.collectAsStateWithLifecycle()
+        val columns by viewModel.columns.collectAsStateWithLifecycle()
+        val showLabels by viewModel.showLabels.collectAsStateWithLifecycle()
 
         val (currentTiles, otherTiles) = tiles.partition { it.isCurrent }
         val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
@@ -110,27 +113,56 @@
         }
         val isIconOnly: (TileSpec) -> Boolean =
             remember(iconOnlySpecs) { { tileSpec: TileSpec -> tileSpec in iconOnlySpecs } }
-        val tileHeight = dimensionResource(id = R.dimen.qs_tile_height)
+        val largeTileHeight = tileHeight()
+        val iconTileHeight = tileHeight(showLabels)
         val tilePadding = dimensionResource(R.dimen.qs_tile_margin_vertical)
 
         Column(
             verticalArrangement = Arrangement.spacedBy(tilePadding),
             modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState())
         ) {
+            Row(
+                modifier =
+                    Modifier.background(
+                            color = MaterialTheme.colorScheme.surfaceVariant,
+                            alpha = { 1f },
+                            shape = RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius))
+                        )
+                        .padding(tilePadding)
+            ) {
+                Column(Modifier.padding(start = tilePadding)) {
+                    Text(
+                        text = "Show text labels",
+                        color = MaterialTheme.colorScheme.onBackground,
+                        fontWeight = FontWeight.Bold
+                    )
+                    Text(
+                        text = "Display names under each tile",
+                        color = MaterialTheme.colorScheme.onBackground
+                    )
+                }
+                Spacer(modifier = Modifier.weight(1f))
+                Switch(checked = showLabels, onCheckedChange = { viewModel.setShowLabels(it) })
+            }
+
             CurrentTiles(
                 tiles = currentTiles,
-                tileHeight = tileHeight,
+                largeTileHeight = largeTileHeight,
+                iconTileHeight = iconTileHeight,
                 tilePadding = tilePadding,
                 onRemoveTile = onRemoveTile,
                 isIconOnly = isIconOnly,
                 columns = columns,
+                showLabels = showLabels,
             )
             AvailableTiles(
                 tiles = otherTiles,
-                tileHeight = tileHeight,
+                largeTileHeight = largeTileHeight,
+                iconTileHeight = iconTileHeight,
                 tilePadding = tilePadding,
                 addTileToEnd = addTileToEnd,
                 isIconOnly = isIconOnly,
+                showLabels = showLabels,
                 columns = columns,
             )
         }
@@ -139,23 +171,31 @@
     @Composable
     private fun CurrentTiles(
         tiles: List<EditTileViewModel>,
-        tileHeight: Dp,
+        largeTileHeight: Dp,
+        iconTileHeight: Dp,
         tilePadding: Dp,
         onRemoveTile: (TileSpec) -> Unit,
         isIconOnly: (TileSpec) -> Boolean,
+        showLabels: Boolean,
         columns: Int,
     ) {
         val (smallTiles, largeTiles) = tiles.partition { isIconOnly(it.tileSpec) }
 
-        val largeGridHeight = gridHeight(largeTiles.size, tileHeight, columns / 2, tilePadding)
-        val smallGridHeight = gridHeight(smallTiles.size, tileHeight, columns, tilePadding)
+        val largeGridHeight = gridHeight(largeTiles.size, largeTileHeight, columns / 2, tilePadding)
+        val smallGridHeight = gridHeight(smallTiles.size, iconTileHeight, columns, tilePadding)
 
         CurrentTilesContainer {
             TileLazyGrid(
                 columns = GridCells.Fixed(columns),
                 modifier = Modifier.height(largeGridHeight),
             ) {
-                editTiles(largeTiles, ClickAction.REMOVE, onRemoveTile, { false }, true)
+                editTiles(
+                    largeTiles,
+                    ClickAction.REMOVE,
+                    onRemoveTile,
+                    { false },
+                    indicatePosition = true
+                )
             }
         }
         CurrentTilesContainer {
@@ -163,7 +203,14 @@
                 columns = GridCells.Fixed(columns),
                 modifier = Modifier.height(smallGridHeight),
             ) {
-                editTiles(smallTiles, ClickAction.REMOVE, onRemoveTile, { true }, true)
+                editTiles(
+                    smallTiles,
+                    ClickAction.REMOVE,
+                    onRemoveTile,
+                    { true },
+                    showLabels = showLabels,
+                    indicatePosition = true
+                )
             }
         }
     }
@@ -171,19 +218,21 @@
     @Composable
     private fun AvailableTiles(
         tiles: List<EditTileViewModel>,
-        tileHeight: Dp,
+        largeTileHeight: Dp,
+        iconTileHeight: Dp,
         tilePadding: Dp,
         addTileToEnd: (TileSpec) -> Unit,
         isIconOnly: (TileSpec) -> Boolean,
+        showLabels: Boolean,
         columns: Int,
     ) {
         val (tilesStock, tilesCustom) = tiles.partition { it.appName == null }
         val (smallTiles, largeTiles) = tilesStock.partition { isIconOnly(it.tileSpec) }
 
-        val largeGridHeight = gridHeight(largeTiles.size, tileHeight, columns / 2, tilePadding)
-        val smallGridHeight = gridHeight(smallTiles.size, tileHeight, columns, tilePadding)
+        val largeGridHeight = gridHeight(largeTiles.size, largeTileHeight, columns / 2, tilePadding)
+        val smallGridHeight = gridHeight(smallTiles.size, iconTileHeight, columns, tilePadding)
         val largeGridHeightCustom =
-            gridHeight(tilesCustom.size, tileHeight, columns / 2, tilePadding)
+            gridHeight(tilesCustom.size, largeTileHeight, columns / 2, tilePadding)
 
         // Add up the height of all three grids and add padding in between
         val gridHeight =
@@ -199,7 +248,13 @@
                 fillUpRow(nTiles = largeTiles.size, columns = columns / 2)
 
                 // Small tiles
-                editTiles(smallTiles, ClickAction.ADD, addTileToEnd, isIconOnly)
+                editTiles(
+                    smallTiles,
+                    ClickAction.ADD,
+                    addTileToEnd,
+                    isIconOnly,
+                    showLabels = showLabels
+                )
                 fillUpRow(nTiles = smallTiles.size, columns = columns)
 
                 // Custom tiles, all large
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
index ddd97c2..70d629f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
@@ -27,11 +27,11 @@
 import androidx.compose.ui.res.dimensionResource
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor
-import com.android.systemui.qs.panels.domain.interactor.InfiniteGridSizeInteractor
 import com.android.systemui.qs.panels.shared.model.SizedTile
 import com.android.systemui.qs.panels.shared.model.TileRow
 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
 import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
 import com.android.systemui.qs.pipeline.shared.TileSpec
 import com.android.systemui.res.R
@@ -41,8 +41,8 @@
 class StretchedGridLayout
 @Inject
 constructor(
-    private val iconTilesInteractor: IconTilesInteractor,
-    private val gridSizeInteractor: InfiniteGridSizeInteractor,
+    private val iconTilesViewModel: IconTilesViewModel,
+    private val gridSizeViewModel: InfiniteGridSizeViewModel,
 ) : GridLayout {
 
     @Composable
@@ -60,7 +60,7 @@
         // Icon [3 | 4]
         // Large [6 | 8]
         val columns = 12
-        val iconTilesSpecs by iconTilesInteractor.iconTilesSpecs.collectAsStateWithLifecycle()
+        val iconTilesSpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle()
         val stretchedTiles =
             remember(tiles) {
                 val sizedTiles =
@@ -80,9 +80,9 @@
         TileLazyGrid(columns = GridCells.Fixed(columns), modifier = modifier) {
             items(stretchedTiles.size, span = { GridItemSpan(stretchedTiles[it].width) }) { index ->
                 Tile(
-                    stretchedTiles[index].tile,
-                    iconTilesSpecs.contains(stretchedTiles[index].tile.spec),
-                    Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
+                    tile = stretchedTiles[index].tile,
+                    iconOnly = iconTilesSpecs.contains(stretchedTiles[index].tile.spec),
+                    modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
                 )
             }
         }
@@ -95,8 +95,8 @@
         onAddTile: (TileSpec, Int) -> Unit,
         onRemoveTile: (TileSpec) -> Unit
     ) {
-        val iconOnlySpecs by iconTilesInteractor.iconTilesSpecs.collectAsStateWithLifecycle()
-        val columns by gridSizeInteractor.columns.collectAsStateWithLifecycle()
+        val iconOnlySpecs by iconTilesViewModel.iconTilesSpecs.collectAsStateWithLifecycle()
+        val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
 
         DefaultEditTileGrid(
             tiles = tiles,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
index e8c65a5..a6838c0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/Tile.kt
@@ -69,6 +69,9 @@
 import androidx.compose.ui.semantics.onClick
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.animation.Expandable
@@ -98,6 +101,7 @@
 fun Tile(
     tile: TileViewModel,
     iconOnly: Boolean,
+    showLabels: Boolean = false,
     modifier: Modifier,
 ) {
     val state: TileUiState by
@@ -136,7 +140,8 @@
                 secondaryLabel = state.secondaryLabel.toString(),
                 icon = icon,
                 colors = state.colors,
-                iconOnly = iconOnly
+                iconOnly = iconOnly,
+                showLabels = showLabels,
             )
         }
     }
@@ -213,6 +218,7 @@
     clickAction: ClickAction,
     onClick: (TileSpec) -> Unit,
     isIconOnly: (TileSpec) -> Boolean,
+    showLabels: Boolean = false,
     indicatePosition: Boolean = false,
 ) {
     items(
@@ -250,10 +256,13 @@
                         this.stateDescription = stateDescription
                     }
         ) {
+            val iconOnly = isIconOnly(viewModel.tileSpec)
+            val tileHeight = tileHeight(iconOnly && showLabels)
             EditTile(
                 tileViewModel = viewModel,
-                isIconOnly(viewModel.tileSpec),
-                modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
+                iconOnly = iconOnly,
+                showLabels = showLabels,
+                modifier = Modifier.height(tileHeight)
             )
             if (canClick) {
                 Badge(clickAction, Modifier.align(Alignment.TopEnd))
@@ -281,6 +290,7 @@
 fun EditTile(
     tileViewModel: EditTileViewModel,
     iconOnly: Boolean,
+    showLabels: Boolean,
     modifier: Modifier = Modifier,
 ) {
     val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec
@@ -297,6 +307,7 @@
             colors = colors,
             icon = tileViewModel.icon,
             iconOnly = iconOnly,
+            showLabels = showLabels,
             animateIconToEnd = true,
         )
     }
@@ -380,9 +391,26 @@
     icon: Icon,
     colors: TileColorAttributes,
     iconOnly: Boolean,
+    showLabels: Boolean = false,
     animateIconToEnd: Boolean = false,
 ) {
-    TileIcon(icon, colorAttr(colors.icon), animateIconToEnd)
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally,
+        verticalArrangement = Arrangement.Center,
+        modifier = Modifier.fillMaxHeight()
+    ) {
+        TileIcon(icon, colorAttr(colors.icon), animateIconToEnd)
+
+        if (iconOnly && showLabels) {
+            Text(
+                label,
+                maxLines = 2,
+                color = colorAttr(colors.label),
+                overflow = TextOverflow.Ellipsis,
+                textAlign = TextAlign.Center,
+            )
+        }
+    }
 
     if (!iconOnly) {
         Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
@@ -401,3 +429,16 @@
         }
     }
 }
+
+@Composable
+fun tileHeight(iconWithLabel: Boolean = false): Dp {
+    return if (iconWithLabel) {
+        TileDimensions.IconTileWithLabelHeight
+    } else {
+        dimensionResource(id = R.dimen.qs_tile_height)
+    }
+}
+
+private object TileDimensions {
+    val IconTileWithLabelHeight = 100.dp
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconLabelVisibilityViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconLabelVisibilityViewModel.kt
new file mode 100644
index 0000000..5d4b8f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconLabelVisibilityViewModel.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.domain.interactor.IconLabelVisibilityInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+interface IconLabelVisibilityViewModel {
+    val showLabels: StateFlow<Boolean>
+
+    fun setShowLabels(showLabels: Boolean)
+}
+
+@SysUISingleton
+class IconLabelVisibilityViewModelImpl
+@Inject
+constructor(private val interactor: IconLabelVisibilityInteractor) : IconLabelVisibilityViewModel {
+    override val showLabels: StateFlow<Boolean> = interactor.showLabels
+
+    override fun setShowLabels(showLabels: Boolean) {
+        interactor.setShowLabels(showLabels)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt
new file mode 100644
index 0000000..9ad00c8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModel.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+interface IconTilesViewModel {
+    val iconTilesSpecs: StateFlow<Set<TileSpec>>
+}
+
+@SysUISingleton
+class IconTilesViewModelImpl @Inject constructor(interactor: IconTilesInteractor) :
+    IconTilesViewModel {
+    override val iconTilesSpecs = interactor.iconTilesSpecs
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt
new file mode 100644
index 0000000..a4ee58f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.domain.interactor.InfiniteGridSizeInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+interface InfiniteGridSizeViewModel {
+    val columns: StateFlow<Int>
+}
+
+@SysUISingleton
+class InfiniteGridSizeViewModelImpl @Inject constructor(interactor: InfiniteGridSizeInteractor) :
+    InfiniteGridSizeViewModel {
+    override val columns: StateFlow<Int> = interactor.columns
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
new file mode 100644
index 0000000..730cf63
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+@SysUISingleton
+class PartitionedGridViewModel
+@Inject
+constructor(
+    iconTilesViewModel: IconTilesViewModel,
+    gridSizeViewModel: InfiniteGridSizeViewModel,
+    iconLabelVisibilityViewModel: IconLabelVisibilityViewModel,
+) :
+    IconTilesViewModel by iconTilesViewModel,
+    InfiniteGridSizeViewModel by gridSizeViewModel,
+    IconLabelVisibilityViewModel by iconLabelVisibilityViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 56588ff..8887f58 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -677,6 +677,10 @@
             mId = id;
         }
 
+        public int getResourceId() {
+            return mId;
+        }
+
         @Override
         public boolean equals(Object o) {
             return o instanceof DrawableIconWithRes && ((DrawableIconWithRes) o).mId == mId;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
index 76aa146..f218d86 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/QuickAccessWalletTile.java
@@ -16,8 +16,13 @@
 
 package com.android.systemui.qs.tiles;
 
-import static android.graphics.drawable.Icon.TYPE_URI;
 import static android.provider.Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT;
+import static android.graphics.drawable.Icon.TYPE_URI;
+import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP;
+import static android.graphics.drawable.Icon.TYPE_RESOURCE;
+import static android.graphics.drawable.Icon.TYPE_BITMAP;
+import static android.graphics.drawable.Icon.TYPE_ADAPTIVE_BITMAP;
+import static android.graphics.drawable.Icon.TYPE_DATA;
 
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE;
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE;
@@ -237,11 +242,21 @@
                 return;
             }
             mSelectedCard = cards.get(selectedIndex);
-            android.graphics.drawable.Icon cardImageIcon = mSelectedCard.getCardImage();
-            if (cardImageIcon.getType() == TYPE_URI) {
-                mCardViewDrawable = null;
-            } else {
-                mCardViewDrawable = mSelectedCard.getCardImage().loadDrawable(mContext);
+            switch (mSelectedCard.getCardImage().getType()) {
+                case TYPE_URI:
+                case TYPE_URI_ADAPTIVE_BITMAP:
+                    mCardViewDrawable = null;
+                    break;
+                case TYPE_RESOURCE:
+                case TYPE_BITMAP:
+                case TYPE_ADAPTIVE_BITMAP:
+                case TYPE_DATA:
+                    mCardViewDrawable = mSelectedCard.getCardImage().loadDrawable(mContext);
+                    break;
+                default:
+                    Log.e(TAG, "Unknown icon type: " + mSelectedCard.getCardImage().getType());
+                    mCardViewDrawable = null;
+                    break;
             }
             refreshState();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
index c0fc52e..f088943 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
@@ -18,6 +18,7 @@
 
 import android.content.res.Resources
 import android.content.res.Resources.Theme
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
 import com.android.systemui.qs.tiles.impl.alarm.domain.model.AlarmTileModel
@@ -82,7 +83,8 @@
                     secondaryLabel = resources.getString(R.string.qs_alarm_tile_no_alarm)
                 }
             }
-
+            iconRes = R.drawable.ic_alarm
+            icon = { Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) }
             sideViewIcon = QSTileState.SideViewIcon.Chevron
             contentDescription = label
             supportedActions = setOf(QSTileState.UserAction.CLICK)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt
index 0c08fba..bcf0935 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt
@@ -38,17 +38,10 @@
         QSTileState.build(resources, theme, config.uiConfig) {
             label = resources.getString(R.string.battery_detail_switch_title)
             contentDescription = label
-
-            icon = {
-                Icon.Loaded(
-                    resources.getDrawable(
-                        if (data.isPowerSaving) R.drawable.qs_battery_saver_icon_on
-                        else R.drawable.qs_battery_saver_icon_off,
-                        theme
-                    ),
-                    null
-                )
-            }
+            iconRes =
+                if (data.isPowerSaving) R.drawable.qs_battery_saver_icon_on
+                else R.drawable.qs_battery_saver_icon_off
+            icon = { Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) }
 
             sideViewIcon = QSTileState.SideViewIcon.None
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt
index 1efbfd7..cad7c65 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt
@@ -37,6 +37,8 @@
         QSTileState.build(resources, theme, config.uiConfig) {
             val subtitleArray = resources.getStringArray(R.array.tile_states_color_correction)
 
+            iconRes = R.drawable.ic_qs_color_correction
+
             if (data.isEnabled) {
                 activationState = QSTileState.ActivationState.ACTIVE
                 secondaryLabel = subtitleArray[2]
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt
index 58e7613..d7d6124 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt
@@ -37,14 +37,16 @@
 
     override fun map(config: QSTileConfig, data: FlashlightTileModel): QSTileState =
         QSTileState.build(resources, theme, config.uiConfig) {
+            iconRes =
+                if (data is FlashlightTileModel.FlashlightAvailable && data.isEnabled) {
+                    R.drawable.qs_flashlight_icon_on
+                } else {
+                    R.drawable.qs_flashlight_icon_off
+                }
             val icon =
                 Icon.Loaded(
                     resources.getDrawable(
-                        if (data is FlashlightTileModel.FlashlightAvailable && data.isEnabled) {
-                            R.drawable.qs_flashlight_icon_on
-                        } else {
-                            R.drawable.qs_flashlight_icon_off
-                        },
+                        iconRes!!,
                         theme,
                     ),
                     contentDescription = null
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt
index 26069c7..6b4dda1 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt
@@ -36,10 +36,11 @@
 
     override fun map(config: QSTileConfig, data: FontScalingTileModel): QSTileState =
         QSTileState.build(resources, theme, config.uiConfig) {
+            iconRes = R.drawable.ic_qs_font_scaling
             val icon =
                 Icon.Loaded(
                     resources.getDrawable(
-                        R.drawable.ic_qs_font_scaling,
+                        iconRes!!,
                         theme,
                     ),
                     contentDescription = null
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
index caae4d2..e543e4b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt
@@ -53,6 +53,7 @@
             stateDescription = data.stateDescription.loadContentDescription(context)
             contentDescription = data.contentDescription.loadContentDescription(context)
 
+            iconRes = data.iconId
             if (data.icon != null) {
                 this.icon = { data.icon }
             } else if (data.iconId != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt
index 4af9854..40aee65 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt
@@ -41,22 +41,13 @@
             if (data.isEnabled) {
                 activationState = QSTileState.ActivationState.ACTIVE
                 secondaryLabel = subtitleArray[2]
-                icon = {
-                    Icon.Loaded(
-                        resources.getDrawable(R.drawable.qs_invert_colors_icon_on, theme),
-                        null
-                    )
-                }
+                iconRes = R.drawable.qs_invert_colors_icon_on
             } else {
                 activationState = QSTileState.ActivationState.INACTIVE
                 secondaryLabel = subtitleArray[1]
-                icon = {
-                    Icon.Loaded(
-                        resources.getDrawable(R.drawable.qs_invert_colors_icon_off, theme),
-                        null
-                    )
-                }
+                iconRes = R.drawable.qs_invert_colors_icon_off
             }
+            icon = { Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) }
             contentDescription = label
             supportedActions =
                 setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt
index fe5445d..d58f5ab 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt
@@ -37,14 +37,16 @@
 
     override fun map(config: QSTileConfig, data: LocationTileModel): QSTileState =
         QSTileState.build(resources, theme, config.uiConfig) {
+            iconRes =
+                if (data.isEnabled) {
+                    R.drawable.qs_location_icon_on
+                } else {
+                    R.drawable.qs_location_icon_off
+                }
             val icon =
                 Icon.Loaded(
                     resources.getDrawable(
-                        if (data.isEnabled) {
-                            R.drawable.qs_location_icon_on
-                        } else {
-                            R.drawable.qs_location_icon_off
-                        },
+                        iconRes!!,
                         theme,
                     ),
                     contentDescription = null
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt
index 5c2dcfc..bcf7cc7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt
@@ -52,21 +52,14 @@
 
             if (data.isActivated) {
                 activationState = QSTileState.ActivationState.ACTIVE
-                val loadedIcon =
-                    Icon.Loaded(
-                        resources.getDrawable(R.drawable.qs_nightlight_icon_on, theme),
-                        contentDescription = null
-                    )
-                icon = { loadedIcon }
+                iconRes = R.drawable.qs_nightlight_icon_on
             } else {
                 activationState = QSTileState.ActivationState.INACTIVE
-                val loadedIcon =
-                    Icon.Loaded(
-                        resources.getDrawable(R.drawable.qs_nightlight_icon_off, theme),
-                        contentDescription = null
-                    )
-                icon = { loadedIcon }
+                iconRes = R.drawable.qs_nightlight_icon_off
             }
+            val loadedIcon =
+                Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null)
+            icon = { loadedIcon }
 
             secondaryLabel = getSecondaryLabel(data, resources)
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt
index 9166ed8..4080996 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt
@@ -38,15 +38,8 @@
         QSTileState.build(resources, theme, config.uiConfig) {
             val subtitleArray = resources.getStringArray(R.array.tile_states_onehanded)
             label = resources.getString(R.string.quick_settings_onehanded_label)
-            icon = {
-                Icon.Loaded(
-                    resources.getDrawable(
-                        com.android.internal.R.drawable.ic_qs_one_handed_mode,
-                        theme
-                    ),
-                    null
-                )
-            }
+            iconRes = com.android.internal.R.drawable.ic_qs_one_handed_mode
+            icon = { Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) }
             if (data.isEnabled) {
                 activationState = QSTileState.ActivationState.ACTIVE
                 secondaryLabel = subtitleArray[2]
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt
index 45a7717..8231742 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt
@@ -38,9 +38,8 @@
         QSTileState.build(resources, theme, config.uiConfig) {
             label = resources.getString(R.string.qr_code_scanner_title)
             contentDescription = label
-            icon = {
-                Icon.Loaded(resources.getDrawable(R.drawable.ic_qr_code_scanner, theme), null)
-            }
+            iconRes = R.drawable.ic_qr_code_scanner
+            icon = { Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) }
             sideViewIcon = QSTileState.SideViewIcon.Chevron
             supportedActions = setOf(QSTileState.UserAction.CLICK)
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt
index fca93df..85ee022 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt
@@ -39,28 +39,23 @@
         QSTileState.build(resources, theme, config.uiConfig) {
             if (data.isEnabled) {
                 activationState = QSTileState.ActivationState.ACTIVE
-                icon = {
-                    Icon.Loaded(
-                        drawable = resources.getDrawable(R.drawable.qs_extra_dim_icon_on, theme),
-                        contentDescription = null
-                    )
-                }
-
+                iconRes = R.drawable.qs_extra_dim_icon_on
                 secondaryLabel =
                     resources
                         .getStringArray(R.array.tile_states_reduce_brightness)[Tile.STATE_ACTIVE]
             } else {
                 activationState = QSTileState.ActivationState.INACTIVE
-                icon = {
-                    Icon.Loaded(
-                        drawable = resources.getDrawable(R.drawable.qs_extra_dim_icon_off, theme),
-                        contentDescription = null
-                    )
-                }
+                iconRes = R.drawable.qs_extra_dim_icon_off
                 secondaryLabel =
                     resources
                         .getStringArray(R.array.tile_states_reduce_brightness)[Tile.STATE_INACTIVE]
             }
+            icon = {
+                Icon.Loaded(
+                    drawable = resources.getDrawable(iconRes!!, theme),
+                    contentDescription = null
+                )
+            }
             label =
                 resources.getString(com.android.internal.R.string.reduce_bright_colors_feature_name)
             contentDescription = label
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt
index 070cdef..8e80fb0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt
@@ -44,12 +44,7 @@
             if (data.isRotationLocked) {
                 activationState = QSTileState.ActivationState.INACTIVE
                 this.secondaryLabel = EMPTY_SECONDARY_STRING
-                this.icon = {
-                    Icon.Loaded(
-                        resources.getDrawable(R.drawable.qs_auto_rotate_icon_off, theme),
-                        contentDescription = null
-                    )
-                }
+                iconRes = R.drawable.qs_auto_rotate_icon_off
             } else {
                 activationState = QSTileState.ActivationState.ACTIVE
                 this.secondaryLabel =
@@ -58,12 +53,10 @@
                     } else {
                         EMPTY_SECONDARY_STRING
                     }
-                this.icon = {
-                    Icon.Loaded(
-                        resources.getDrawable(R.drawable.qs_auto_rotate_icon_on, theme),
-                        contentDescription = null
-                    )
-                }
+                this.iconRes = R.drawable.qs_auto_rotate_icon_on
+            }
+            this.icon = {
+                Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null)
             }
             if (isDeviceFoldable()) {
                 this.secondaryLabel = getSecondaryLabelWithPosture(this.activationState)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt
index df25600..888bba87 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt
@@ -36,7 +36,6 @@
     override fun map(config: QSTileConfig, data: DataSaverTileModel): QSTileState =
         QSTileState.build(resources, theme, config.uiConfig) {
             with(data) {
-                val iconRes: Int
                 if (isEnabled) {
                     activationState = QSTileState.ActivationState.ACTIVE
                     iconRes = R.drawable.qs_data_saver_icon_on
@@ -47,7 +46,7 @@
                     secondaryLabel = resources.getStringArray(R.array.tile_states_saver)[1]
                 }
                 val loadedIcon =
-                    Icon.Loaded(resources.getDrawable(iconRes, theme), contentDescription = null)
+                    Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null)
                 icon = { loadedIcon }
                 contentDescription = label
                 supportedActions =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
index b58774b..7446708 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
@@ -42,9 +42,10 @@
             when (data) {
                 is ScreenRecordModel.Recording -> {
                     activationState = QSTileState.ActivationState.ACTIVE
+                    iconRes = R.drawable.qs_screen_record_icon_on
                     val loadedIcon =
                         Icon.Loaded(
-                            resources.getDrawable(R.drawable.qs_screen_record_icon_on, theme),
+                            resources.getDrawable(iconRes!!, theme),
                             contentDescription = null
                         )
                     icon = { loadedIcon }
@@ -53,9 +54,10 @@
                 }
                 is ScreenRecordModel.Starting -> {
                     activationState = QSTileState.ActivationState.ACTIVE
+                    iconRes = R.drawable.qs_screen_record_icon_on
                     val loadedIcon =
                         Icon.Loaded(
-                            resources.getDrawable(R.drawable.qs_screen_record_icon_on, theme),
+                            resources.getDrawable(iconRes!!, theme),
                             contentDescription = null
                         )
                     icon = { loadedIcon }
@@ -65,9 +67,10 @@
                 }
                 is ScreenRecordModel.DoingNothing -> {
                     activationState = QSTileState.ActivationState.INACTIVE
+                    iconRes = R.drawable.qs_screen_record_icon_off
                     val loadedIcon =
                         Icon.Loaded(
-                            resources.getDrawable(R.drawable.qs_screen_record_icon_off, theme),
+                            resources.getDrawable(iconRes!!, theme),
                             contentDescription = null
                         )
                     icon = { loadedIcon }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt
index 52622d2..597cf27 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt
@@ -50,15 +50,8 @@
             contentDescription = label
             supportedActions =
                 setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
-            icon = {
-                Icon.Loaded(
-                    resources.getDrawable(
-                        sensorPrivacyTileResources.getIconRes(data.isBlocked),
-                        theme
-                    ),
-                    null
-                )
-            }
+            iconRes = sensorPrivacyTileResources.getIconRes(data.isBlocked)
+            icon = { Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) }
 
             sideViewIcon = QSTileState.SideViewIcon.None
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt
index ffef2b6..f29c745d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt
@@ -117,12 +117,12 @@
                     }
                 }
 
-                val iconRes =
+                iconRes =
                     if (activationState == QSTileState.ActivationState.ACTIVE)
                         R.drawable.qs_light_dark_theme_icon_on
                     else R.drawable.qs_light_dark_theme_icon_off
                 val loadedIcon =
-                    Icon.Loaded(resources.getDrawable(iconRes, theme), contentDescription = null)
+                    Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null)
                 icon = { loadedIcon }
 
                 supportedActions =
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt
index 55445bb..eee95b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt
@@ -41,15 +41,9 @@
         QSTileState.build(resources, theme, config.uiConfig) {
             label = getTileLabel()!!
             contentDescription = label
-
+            iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status
             icon = {
-                Icon.Loaded(
-                    resources.getDrawable(
-                        com.android.internal.R.drawable.stat_sys_managed_profile_status,
-                        theme
-                    ),
-                    contentDescription = null
-                )
+                Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null)
             }
 
             when (data) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
index b927e41..ae6c014 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt
@@ -29,10 +29,14 @@
  * [QSTileState.build] for better state creation experience and preset default values for certain
  * fields.
  *
+ * @param iconRes For when we want to have Loaded icon, but still keep a reference to the resource
+ *   id. A use case would be for tests that have to compare animated drawables.
+ *
  * // TODO(b/http://b/299909989): Clean up legacy mappings after the transition
  */
 data class QSTileState(
     val icon: () -> Icon?,
+    val iconRes: Int?,
     val label: CharSequence,
     val activationState: ActivationState,
     val secondaryLabel: CharSequence?,
@@ -111,6 +115,7 @@
         var icon: () -> Icon?,
         var label: CharSequence,
     ) {
+        var iconRes: Int? = null
         var activationState: ActivationState = ActivationState.INACTIVE
         var secondaryLabel: CharSequence? = null
         var supportedActions: Set<UserAction> = setOf(UserAction.CLICK)
@@ -123,6 +128,7 @@
         fun build(): QSTileState =
             QSTileState(
                 icon,
+                iconRes,
                 label,
                 activationState,
                 secondaryLabel,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 5346b23..7be13e0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.plugins.qs.QSTile
 import com.android.systemui.qs.QSHost
 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon
+import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIconWithRes
 import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -241,7 +242,9 @@
 
                 iconSupplier = Supplier {
                     when (val stateIcon = viewModelState.icon()) {
-                        is Icon.Loaded -> DrawableIcon(stateIcon.drawable)
+                        is Icon.Loaded ->
+                            if (viewModelState.iconRes == null) DrawableIcon(stateIcon.drawable)
+                            else DrawableIconWithRes(stateIcon.drawable, viewModelState.iconRes)
                         is Icon.Resource -> ResourceIcon.get(stateIcon.res)
                         null -> null
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index fb872d5..c7326b08 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -46,7 +46,6 @@
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -89,8 +88,10 @@
     /**
      * A view with the QS content ([QSContainerImpl]), managed by an instance of [QSImpl] tracked by
      * the interactor.
+     *
+     * A null value means that there is no inflated view yet. See [inflate].
      */
-    val qsView: Flow<View>
+    val qsView: StateFlow<View?>
 
     /** Sets the [MirrorController] in [QSImpl]. Set to `null` to remove. */
     fun setBrightnessMirrorController(mirrorController: MirrorController?)
@@ -101,9 +102,22 @@
      */
     suspend fun inflate(context: Context)
 
-    /** Set the current state for QS. [state]. */
+    /**
+     * Set the current state for QS. [state].
+     *
+     * This will not trigger expansion (animation between QQS or QS) or squishiness to be applied.
+     * For that, use [applyLatestExpansionAndSquishiness] outside of the composition phase.
+     */
     fun setState(state: State)
 
+    /**
+     * Explicitly applies the expansion and squishiness value from the latest state set. Call this
+     * only outside of the composition phase as this will call [QSImpl.setQsExpansion] that is
+     * normally called during animations. In particular, this will read the value of
+     * [State.squishiness], that is not safe to read in the composition phase.
+     */
+    fun applyLatestExpansionAndSquishiness()
+
     /** Propagates the bottom nav bar size to [QSImpl] to be used as necessary. */
     suspend fun applyBottomNavBarPadding(padding: Int)
 
@@ -141,14 +155,24 @@
             override val squishiness = { 1f }
         }
 
-        /** State for appearing QQS from Lockscreen or Gone */
-        data class UnsquishingQQS(override val squishiness: () -> Float) : State {
+        /**
+         * State for appearing QQS from Lockscreen or Gone.
+         *
+         * This should not be a data class, as it has a method parameter and even if it's the same
+         * lambda the output value may have changed.
+         */
+        class UnsquishingQQS(override val squishiness: () -> Float) : State {
             override val isVisible = true
             override val expansion = 0f
         }
 
-        /** State for appearing QS from Lockscreen or Gone, used in Split shade */
-        data class UnsquishingQS(override val squishiness: () -> Float) : State {
+        /**
+         * State for appearing QS from Lockscreen or Gone, used in Split shade.
+         *
+         * This should not be a data class, as it has a method parameter and even if it's the same
+         * lambda the output value may have changed.
+         */
+        class UnsquishingQS(override val squishiness: () -> Float) : State {
             override val isVisible = true
             override val expansion = 1f
         }
@@ -236,7 +260,10 @@
 
     private val _qsImpl: MutableStateFlow<QSImpl?> = MutableStateFlow(null)
     val qsImpl = _qsImpl.asStateFlow()
-    override val qsView: Flow<View> = _qsImpl.map { it?.view }.filterNotNull()
+    override val qsView: StateFlow<View?> =
+        _qsImpl
+            .map { it?.view }
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), _qsImpl.value?.view)
 
     override val qqsHeight: Int
         get() = qsImpl.value?.qqsHeight ?: 0
@@ -370,7 +397,12 @@
         setQsVisible(state.isVisible)
         setExpanded(state.isVisible && state.expansion > 0f)
         setListening(state.isVisible)
-        setQsExpansion(state.expansion, 1f, 0f, state.squishiness())
+    }
+
+    override fun applyLatestExpansionAndSquishiness() {
+        val qsImpl = _qsImpl.value
+        val state = state.value
+        qsImpl?.setQsExpansion(state.expansion, 1f, 0f, state.squishiness())
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
index b971781..bccbb11 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
@@ -2,12 +2,14 @@
 
 import android.content.Context
 import android.util.AttributeSet
+import android.view.MotionEvent
 import android.view.View
 import android.view.WindowInsets
 import com.android.systemui.scene.shared.model.Scene
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
 import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.shade.TouchLogger
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -60,4 +62,16 @@
         this.windowInsets.value = windowInsets
         return windowInsets
     }
+
+    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+        viewModel.onMotionEvent(ev)
+        return super.dispatchTouchEvent(ev).also {
+            TouchLogger.logDispatchTouch(TAG, ev, it)
+            viewModel.onMotionEventComplete()
+        }
+    }
+
+    companion object {
+        private const val TAG = "SceneWindowRootView"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
index 5469a4e..e024710 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
@@ -119,6 +119,7 @@
         IMediaProjection projection = IMediaProjection.Stub.asInterface(proj.asBinder());
         if (mCaptureRegion != null) {
             projection.setLaunchCookie(mCaptureRegion.getLaunchCookie());
+            projection.setTaskId(mCaptureRegion.getTaskId());
         }
         mMediaProjection = new MediaProjection(mContext, projection);
         mMediaProjection.registerCallback(this, mHandler);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
index 4ab0918..9e62280 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionIntentExecutor.kt
@@ -90,12 +90,18 @@
             )
         }
         systemUiProxy.dismissKeyguard()
-        transitionCoordinator?.startExit()
+        var transitionOptions: ActivityOptions? = null
+        if (transitionCoordinator?.decor?.isAttachedToWindow == true) {
+            transitionCoordinator.startExit()
+            transitionOptions = options
+        }
 
         if (user == myUserHandle()) {
-            withContext(mainDispatcher) { context.startActivity(intent, options?.toBundle()) }
+            withContext(mainDispatcher) {
+                context.startActivity(intent, transitionOptions?.toBundle())
+            }
         } else {
-            launchCrossProfileIntent(user, intent, options?.toBundle())
+            launchCrossProfileIntent(user, intent, transitionOptions?.toBundle())
         }
 
         if (overrideTransition) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsController.kt
new file mode 100644
index 0000000..2ffb783
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsController.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.app.assist.AssistContent
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonAppearance
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.UUID
+
+/**
+ * Responsible for obtaining the actions for each screenshot and sending them to the view model.
+ * Ensures that only actions from screenshots that are currently being shown are added to the view
+ * model.
+ */
+class ScreenshotActionsController
+@AssistedInject
+constructor(
+    private val viewModel: ScreenshotViewModel,
+    private val actionsProviderFactory: ScreenshotActionsProvider.Factory,
+    @Assisted val actionExecutor: ActionExecutor
+) {
+    private val actionProviders: MutableMap<UUID, ScreenshotActionsProvider> = mutableMapOf()
+    private var currentScreenshotId: UUID? = null
+
+    fun setCurrentScreenshot(screenshot: ScreenshotData): UUID {
+        val screenshotId = UUID.randomUUID()
+        currentScreenshotId = screenshotId
+        actionProviders[screenshotId] =
+            actionsProviderFactory.create(
+                screenshotId,
+                screenshot,
+                actionExecutor,
+                ActionsCallback(screenshotId),
+            )
+        return screenshotId
+    }
+
+    fun endScreenshotSession() {
+        currentScreenshotId = null
+    }
+
+    fun onAssistContent(screenshotId: UUID, assistContent: AssistContent?) {
+        actionProviders[screenshotId]?.onAssistContent(assistContent)
+    }
+
+    fun onScrollChipReady(screenshotId: UUID, onClick: Runnable) {
+        if (screenshotId == currentScreenshotId) {
+            actionProviders[screenshotId]?.onScrollChipReady(onClick)
+        }
+    }
+
+    fun onScrollChipInvalidated() {
+        for (provider in actionProviders.values) {
+            provider.onScrollChipInvalidated()
+        }
+    }
+
+    fun setCompletedScreenshot(screenshotId: UUID, result: ScreenshotSavedResult) {
+        if (screenshotId == currentScreenshotId) {
+            actionProviders[screenshotId]?.setCompletedScreenshot(result)
+        }
+    }
+
+    @AssistedFactory
+    interface Factory {
+        fun getController(actionExecutor: ActionExecutor): ScreenshotActionsController
+    }
+
+    inner class ActionsCallback(private val screenshotId: UUID) {
+        fun providePreviewAction(onClick: () -> Unit) {
+            if (screenshotId == currentScreenshotId) {
+                viewModel.setPreviewAction(onClick)
+            }
+        }
+
+        fun provideActionButton(
+            appearance: ActionButtonAppearance,
+            showDuringEntrance: Boolean,
+            onClick: () -> Unit
+        ): Int {
+            if (screenshotId == currentScreenshotId) {
+                return viewModel.addAction(appearance, showDuringEntrance, onClick)
+            }
+            return 0
+        }
+
+        fun updateActionButtonAppearance(buttonId: Int, appearance: ActionButtonAppearance) {
+            if (screenshotId == currentScreenshotId) {
+                viewModel.updateActionAppearance(buttonId, appearance)
+            }
+        }
+
+        fun updateActionButtonVisibility(buttonId: Int, visible: Boolean) {
+            if (screenshotId == currentScreenshotId) {
+                viewModel.setActionVisibility(buttonId, visible)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
index a1dd415..b8029c8 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
@@ -29,10 +29,10 @@
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_SHARE_TAPPED
 import com.android.systemui.screenshot.ui.viewmodel.ActionButtonAppearance
-import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import java.util.UUID
 
 /**
  * Provides actions for screenshots. This class can be overridden by a vendor-specific SysUI
@@ -51,9 +51,10 @@
 
     interface Factory {
         fun create(
+            requestId: UUID,
             request: ScreenshotData,
-            requestId: String,
             actionExecutor: ActionExecutor,
+            actionsCallback: ScreenshotActionsController.ActionsCallback,
         ): ScreenshotActionsProvider
     }
 }
@@ -62,11 +63,11 @@
 @AssistedInject
 constructor(
     private val context: Context,
-    private val viewModel: ScreenshotViewModel,
     private val uiEventLogger: UiEventLogger,
+    @Assisted val requestId: UUID,
     @Assisted val request: ScreenshotData,
-    @Assisted val requestId: String,
     @Assisted val actionExecutor: ActionExecutor,
+    @Assisted val actionsCallback: ScreenshotActionsController.ActionsCallback,
 ) : ScreenshotActionsProvider {
     private var addedScrollChip = false
     private var onScrollClick: Runnable? = null
@@ -74,7 +75,7 @@
     private var result: ScreenshotSavedResult? = null
 
     init {
-        viewModel.setPreviewAction {
+        actionsCallback.providePreviewAction {
             debugLog(LogConfig.DEBUG_ACTIONS) { "Preview tapped" }
             uiEventLogger.log(SCREENSHOT_PREVIEW_TAPPED, 0, request.packageNameString)
             onDeferrableActionTapped { result ->
@@ -85,26 +86,8 @@
                 )
             }
         }
-        viewModel.addAction(
-            ActionButtonAppearance(
-                AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit),
-                context.resources.getString(R.string.screenshot_edit_label),
-                context.resources.getString(R.string.screenshot_edit_description),
-            ),
-            showDuringEntrance = true,
-        ) {
-            debugLog(LogConfig.DEBUG_ACTIONS) { "Edit tapped" }
-            uiEventLogger.log(SCREENSHOT_EDIT_TAPPED, 0, request.packageNameString)
-            onDeferrableActionTapped { result ->
-                actionExecutor.startSharedTransition(
-                    createEdit(result.uri, context),
-                    result.user,
-                    true
-                )
-            }
-        }
 
-        viewModel.addAction(
+        actionsCallback.provideActionButton(
             ActionButtonAppearance(
                 AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share),
                 context.resources.getString(R.string.screenshot_share_label),
@@ -122,12 +105,31 @@
                 )
             }
         }
+
+        actionsCallback.provideActionButton(
+            ActionButtonAppearance(
+                AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit),
+                context.resources.getString(R.string.screenshot_edit_label),
+                context.resources.getString(R.string.screenshot_edit_description),
+            ),
+            showDuringEntrance = true,
+        ) {
+            debugLog(LogConfig.DEBUG_ACTIONS) { "Edit tapped" }
+            uiEventLogger.log(SCREENSHOT_EDIT_TAPPED, 0, request.packageNameString)
+            onDeferrableActionTapped { result ->
+                actionExecutor.startSharedTransition(
+                    createEdit(result.uri, context),
+                    result.user,
+                    true
+                )
+            }
+        }
     }
 
     override fun onScrollChipReady(onClick: Runnable) {
         onScrollClick = onClick
         if (!addedScrollChip) {
-            viewModel.addAction(
+            actionsCallback.provideActionButton(
                 ActionButtonAppearance(
                     AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_scroll),
                     context.resources.getString(R.string.screenshot_scroll_label),
@@ -161,9 +163,10 @@
     @AssistedFactory
     interface Factory : ScreenshotActionsProvider.Factory {
         override fun create(
+            requestId: UUID,
             request: ScreenshotData,
-            requestId: String,
             actionExecutor: ActionExecutor,
+            actionsCallback: ScreenshotActionsController.ActionsCallback,
         ): DefaultScreenshotActionsProvider
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 4c60090..e8dfac8 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -192,7 +192,6 @@
     private final WindowContext mContext;
     private final FeatureFlags mFlags;
     private final ScreenshotViewProxy mViewProxy;
-    private final ScreenshotActionsProvider.Factory mActionsProviderFactory;
     private final ScreenshotNotificationsController mNotificationsController;
     private final ScreenshotSmartActions mScreenshotSmartActions;
     private final UiEventLogger mUiEventLogger;
@@ -202,7 +201,7 @@
     private final ExecutorService mBgExecutor;
     private final BroadcastSender mBroadcastSender;
     private final BroadcastDispatcher mBroadcastDispatcher;
-    private final ActionExecutor mActionExecutor;
+    private final ScreenshotActionsController mActionsController;
 
     private final WindowManager mWindowManager;
     private final WindowManager.LayoutParams mWindowLayoutParams;
@@ -217,16 +216,18 @@
     private final ActionIntentExecutor mActionIntentExecutor;
     private final UserManager mUserManager;
     private final AssistContentRequester mAssistContentRequester;
+    private final ActionExecutor mActionExecutor;
+
 
     private final MessageContainerController mMessageContainerController;
     private final AnnouncementResolver mAnnouncementResolver;
     private Bitmap mScreenBitmap;
     private SaveImageInBackgroundTask mSaveInBgTask;
     private boolean mScreenshotTakenInPortrait;
-    private boolean mBlockAttach;
+    private boolean mAttachRequested;
+    private boolean mDetachRequested;
     private Animator mScreenshotAnimation;
     private RequestCallback mCurrentRequestCallback;
-    private ScreenshotActionsProvider mActionsProvider;
     private String mPackageName = "";
     private final BroadcastReceiver mCopyBroadcastReceiver;
 
@@ -253,7 +254,6 @@
             WindowManager windowManager,
             FeatureFlags flags,
             ScreenshotViewProxy.Factory viewProxyFactory,
-            ScreenshotActionsProvider.Factory actionsProviderFactory,
             ScreenshotSmartActions screenshotSmartActions,
             ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory,
             UiEventLogger uiEventLogger,
@@ -265,6 +265,7 @@
             BroadcastSender broadcastSender,
             BroadcastDispatcher broadcastDispatcher,
             ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
+            ScreenshotActionsController.Factory screenshotActionsControllerFactory,
             ActionIntentExecutor actionIntentExecutor,
             ActionExecutor.Factory actionExecutorFactory,
             UserManager userManager,
@@ -276,7 +277,6 @@
             @Assisted boolean showUIOnExternalDisplay
     ) {
         mScreenshotSmartActions = screenshotSmartActions;
-        mActionsProviderFactory = actionsProviderFactory;
         mNotificationsController = screenshotNotificationsControllerFactory.create(
                 display.getDisplayId());
         mUiEventLogger = uiEventLogger;
@@ -327,6 +327,8 @@
                     finishDismiss();
                     return Unit.INSTANCE;
                 });
+        mActionsController = screenshotActionsControllerFactory.getController(mActionExecutor);
+
 
         // Sound is only reproduced from the controller of the default display.
         if (mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY) {
@@ -403,20 +405,21 @@
             return;
         }
 
+        final UUID requestId;
         if (screenshotShelfUi2()) {
-            final UUID requestId = UUID.randomUUID();
-            final String screenshotId = String.format("Screenshot_%s", requestId);
-            mActionsProvider = mActionsProviderFactory.create(
-                    screenshot, screenshotId, mActionExecutor);
+            requestId = mActionsController.setCurrentScreenshot(screenshot);
             saveScreenshotInBackground(screenshot, requestId, finisher);
 
             if (screenshot.getTaskId() >= 0) {
-                mAssistContentRequester.requestAssistContent(screenshot.getTaskId(),
-                        assistContent -> mActionsProvider.onAssistContent(assistContent));
+                mAssistContentRequester.requestAssistContent(
+                        screenshot.getTaskId(),
+                        assistContent ->
+                                mActionsController.onAssistContent(requestId, assistContent));
             } else {
-                mActionsProvider.onAssistContent(null);
+                mActionsController.onAssistContent(requestId, null);
             }
         } else {
+            requestId = UUID.randomUUID(); // passed through but unused for legacy UI
             saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher,
                     this::showUiOnActionsReady, this::showUiOnQuickShareActionReady);
         }
@@ -425,7 +428,7 @@
         setWindowFocusable(true);
         mViewProxy.requestFocus();
 
-        enqueueScrollCaptureRequest(screenshot.getUserHandle());
+        enqueueScrollCaptureRequest(requestId, screenshot.getUserHandle());
 
         attachWindow();
 
@@ -586,11 +589,11 @@
         mWindow.setContentView(mViewProxy.getView());
     }
 
-    private void enqueueScrollCaptureRequest(UserHandle owner) {
+    private void enqueueScrollCaptureRequest(UUID requestId, UserHandle owner) {
         // Wait until this window is attached to request because it is
         // the reference used to locate the target window (below).
         withWindowAttached(() -> {
-            requestScrollCapture(owner);
+            requestScrollCapture(requestId, owner);
             mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
                     new ViewRootImpl.ActivityConfigCallback() {
                         @Override
@@ -600,14 +603,14 @@
                                 // Hide the scroll chip until we know it's available in this
                                 // orientation
                                 if (screenshotShelfUi2()) {
-                                    mActionsProvider.onScrollChipInvalidated();
+                                    mActionsController.onScrollChipInvalidated();
                                 } else {
                                     mViewProxy.hideScrollChip();
                                 }
                                 // Delay scroll capture eval a bit to allow the underlying activity
                                 // to set up in the new orientation.
                                 mScreenshotHandler.postDelayed(
-                                        () -> requestScrollCapture(owner), 150);
+                                        () -> requestScrollCapture(requestId, owner), 150);
                                 mViewProxy.updateInsets(
                                         mWindowManager.getCurrentWindowMetrics().getWindowInsets());
                                 // Screenshot animation calculations won't be valid anymore,
@@ -629,7 +632,7 @@
         });
     }
 
-    private void requestScrollCapture(UserHandle owner) {
+    private void requestScrollCapture(UUID requestId, UserHandle owner) {
         mScrollCaptureExecutor.requestScrollCapture(
                 mDisplay.getDisplayId(),
                 mWindow.getDecorView().getWindowToken(),
@@ -637,10 +640,8 @@
                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION,
                             0, response.getPackageName());
                     if (screenshotShelfUi2()) {
-                        if (mActionsProvider != null) {
-                            mActionsProvider.onScrollChipReady(
-                                    () -> onScrollButtonClicked(owner, response));
-                        }
+                        mActionsController.onScrollChipReady(requestId,
+                                () -> onScrollButtonClicked(owner, response));
                     } else {
                         mViewProxy.showScrollChip(response.getPackageName(),
                                 () -> onScrollButtonClicked(owner, response));
@@ -672,7 +673,7 @@
                 () -> {
                     final Intent intent = ActionIntentCreator.INSTANCE.createLongScreenshotIntent(
                             owner, mContext);
-                    mActionIntentExecutor.launchIntentAsync(intent, owner, true, null, null);
+                    mContext.startActivity(intent);
                 },
                 mViewProxy::restoreNonScrollingUi,
                 mViewProxy::startLongScreenshotTransition);
@@ -687,7 +688,7 @@
                     new ViewTreeObserver.OnWindowAttachListener() {
                         @Override
                         public void onWindowAttached() {
-                            mBlockAttach = false;
+                            mAttachRequested = false;
                             decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
                             action.run();
                         }
@@ -703,13 +704,13 @@
     @MainThread
     private void attachWindow() {
         View decorView = mWindow.getDecorView();
-        if (decorView.isAttachedToWindow() || mBlockAttach) {
+        if (decorView.isAttachedToWindow() || mAttachRequested) {
             return;
         }
         if (DEBUG_WINDOW) {
             Log.d(TAG, "attachWindow");
         }
-        mBlockAttach = true;
+        mAttachRequested = true;
         mWindowManager.addView(decorView, mWindowLayoutParams);
         decorView.requestApplyInsets();
 
@@ -727,6 +728,11 @@
                 Log.d(TAG, "Removing screenshot window");
             }
             mWindowManager.removeViewImmediate(decorView);
+            mDetachRequested = false;
+        }
+        if (mAttachRequested && !mDetachRequested) {
+            mDetachRequested = true;
+            withWindowAttached(this::removeWindow);
         }
 
         mViewProxy.stopInputListening();
@@ -826,6 +832,7 @@
     /** Reset screenshot view and then call onCompleteRunnable */
     private void finishDismiss() {
         Log.d(TAG, "finishDismiss");
+        mActionsController.endScreenshotSession();
         mScrollCaptureExecutor.close();
         if (mCurrentRequestCallback != null) {
             mCurrentRequestCallback.onFinish();
@@ -846,9 +853,8 @@
                 ImageExporter.Result result = future.get();
                 Log.d(TAG, "Saved screenshot: " + result);
                 logScreenshotResultStatus(result.uri, screenshot.getUserHandle());
-                mScreenshotHandler.resetTimeout();
                 if (result.uri != null) {
-                    mActionsProvider.setCompletedScreenshot(new ScreenshotSavedResult(
+                    mActionsController.setCompletedScreenshot(requestId, new ScreenshotSavedResult(
                             result.uri, screenshot.getUserOrDefault(), result.timestamp));
                 }
                 if (DEBUG_CALLBACK) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
index 3ac070a..1b5fa34 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -22,8 +22,13 @@
 import android.content.Context
 import android.graphics.Bitmap
 import android.graphics.Rect
+import android.graphics.Region
+import android.os.Looper
+import android.view.Choreographer
+import android.view.InputEvent
 import android.view.KeyEvent
 import android.view.LayoutInflater
+import android.view.MotionEvent
 import android.view.ScrollCaptureResponse
 import android.view.View
 import android.view.ViewTreeObserver
@@ -48,6 +53,8 @@
 import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder
 import com.android.systemui.screenshot.ui.viewmodel.AnimationState
 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import com.android.systemui.shared.system.InputChannelCompat
+import com.android.systemui.shared.system.InputMonitorCompat
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
@@ -91,6 +98,8 @@
     override var isPendingSharedTransition = false
 
     private val animationController = ScreenshotAnimationController(view, viewModel)
+    private var inputMonitor: InputMonitorCompat? = null
+    private var inputEventReceiver: InputChannelCompat.InputEventReceiver? = null
 
     init {
         shelfViewBinder.bind(
@@ -106,20 +115,25 @@
         setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
         debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" }
         view.viewTreeObserver.addOnComputeInternalInsetsListener { info ->
-            val touchableRegion =
-                view.getTouchRegion(
-                    windowManager.currentWindowMetrics.windowInsets.getInsets(
-                        WindowInsets.Type.systemGestures()
-                    )
-                )
             info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION)
-            info.touchableRegion.set(touchableRegion)
+            info.touchableRegion.set(getTouchRegion())
         }
         screenshotPreview = view.screenshotPreview
         thumbnailObserver.setViews(
             view.blurredScreenshotPreview,
             view.requireViewById(R.id.screenshot_preview_border)
         )
+        view.addOnAttachStateChangeListener(
+            object : View.OnAttachStateChangeListener {
+                override fun onViewAttachedToWindow(v: View) {
+                    startInputListening()
+                }
+
+                override fun onViewDetachedFromWindow(v: View) {
+                    stopInputListening()
+                }
+            }
+        )
     }
 
     override fun reset() {
@@ -236,7 +250,12 @@
         callbacks?.onUserInteraction() // reset the timeout
     }
 
-    override fun stopInputListening() {}
+    override fun stopInputListening() {
+        inputMonitor?.dispose()
+        inputMonitor = null
+        inputEventReceiver?.dispose()
+        inputEventReceiver = null
+    }
 
     override fun requestFocus() {
         view.requestFocus()
@@ -303,6 +322,32 @@
         )
     }
 
+    private fun startInputListening() {
+        stopInputListening()
+        inputMonitor =
+            InputMonitorCompat("Screenshot", displayId).also {
+                inputEventReceiver =
+                    it.getInputReceiver(Looper.getMainLooper(), Choreographer.getInstance()) {
+                        ev: InputEvent? ->
+                        if (
+                            ev is MotionEvent &&
+                                ev.actionMasked == MotionEvent.ACTION_DOWN &&
+                                !getTouchRegion().contains(ev.rawX.toInt(), ev.rawY.toInt())
+                        ) {
+                            callbacks?.onTouchOutside()
+                        }
+                    }
+            }
+    }
+
+    private fun getTouchRegion(): Region {
+        return view.getTouchRegion(
+            windowManager.currentWindowMetrics.windowInsets.getInsets(
+                WindowInsets.Type.systemGestures()
+            )
+        )
+    }
+
     @AssistedFactory
     interface Factory : ScreenshotViewProxy.Factory {
         override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index a0c9391..da2024b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -17,9 +17,12 @@
 package com.android.systemui.shade
 
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
 import com.android.systemui.plugins.qs.QSContainerController
 import com.android.systemui.qs.ui.adapter.QSSceneAdapterImpl
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.carrier.ShadeCarrierGroupControllerLog
 import com.android.systemui.shade.data.repository.PrivacyChipRepository
 import com.android.systemui.shade.data.repository.PrivacyChipRepositoryImpl
 import com.android.systemui.shade.data.repository.ShadeRepository
@@ -143,6 +146,13 @@
         fun providesQSContainerController(impl: QSSceneAdapterImpl): QSContainerController {
             return impl
         }
+
+        @Provides
+        @SysUISingleton
+        @ShadeCarrierGroupControllerLog
+        fun provideShadeCarrierLog(factory: LogBufferFactory): LogBuffer {
+            return factory.create("ShadeCarrierGroupControllerLog", 400)
+        }
     }
 
     @Binds
diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
index 4b6dd8d..5e4b173 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
@@ -97,6 +97,8 @@
 
     private final SlotIndexResolver mSlotIndexResolver;
 
+    private final ShadeCarrierGroupControllerLogger mLogger;
+
     private final SignalCallback mSignalCallback = new SignalCallback() {
                 @Override
                 public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) {
@@ -148,6 +150,7 @@
             ActivityStarter activityStarter,
             @Background Handler bgHandler,
             @Main Looper mainLooper,
+            ShadeCarrierGroupControllerLogger logger,
             NetworkController networkController,
             CarrierTextManager.Builder carrierTextManagerBuilder,
             Context context,
@@ -160,6 +163,7 @@
         mContext = context;
         mActivityStarter = activityStarter;
         mBgHandler = bgHandler;
+        mLogger = logger;
         mNetworkController = networkController;
         mStatusBarPipelineFlags = statusBarPipelineFlags;
         mCarrierTextManager = carrierTextManagerBuilder
@@ -374,10 +378,13 @@
             return;
         }
 
+        mLogger.logHandleUpdateCarrierInfo(info);
+
         mNoSimTextView.setVisibility(View.GONE);
         if (!info.airplaneMode && info.anySimReady) {
             boolean[] slotSeen = new boolean[SIM_SLOTS];
             if (info.listOfCarriers.length == info.subscriptionIds.length) {
+                mLogger.logUsingSimViews();
                 for (int i = 0; i < SIM_SLOTS && i < info.listOfCarriers.length; i++) {
                     int slot = getSlotIndex(info.subscriptionIds[i]);
                     if (slot >= SIM_SLOTS) {
@@ -405,9 +412,11 @@
                     }
                 }
             } else {
-                Log.e(TAG, "Carrier information arrays not of same length");
+                mLogger.logInvalidArrayLengths(
+                        info.listOfCarriers.length, info.subscriptionIds.length);
             }
         } else {
+            mLogger.logUsingNoSimView(info.carrierText);
             // No sims or airplane mode (but not WFC). Do not show ShadeCarrierGroup,
             // instead just show info.carrierText in a different view.
             for (int i = 0; i < SIM_SLOTS; i++) {
@@ -458,6 +467,7 @@
         private final ActivityStarter mActivityStarter;
         private final Handler mHandler;
         private final Looper mLooper;
+        private final ShadeCarrierGroupControllerLogger mLogger;
         private final NetworkController mNetworkController;
         private final CarrierTextManager.Builder mCarrierTextControllerBuilder;
         private final Context mContext;
@@ -472,6 +482,7 @@
                 ActivityStarter activityStarter,
                 @Background Handler handler,
                 @Main Looper looper,
+                ShadeCarrierGroupControllerLogger logger,
                 NetworkController networkController,
                 CarrierTextManager.Builder carrierTextControllerBuilder,
                 Context context,
@@ -484,6 +495,7 @@
             mActivityStarter = activityStarter;
             mHandler = handler;
             mLooper = looper;
+            mLogger = logger;
             mNetworkController = networkController;
             mCarrierTextControllerBuilder = carrierTextControllerBuilder;
             mContext = context;
@@ -505,6 +517,7 @@
                     mActivityStarter,
                     mHandler,
                     mLooper,
+                    mLogger,
                     mNetworkController,
                     mCarrierTextControllerBuilder,
                     mContext,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerLog.kt b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerLog.kt
new file mode 100644
index 0000000..36aa7a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerLog.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.carrier
+
+import javax.inject.Qualifier
+
+/** A [LogBuffer] for detailed carrier text logs for [ShadeCarrierGroupController]. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ShadeCarrierGroupControllerLog
diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerLogger.kt
new file mode 100644
index 0000000..af06a35
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerLogger.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.carrier
+
+import com.android.keyguard.CarrierTextManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import javax.inject.Inject
+
+/** Logger for [ShadeCarrierGroupController], mostly to try and solve b/341841138. */
+@SysUISingleton
+class ShadeCarrierGroupControllerLogger
+@Inject
+constructor(@ShadeCarrierGroupControllerLog val buffer: LogBuffer) {
+    /** De-structures the info object so that we don't have to generate new strings */
+    fun logHandleUpdateCarrierInfo(info: CarrierTextManager.CarrierTextCallbackInfo) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            {
+                str1 = "${info.carrierText}"
+                bool1 = info.anySimReady
+                bool2 = info.airplaneMode
+            },
+            {
+                "handleUpdateCarrierInfo: " +
+                    "result=(carrierText=$str1, anySimReady=$bool1, airplaneMode=$bool2)"
+            },
+        )
+    }
+
+    fun logInvalidArrayLengths(numCarriers: Int, numSubs: Int) {
+        buffer.log(
+            TAG,
+            LogLevel.ERROR,
+            {
+                int1 = numCarriers
+                int2 = numSubs
+            },
+            { "â”— carriers.length != subIds.length. carriers.length=$int1 subs.length=$int2" },
+        )
+    }
+
+    fun logUsingNoSimView(text: CharSequence) {
+        buffer.log(
+            TAG,
+            LogLevel.VERBOSE,
+            { str1 = "$text" },
+            { "â”— updating No SIM view with text=$str1" },
+        )
+    }
+
+    fun logUsingSimViews() {
+        buffer.log(TAG, LogLevel.VERBOSE, {}, { "â”— updating SIM views" })
+    }
+
+    private companion object {
+        const val TAG = "SCGC"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FlingInfo.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FlingInfo.kt
index d7f96e6..ea2f9ab 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/FlingInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/FlingInfo.kt
@@ -16,11 +16,16 @@
 
 package com.android.systemui.shade.data.repository
 
+import java.util.UUID
+
 /**
  * Information about a fling on the shade: whether we're flinging expanded or collapsed, and the
  * velocity of the touch gesture that started the fling (if applicable).
  */
-data class FlingInfo(
+data class FlingInfo @JvmOverloads constructor(
     val expand: Boolean,
     val velocity: Float = 0f,
+
+    /** Required to emit duplicate FlingInfo from StateFlow. */
+    val id: UUID = UUID.randomUUID()
 )
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
index 53c10a3..fe16fc0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
@@ -49,11 +49,13 @@
     sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
     shadeRepository: ShadeRepository,
 ) : BaseShadeInteractor {
+    override val shadeMode: StateFlow<ShadeMode> = shadeRepository.shadeMode
+
     override val shadeExpansion: StateFlow<Float> =
-        sceneBasedExpansion(sceneInteractor, Scenes.Shade)
+        sceneBasedExpansion(sceneInteractor, notificationsScene)
             .stateIn(scope, SharingStarted.Eagerly, 0f)
 
-    private val sceneBasedQsExpansion = sceneBasedExpansion(sceneInteractor, Scenes.QuickSettings)
+    private val sceneBasedQsExpansion = sceneBasedExpansion(sceneInteractor, quickSettingsScene)
 
     override val qsExpansion: StateFlow<Float> =
         combine(
@@ -81,7 +83,7 @@
                 when (state) {
                     is ObservableTransitionState.Idle -> false
                     is ObservableTransitionState.Transition ->
-                        state.toScene == Scenes.QuickSettings && state.fromScene != Scenes.Shade
+                        state.toScene == quickSettingsScene && state.fromScene != notificationsScene
                 }
             }
             .distinctUntilChanged()
@@ -90,7 +92,7 @@
         sceneInteractor.transitionState
             .map { state ->
                 when (state) {
-                    is ObservableTransitionState.Idle -> state.currentScene == Scenes.QuickSettings
+                    is ObservableTransitionState.Idle -> state.currentScene == quickSettingsScene
                     is ObservableTransitionState.Transition -> false
                 }
             }
@@ -106,12 +108,10 @@
             .stateIn(scope, SharingStarted.Eagerly, false)
 
     override val isUserInteractingWithShade: Flow<Boolean> =
-        sceneBasedInteracting(sceneInteractor, Scenes.Shade)
+        sceneBasedInteracting(sceneInteractor, notificationsScene)
 
     override val isUserInteractingWithQs: Flow<Boolean> =
-        sceneBasedInteracting(sceneInteractor, Scenes.QuickSettings)
-
-    override val shadeMode: StateFlow<ShadeMode> = shadeRepository.shadeMode
+        sceneBasedInteracting(sceneInteractor, quickSettingsScene)
 
     /**
      * Returns a flow that uses scene transition progress to and from a scene that is pulled down
@@ -154,4 +154,20 @@
                 }
             }
             .distinctUntilChanged()
+
+    private val notificationsScene: SceneKey
+        get() =
+            if (shadeMode.value is ShadeMode.Dual) {
+                Scenes.NotificationsShade
+            } else {
+                Scenes.Shade
+            }
+
+    private val quickSettingsScene: SceneKey
+        get() =
+            if (shadeMode.value is ShadeMode.Dual) {
+                Scenes.QuickSettingsShade
+            } else {
+                Scenes.QuickSettings
+            }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 8d8a36a..47939ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -98,6 +98,8 @@
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor;
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardIndication;
@@ -183,11 +185,14 @@
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private KeyguardInteractor mKeyguardInteractor;
     private final BiometricMessageInteractor mBiometricMessageInteractor;
+    private DeviceEntryFingerprintAuthInteractor mDeviceEntryFingerprintAuthInteractor;
+    private DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor;
     private String mPersistentUnlockMessage;
     private String mAlignmentIndication;
     private boolean mForceIsDismissible;
     private CharSequence mTrustGrantedIndication;
     private CharSequence mTransientIndication;
+    private CharSequence mTrustAgentErrorMessage;
     private CharSequence mBiometricMessage;
     private CharSequence mBiometricMessageFollowUp;
     private BiometricSourceType mBiometricMessageSource;
@@ -235,6 +240,13 @@
     final Consumer<Set<Integer>> mCoExAcquisitionMsgIdsToShowCallback =
             (Set<Integer> coExFaceAcquisitionMsgIdsToShow) -> mCoExFaceAcquisitionMsgIdsToShow =
                     coExFaceAcquisitionMsgIdsToShow;
+    @VisibleForTesting
+    final Consumer<Boolean> mIsFingerprintEngagedCallback =
+            (Boolean isEngaged) -> {
+                if (!isEngaged) {
+                    showTrustAgentErrorMessage(mTrustAgentErrorMessage);
+                }
+            };
     private final ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() {
         @Override
         public void onScreenTurnedOn() {
@@ -295,7 +307,9 @@
             FeatureFlags flags,
             IndicationHelper indicationHelper,
             KeyguardInteractor keyguardInteractor,
-            BiometricMessageInteractor biometricMessageInteractor
+            BiometricMessageInteractor biometricMessageInteractor,
+            DeviceEntryFingerprintAuthInteractor deviceEntryFingerprintAuthInteractor,
+            DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor
     ) {
         mContext = context;
         mBroadcastDispatcher = broadcastDispatcher;
@@ -325,6 +339,8 @@
         mIndicationHelper = indicationHelper;
         mKeyguardInteractor = keyguardInteractor;
         mBiometricMessageInteractor = biometricMessageInteractor;
+        mDeviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor;
+        mDeviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor;
 
         mFaceAcquiredMessageDeferral = faceHelpMessageDeferral.create();
 
@@ -409,6 +425,8 @@
         collectFlow(mIndicationArea,
                 mBiometricMessageInteractor.getCoExFaceAcquisitionMsgIdsToShow(),
                 mCoExAcquisitionMsgIdsToShowCallback);
+        collectFlow(mIndicationArea, mDeviceEntryFingerprintAuthInteractor.isEngaged(),
+                mIsFingerprintEngagedCallback);
     }
 
     /**
@@ -944,19 +962,25 @@
 
         if (!isSuccessMessage
                 && mBiometricMessageSource == FINGERPRINT
-                && biometricSourceType != FINGERPRINT) {
-            // drop all non-fingerprint biometric messages if there's a fingerprint message showing
-            mKeyguardLogger.logDropNonFingerprintMessage(
+                && biometricSourceType == FACE) {
+            // drop any face messages if there's a fingerprint message showing
+            mKeyguardLogger.logDropFaceMessage(
                     biometricMessage,
-                    biometricMessageFollowUp,
-                    biometricSourceType
+                    biometricMessageFollowUp
             );
             return;
         }
 
-        mBiometricMessage = biometricMessage;
-        mBiometricMessageFollowUp = biometricMessageFollowUp;
-        mBiometricMessageSource = biometricSourceType;
+        if (mBiometricMessageSource != null && biometricSourceType == null) {
+            // If there's a current biometric message showing and a non-biometric message
+            // arrives, update the followup message with the non-biometric message.
+            // Keep the biometricMessage and biometricMessageSource the same.
+            mBiometricMessageFollowUp = biometricMessage;
+        } else {
+            mBiometricMessage = biometricMessage;
+            mBiometricMessageFollowUp = biometricMessageFollowUp;
+            mBiometricMessageSource = biometricSourceType;
+        }
 
         mHandler.removeMessages(MSG_SHOW_ACTION_TO_UNLOCK);
         hideBiometricMessageDelayed(
@@ -1455,7 +1479,7 @@
 
         @Override
         public void onTrustAgentErrorMessage(CharSequence message) {
-            showBiometricMessage(message, null);
+            showTrustAgentErrorMessage(message);
         }
 
         @Override
@@ -1467,6 +1491,10 @@
                 hideBiometricMessage();
                 mBiometricErrorMessageToShowOnScreenOn = null;
             }
+
+            if (!running && biometricSourceType == FACE) {
+                showTrustAgentErrorMessage(mTrustAgentErrorMessage);
+            }
         }
 
         @Override
@@ -1533,6 +1561,25 @@
         return getCurrentUser() == userId;
     }
 
+    /**
+     * Only show trust agent messages after biometrics are no longer active.
+     */
+    private void showTrustAgentErrorMessage(CharSequence message) {
+        if (message == null) {
+            mTrustAgentErrorMessage = null;
+            return;
+        }
+        boolean fpEngaged = mDeviceEntryFingerprintAuthInteractor.isEngaged().getValue();
+        boolean faceRunning = mDeviceEntryFaceAuthInteractor.isRunning();
+        if (fpEngaged || faceRunning) {
+            mKeyguardLogger.delayShowingTrustAgentError(message, fpEngaged, faceRunning);
+            mTrustAgentErrorMessage = message;
+        } else {
+            mTrustAgentErrorMessage = null;
+            showBiometricMessage(message, null);
+        }
+    }
+
     protected void showTrustGrantedMessage(boolean dismissKeyguard, @Nullable String message) {
         mTrustGrantedIndication = message;
         updateDeviceEntryIndication(false);
@@ -1639,6 +1686,7 @@
             new KeyguardStateController.Callback() {
         @Override
         public void onUnlockedChanged() {
+            mTrustAgentErrorMessage = null;
             updateDeviceEntryIndication(false);
         }
 
@@ -1649,6 +1697,7 @@
                 mKeyguardLogger.log(TAG, LogLevel.DEBUG, "clear messages");
                 mTopIndicationView.clearMessages();
                 mRotateTextViewController.clearMessages();
+                mTrustAgentErrorMessage = null;
             } else {
                 updateDeviceEntryIndication(false);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 5bf2f41..03c6670 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -15,7 +15,7 @@
  */
 package com.android.systemui.statusbar;
 
-import static com.android.systemui.Flags.mediaControlsUserInitiatedDismiss;
+import static com.android.systemui.Flags.mediaControlsUserInitiatedDeleteintent;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -178,7 +178,7 @@
 
             @Override
             public void onMediaDataRemoved(@NonNull String key, boolean userInitiated) {
-                if (mediaControlsUserInitiatedDismiss() && !userInitiated) {
+                if (mediaControlsUserInitiatedDeleteintent() && !userInitiated) {
                     // Dismissing the notification will send the app's deleteIntent, so ignore if
                     // this was an automatic removal
                     Log.d(TAG, "Not dismissing " + key + " because it was removed by the system");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt
index e85df0e..c57cf69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractor.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor
-import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt
index 70362c8..c3d37fb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.chips.domain.interactor
 
-import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
 import kotlinx.coroutines.flow.StateFlow
 
 /** Interface for an interactor that knows the state of a single type of ongoing activity chip. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/model/OngoingActivityChipModel.kt
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/model/OngoingActivityChipModel.kt
index e63713b..c753918 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/model/OngoingActivityChipModel.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.chips.ui.model
+package com.android.systemui.statusbar.chips.domain.model
 
 import android.view.View
 import com.android.systemui.common.shared.model.Icon
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
index 6f16969..bff5686 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
@@ -16,17 +16,51 @@
 
 package com.android.systemui.statusbar.chips.screenrecord.domain.interactor
 
+import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel
+import com.android.systemui.screenrecord.data.repository.ScreenRecordRepository
 import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor
+import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 
 /** Interactor for the screen recording chip shown in the status bar. */
 @SysUISingleton
-open class ScreenRecordChipInteractor @Inject constructor() : OngoingActivityChipInteractor {
-    // TODO(b/332662551): Implement this flow.
+open class ScreenRecordChipInteractor
+@Inject
+constructor(
+    @Application scope: CoroutineScope,
+    screenRecordRepository: ScreenRecordRepository,
+    val systemClock: SystemClock,
+) : OngoingActivityChipInteractor {
     override val chip: StateFlow<OngoingActivityChipModel> =
-        MutableStateFlow(OngoingActivityChipModel.Hidden)
+        screenRecordRepository.screenRecordState
+            .map { state ->
+                when (state) {
+                    is ScreenRecordModel.DoingNothing,
+                    // TODO(b/332662551): Implement the 3-2-1 countdown chip.
+                    is ScreenRecordModel.Starting -> OngoingActivityChipModel.Hidden
+                    is ScreenRecordModel.Recording ->
+                        OngoingActivityChipModel.Shown(
+                            // TODO(b/332662551): Also provide a content description.
+                            icon =
+                                Icon.Resource(
+                                    R.drawable.stat_sys_screen_record,
+                                    contentDescription = null
+                                ),
+                            startTimeMs = systemClock.elapsedRealtime()
+                        ) {
+                            // TODO(b/332662551): Implement the pause dialog.
+                        }
+                }
+            }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 47b2b03..208eb50 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -19,8 +19,8 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor
+import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
-import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt
index 8505c5f..0d5ade7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/GesturePointerEventListener.kt
@@ -98,10 +98,10 @@
             return
         }
         val r = mContext.resources
-        val defaultThreshold = r.getDimensionPixelSize(R.dimen.system_gestures_start_threshold)
-        mSwipeStartThreshold[defaultThreshold, defaultThreshold, defaultThreshold] =
-            defaultThreshold
-        mSwipeDistanceThreshold = defaultThreshold
+        val startThreshold = r.getDimensionPixelSize(R.dimen.system_gestures_start_threshold)
+        mSwipeStartThreshold[startThreshold, startThreshold, startThreshold] = startThreshold
+        mSwipeDistanceThreshold =
+            r.getDimensionPixelSize(R.dimen.system_gestures_distance_threshold)
         val display = DisplayManagerGlobal.getInstance().getRealDisplay(mContext.displayId)
         val displayCutout = display.cutout
         if (displayCutout != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
index 2fd0a53..0ece88d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/gesture/SwipeUpGestureHandler.kt
@@ -46,7 +46,7 @@
     private var monitoringCurrentTouch: Boolean = false
 
     private var swipeDistanceThreshold: Int = context.resources.getDimensionPixelSize(
-        com.android.internal.R.dimen.system_gestures_start_threshold
+        com.android.internal.R.dimen.system_gestures_distance_threshold
     )
 
     override fun onInputEvent(ev: InputEvent) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 87f11f13..938a71f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -48,6 +48,9 @@
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.settings.SystemSettings
 import com.android.systemui.util.time.SystemClock
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
+import kotlin.jvm.optionals.getOrElse
 
 class PeekDisabledSuppressor(
     private val globalSettings: GlobalSettings,
@@ -119,12 +122,18 @@
         }
 }
 
-class PeekAlreadyBubbledSuppressor(private val statusBarStateController: StatusBarStateController) :
-    VisualInterruptionFilter(types = setOf(PEEK), reason = "already bubbled") {
+class PeekAlreadyBubbledSuppressor(
+    private val statusBarStateController: StatusBarStateController,
+    private val bubbles: Optional<Bubbles>
+) : VisualInterruptionFilter(types = setOf(PEEK), reason = "already bubbled") {
     override fun shouldSuppress(entry: NotificationEntry) =
         when {
             statusBarStateController.state != SHADE -> false
-            else -> entry.isBubble
+            else -> {
+                val bubblesCanShowNotification =
+                    bubbles.map { it.canShowBubbleNotification() }.getOrElse { false }
+                entry.isBubble && bubblesCanShowNotification
+            }
         }
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index 9c6a423..74925c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -52,9 +52,11 @@
 import com.android.systemui.util.EventLog;
 import com.android.systemui.util.settings.GlobalSettings;
 import com.android.systemui.util.time.SystemClock;
+import com.android.wm.shell.bubbles.Bubbles;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 
 import javax.inject.Inject;
 
@@ -83,6 +85,7 @@
     private final SystemClock mSystemClock;
     private final GlobalSettings mGlobalSettings;
     private final EventLog mEventLog;
+    private final Optional<Bubbles> mBubbles;
 
     @VisibleForTesting
     protected boolean mUseHeadsUp = false;
@@ -132,7 +135,8 @@
             DeviceProvisionedController deviceProvisionedController,
             SystemClock systemClock,
             GlobalSettings globalSettings,
-            EventLog eventLog) {
+            EventLog eventLog,
+            Optional<Bubbles> bubbles) {
         mPowerManager = powerManager;
         mBatteryController = batteryController;
         mAmbientDisplayConfiguration = ambientDisplayConfiguration;
@@ -148,6 +152,7 @@
         mSystemClock = systemClock;
         mGlobalSettings = globalSettings;
         mEventLog = eventLog;
+        mBubbles = bubbles;
         ContentObserver headsUpObserver = new ContentObserver(mainHandler) {
             @Override
             public void onChange(boolean selfChange) {
@@ -440,7 +445,9 @@
         }
 
         boolean inShade = mStatusBarStateController.getState() == SHADE;
-        if (entry.isBubble() && inShade) {
+        boolean bubblesCanShowNotification =
+                mBubbles.isPresent() && mBubbles.get().canShowBubbleNotification();
+        if (entry.isBubble() && inShade && bubblesCanShowNotification) {
             if (log) mLogger.logNoHeadsUpAlreadyBubbled(entry);
             return false;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index f68e194..7e16cd5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -43,6 +43,8 @@
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.settings.SystemSettings
 import com.android.systemui.util.time.SystemClock
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
 import javax.inject.Inject
 
 class VisualInterruptionDecisionProviderImpl
@@ -65,7 +67,8 @@
     private val userTracker: UserTracker,
     private val avalancheProvider: AvalancheProvider,
     private val systemSettings: SystemSettings,
-    private val packageManager: PackageManager
+    private val packageManager: PackageManager,
+    private val bubbles: Optional<Bubbles>
 ) : VisualInterruptionDecisionProvider {
 
     init {
@@ -158,7 +161,7 @@
         addCondition(PulseDisabledSuppressor(ambientDisplayConfiguration, userTracker))
         addCondition(PulseBatterySaverSuppressor(batteryController))
         addFilter(PeekPackageSnoozedSuppressor(headsUpManager))
-        addFilter(PeekAlreadyBubbledSuppressor(statusBarStateController))
+        addFilter(PeekAlreadyBubbledSuppressor(statusBarStateController, bubbles))
         addFilter(PeekDndSuppressor())
         addFilter(PeekNotImportantSuppressor())
         addCondition(PeekDeviceNotInUseSuppressor(powerManager, statusBarStateController))
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 6137381..c2ce114 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.statusbar.notification.stack.ui.viewmodel
 
 import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -25,6 +26,7 @@
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
 import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping
 import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
@@ -34,6 +36,7 @@
 import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
@@ -73,16 +76,16 @@
                     }
                     is ObservableTransitionState.Transition -> {
                         if (
-                            (transitionState.fromScene == Scenes.Shade &&
-                                transitionState.toScene == Scenes.QuickSettings) ||
-                                (transitionState.fromScene == Scenes.QuickSettings &&
-                                    transitionState.toScene == Scenes.Shade)
+                            (transitionState.fromScene == notificationsScene &&
+                                transitionState.toScene == quickSettingsScene) ||
+                                (transitionState.fromScene == quickSettingsScene &&
+                                    transitionState.toScene == notificationsScene)
                         ) {
                             1f
                         } else if (
                             (transitionState.fromScene == Scenes.Gone ||
                                 transitionState.fromScene == Scenes.Lockscreen) &&
-                                transitionState.toScene == Scenes.QuickSettings
+                                transitionState.toScene == quickSettingsScene
                         ) {
                             // during QS expansion, increase fraction at same rate as scrim alpha,
                             // but start when scrim alpha is at EXPANSION_FOR_DELAYED_STACK_FADE_IN.
@@ -152,7 +155,9 @@
 
     /** Whether the notification stack is scrollable or not. */
     val isScrollable: Flow<Boolean> =
-        sceneInteractor.currentScene.map { it == Scenes.Shade }.dumpWhileCollecting("isScrollable")
+        sceneInteractor.currentScene
+            .map { it == notificationsScene }
+            .dumpWhileCollecting("isScrollable")
 
     /** Whether the notification stack is displayed in doze mode. */
     val isDozing: Flow<Boolean> by lazy {
@@ -162,4 +167,22 @@
             keyguardInteractor.get().isDozing.dumpWhileCollecting("isDozing")
         }
     }
+
+    private val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode
+
+    private val notificationsScene: SceneKey
+        get() =
+            if (shadeMode.value is ShadeMode.Dual) {
+                Scenes.NotificationsShade
+            } else {
+                Scenes.Shade
+            }
+
+    private val quickSettingsScene: SceneKey
+        get() =
+            if (shadeMode.value is ShadeMode.Dual) {
+                Scenes.QuickSettingsShade
+            } else {
+                Scenes.QuickSettings
+            }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DialogDelegate.kt
index 25d1f05..2beb66b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DialogDelegate.kt
@@ -19,7 +19,10 @@
 import android.app.Dialog
 import android.content.res.Configuration
 import android.os.Bundle
+import android.util.DisplayMetrics
 import android.view.ViewRootImpl
+import com.android.systemui.animation.back.BackAnimationSpec
+import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi
 
 /**
  * A delegate class that should be implemented in place of subclassing [Dialog].
@@ -49,4 +52,7 @@
     fun getWidth(dialog: T): Int = SystemUIDialog.getDefaultDialogWidth(dialog)
 
     fun getHeight(dialog: T): Int = SystemUIDialog.getDefaultDialogHeight()
+
+    fun getBackAnimationSpec(displayMetricsProvider: () -> DisplayMetrics): BackAnimationSpec =
+        BackAnimationSpec.floatingSystemSurfacesForSysUi(displayMetricsProvider)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index 3063aed..77f3706 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -54,13 +54,13 @@
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
 import com.android.systemui.util.Assert;
+import com.android.systemui.util.CopyOnLoopListenerSet;
+import com.android.systemui.util.IListenerSet;
 
 import dagger.Lazy;
 
 import kotlinx.coroutines.ExperimentalCoroutinesApi;
 
-import java.util.ArrayList;
-
 import javax.inject.Inject;
 
 /**
@@ -69,7 +69,7 @@
 @ExperimentalCoroutinesApi @SysUISingleton
 public final class DozeServiceHost implements DozeHost {
     private static final String TAG = "DozeServiceHost";
-    private final ArrayList<Callback> mCallbacks = new ArrayList<>();
+    private final IListenerSet<Callback> mCallbacks = new CopyOnLoopListenerSet<>();
     private final DozeLog mDozeLog;
     private final PowerManager mPowerManager;
     private boolean mAnimateWakeup;
@@ -178,8 +178,8 @@
      */
     public void fireSideFpsAcquisitionStarted() {
         Assert.isMainThread();
-        for (int i = 0; i < mCallbacks.size(); i++) {
-            mCallbacks.get(i).onSideFingerprintAcquisitionStarted();
+        for (Callback callback : mCallbacks) {
+            callback.onSideFingerprintAcquisitionStarted();
         }
     }
 
@@ -211,7 +211,7 @@
     @Override
     public void addCallback(@NonNull Callback callback) {
         Assert.isMainThread();
-        mCallbacks.add(callback);
+        mCallbacks.addIfAbsent(callback);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
index 11feb97..f99a81e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceController.java
@@ -159,7 +159,7 @@
             public void onLayoutChange(View v, int left, int top, int right, int bottom,
                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
                 if (shouldBeVisible()) {
-                    updateTopEntry();
+                    updateTopEntry("onLayoutChange");
 
                     // trigger scroller to notify the latest panel translation
                     mStackScrollerController.requestLayout();
@@ -220,7 +220,7 @@
 
     @Override
     public void onHeadsUpPinned(NotificationEntry entry) {
-        updateTopEntry();
+        updateTopEntry("onHeadsUpPinned");
         updateHeader(entry);
         updateHeadsUpAndPulsingRoundness(entry);
     }
@@ -231,7 +231,7 @@
         mPhoneStatusBarTransitions.onHeadsUpStateChanged(isHeadsUp);
     }
 
-    private void updateTopEntry() {
+    private void updateTopEntry(String reason) {
         NotificationEntry newEntry = null;
         if (shouldBeVisible()) {
             newEntry = mHeadsUpManager.getTopEntry();
@@ -370,7 +370,7 @@
 
     @Override
     public void onHeadsUpUnPinned(NotificationEntry entry) {
-        updateTopEntry();
+        updateTopEntry("onHeadsUpUnPinned");
         updateHeader(entry);
         updateHeadsUpAndPulsingRoundness(entry);
     }
@@ -388,7 +388,7 @@
             updateHeadsUpHeaders();
         }
         if (isExpanded() != oldIsExpanded) {
-            updateTopEntry();
+            updateTopEntry("setAppearFraction");
         }
     }
 
@@ -462,11 +462,11 @@
     }
 
     public void onStateChanged() {
-        updateTopEntry();
+        updateTopEntry("onStateChanged");
     }
 
     @Override
     public void onFullyHiddenChanged(boolean isFullyHidden) {
-        updateTopEntry();
+        updateTopEntry("onFullyHiddenChanged");
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
index 68457ea..ffc859e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java
@@ -249,7 +249,7 @@
             for (NotificationEntry entry : mEntriesToRemoveAfterExpand) {
                 if (isHeadsUpEntry(entry.getKey())) {
                     // Maybe the heads-up was removed already
-                    removeEntry(entry.getKey());
+                    removeEntry(entry.getKey(), "onExpandingFinished");
                 }
             }
         }
@@ -381,7 +381,7 @@
         for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
             if (isHeadsUpEntry(entry.getKey())) {
                 // Maybe the heads-up was removed already
-                removeEntry(entry.getKey());
+                removeEntry(entry.getKey(), "mOnReorderingAllowedListener");
             }
         }
         mEntriesToRemoveWhenReorderingAllowed.clear();
@@ -572,7 +572,7 @@
                 } else if (mTrackingHeadsUp) {
                     mEntriesToRemoveAfterExpand.add(entry);
                 } else {
-                    removeEntry(entry.getKey());
+                    removeEntry(entry.getKey(), "createRemoveRunnable");
                 }
             };
         }
@@ -661,7 +661,7 @@
                     }
                 }
                 for (String key : keysToRemove) {
-                    removeEntry(key);
+                    removeEntry(key, "mStatusBarStateListener");
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index f767262..6d4301f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -660,10 +660,8 @@
      * whether heads up is visible.
      */
     public void updateForHeadsUp() {
-        if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) {
-            // [KeyguardStatusBarViewBinder] handles visibility changes due to heads up states.
-            return;
-        }
+        // [KeyguardStatusBarViewBinder] handles visibility when SceneContainerFlag is on.
+        SceneContainerFlag.assertInLegacyMode();
         updateForHeadsUp(true);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index c74dde5..e01556f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -269,9 +269,12 @@
             mOnCreateRunnables.get(i).run();
         }
         if (predictiveBackAnimateDialogs()) {
+            View targetView = getWindow().getDecorView();
             DialogKt.registerAnimationOnBackInvoked(
                     /* dialog = */ this,
-                    /* targetView = */ getWindow().getDecorView()
+                    /* targetView = */ targetView,
+                    /* backAnimationSpec= */mDelegate.getBackAnimationSpec(
+                            () -> targetView.getResources().getDisplayMetrics())
             );
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index 12f252d..9d9cc2a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.pipeline.satellite.data.prod
 
 import android.os.OutcomeReceiver
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyManager
 import android.telephony.satellite.NtnSignalStrengthCallback
 import android.telephony.satellite.SatelliteManager
 import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS
@@ -38,6 +40,7 @@
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Unknown
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
 import com.android.systemui.util.kotlin.getOrNull
+import com.android.systemui.util.kotlin.pairwise
 import com.android.systemui.util.time.SystemClock
 import java.util.Optional
 import javax.inject.Inject
@@ -51,12 +54,15 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
@@ -92,13 +98,19 @@
 
     @OptIn(ExperimentalCoroutinesApi::class)
     companion object {
-        /** Convenience function to switch to the supported flow */
+        /**
+         * Convenience function to switch to the supported flow. [retrySignal] is a flow that emits
+         * [Unit] whenever the [supported] flow needs to be restarted
+         */
         fun <T> Flow<SatelliteSupport>.whenSupported(
             supported: (SatelliteManager) -> Flow<T>,
             orElse: Flow<T>,
-        ): Flow<T> = flatMapLatest {
-            when (it) {
-                is Supported -> supported(it.satelliteManager)
+            retrySignal: Flow<Unit>,
+        ): Flow<T> = flatMapLatest { satelliteSupport ->
+            when (satelliteSupport) {
+                is Supported -> {
+                    retrySignal.flatMapLatest { supported(satelliteSupport.satelliteManager) }
+                }
                 else -> orElse
             }
         }
@@ -132,6 +144,7 @@
 @Inject
 constructor(
     satelliteManagerOpt: Optional<SatelliteManager>,
+    telephonyManager: TelephonyManager,
     @Background private val bgDispatcher: CoroutineDispatcher,
     @Application private val scope: CoroutineScope,
     @OemSatelliteInputLog private val logBuffer: LogBuffer,
@@ -201,11 +214,65 @@
         }
     }
 
+    /**
+     * Note that we are given an "unbound" [TelephonyManager] (meaning it was not created with a
+     * specific `subscriptionId`). Therefore this is the radio power state of the
+     * DEFAULT_SUBSCRIPTION_ID subscription. This subscription, I am led to believe, is the one that
+     * would be used for the SatelliteManager subscription.
+     *
+     * By watching power state changes, we can detect if the telephony process crashes.
+     *
+     * See b/337258696 for details
+     */
+    private val radioPowerState: StateFlow<Int> =
+        conflatedCallbackFlow {
+                val cb =
+                    object : TelephonyCallback(), TelephonyCallback.RadioPowerStateListener {
+                        override fun onRadioPowerStateChanged(powerState: Int) {
+                            trySend(powerState)
+                        }
+                    }
+
+                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), cb)
+
+                awaitClose { telephonyManager.unregisterTelephonyCallback(cb) }
+            }
+            .flowOn(bgDispatcher)
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                TelephonyManager.RADIO_POWER_UNAVAILABLE
+            )
+
+    /**
+     * In the event that a telephony phone process has crashed, we expect to see a radio power state
+     * change from ON to something else. This trigger can be used to re-start a flow via
+     * [whenSupported]
+     *
+     * This flow emits [Unit] when started so that newly-started collectors always run, and only
+     * restart when the state goes from ON -> !ON
+     */
+    private val telephonyProcessCrashedEvent: Flow<Unit> =
+        radioPowerState
+            .pairwise()
+            .mapNotNull { (prev: Int, new: Int) ->
+                if (
+                    prev == TelephonyManager.RADIO_POWER_ON &&
+                        new != TelephonyManager.RADIO_POWER_ON
+                ) {
+                    Unit
+                } else {
+                    null
+                }
+            }
+            .onStart { emit(Unit) }
+
     override val connectionState =
         satelliteSupport
             .whenSupported(
                 supported = ::connectionStateFlow,
-                orElse = flowOf(SatelliteConnectionState.Off)
+                orElse = flowOf(SatelliteConnectionState.Off),
+                retrySignal = telephonyProcessCrashedEvent,
             )
             .stateIn(scope, SharingStarted.Eagerly, SatelliteConnectionState.Off)
 
@@ -232,7 +299,11 @@
 
     override val signalStrength =
         satelliteSupport
-            .whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0))
+            .whenSupported(
+                supported = ::signalStrengthFlow,
+                orElse = flowOf(0),
+                retrySignal = telephonyProcessCrashedEvent,
+            )
             .stateIn(scope, SharingStarted.Eagerly, 0)
 
     // By using the SupportedSatelliteManager here, we expect registration never to fail
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
index 332c121..d76fd40 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
@@ -39,6 +40,7 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
 /**
@@ -60,6 +62,7 @@
 }
 
 @OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
 class DeviceBasedSatelliteViewModelImpl
 @Inject
 constructor(
@@ -124,18 +127,37 @@
                 shouldActuallyShowIcon,
                 interactor.connectionState,
             ) { shouldShow, connectionState ->
+                logBuffer.log(
+                    TAG,
+                    LogLevel.INFO,
+                    {
+                        bool1 = shouldShow
+                        str1 = connectionState.name
+                    },
+                    { "Updating carrier text. shouldActuallyShow=$bool1 connectionState=$str1" }
+                )
                 if (shouldShow) {
                     when (connectionState) {
                         SatelliteConnectionState.On,
                         SatelliteConnectionState.Connected ->
                             context.getString(R.string.satellite_connected_carrier_text)
                         SatelliteConnectionState.Off,
-                        SatelliteConnectionState.Unknown -> null
+                        SatelliteConnectionState.Unknown -> {
+                            null
+                        }
                     }
                 } else {
                     null
                 }
             }
+            .onEach {
+                logBuffer.log(
+                    TAG,
+                    LogLevel.INFO,
+                    { str1 = it },
+                    { "Resulting carrier text = $str1" }
+                )
+            }
             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index 8b48bd3..2169154 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -79,7 +79,8 @@
             runnable.run()
             return
         }
-        val fn = "[$label] => AvalancheController.update [${getKey(entry)}]"
+        log { "\n "}
+        val fn = "$label => AvalancheController.update ${getKey(entry)}"
         if (entry == null) {
             log { "Entry is NULL, stop update." }
             return
@@ -88,13 +89,13 @@
             debugRunnableLabelMap[runnable] = label
         }
         if (isShowing(entry)) {
-            log { "\n$fn => [update showing]" }
+            log { "\n$fn => update showing" }
             runnable.run()
         } else if (entry in nextMap) {
-            log { "\n$fn => [update next]" }
+            log { "\n$fn => update next" }
             nextMap[entry]?.add(runnable)
         } else if (headsUpEntryShowing == null) {
-            log { "\n$fn => [showNow]" }
+            log { "\n$fn => showNow" }
             showNow(entry, arrayListOf(runnable))
         } else {
             // Clean up invalid state when entry is in list but not map and vice versa
@@ -133,20 +134,22 @@
             runnable.run()
             return
         }
-        val fn = "[$label] => AvalancheController.delete " + getKey(entry)
+        log { "\n "}
+        val fn = "$label => AvalancheController.delete " + getKey(entry)
         if (entry == null) {
-            log { "$fn => cannot remove NULL entry" }
+            log { "$fn => entry NULL, running runnable" }
+            runnable.run()
             return
         }
         if (entry in nextMap) {
-            log { "$fn => [remove from next]" }
+            log { "$fn => remove from next" }
             if (entry in nextMap) nextMap.remove(entry)
             if (entry in nextList) nextList.remove(entry)
         } else if (entry in debugDropSet) {
-            log { "$fn => [remove from dropset]" }
+            log { "$fn => remove from dropset" }
             debugDropSet.remove(entry)
         } else if (isShowing(entry)) {
-            log { "$fn => [remove showing ${getKey(entry)}]" }
+            log { "$fn => remove showing ${getKey(entry)}" }
             previousHunKey = getKey(headsUpEntryShowing)
             // Show the next HUN before removing this one, so that we don't tell listeners
             // onHeadsUpPinnedModeChanged, which causes
@@ -155,7 +158,7 @@
             showNext()
             runnable.run()
         } else {
-            log { "$fn => [removing untracked ${getKey(entry)}]" }
+            log { "$fn => removing untracked ${getKey(entry)}" }
         }
         logState("after $fn")
     }
@@ -239,6 +242,18 @@
         return keyList
     }
 
+    fun getWaitingEntry(key: String): HeadsUpEntry? {
+        if (!NotificationThrottleHun.isEnabled) {
+            return null
+        }
+        for (headsUpEntry in nextMap.keys) {
+            if (headsUpEntry.mEntry?.key.equals(key)) {
+                return headsUpEntry
+            }
+        }
+        return null
+    }
+
     private fun isShowing(entry: HeadsUpEntry): Boolean {
         return headsUpEntryShowing != null && entry.mEntry?.key == headsUpEntryShowing?.mEntry?.key
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index a7fe49b..2ee98bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -195,26 +195,29 @@
      */
     @Override
     public boolean removeNotification(@NonNull String key, boolean releaseImmediately) {
-        mLogger.logRemoveNotification(key, releaseImmediately);
+        final boolean isWaiting = mAvalancheController.isWaiting(key);
+        mLogger.logRemoveNotification(key, releaseImmediately, isWaiting);
 
         if (mAvalancheController.isWaiting(key)) {
-            removeEntry(key);
+            removeEntry(key, "removeNotification (isWaiting)");
             return true;
         }
         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
         if (headsUpEntry == null) {
             return true;
         }
-        if (releaseImmediately || canRemoveImmediately(key)) {
-            removeEntry(key);
-        } else {
-            headsUpEntry.removeAsSoonAsPossible();
-            return false;
+        if (releaseImmediately) {
+            removeEntry(key, "removeNotification (releaseImmediately)");
+            return true;
         }
-        return true;
+        if (canRemoveImmediately(key)) {
+            removeEntry(key, "removeNotification (canRemoveImmediately)");
+            return true;
+        }
+        headsUpEntry.removeAsSoonAsPossible();
+        return false;
     }
 
-
     /**
      * Called when the notification state has been updated.
      * @param key the key of the entry that was updated
@@ -245,7 +248,8 @@
         if (shouldHeadsUpAgain) {
             headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification");
             if (headsUpEntry != null) {
-                setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry));
+                setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry),
+                        "updateNotificationInternal");
             }
         }
     }
@@ -264,10 +268,10 @@
         List<String> waitingKeysToRemove = mAvalancheController.getWaitingKeys();
 
         for (String key : keysToRemove) {
-            removeEntry(key);
+            removeEntry(key, "releaseAllImmediately (keysToRemove)");
         }
         for (String key : waitingKeysToRemove) {
-            removeEntry(key);
+            removeEntry(key, "releaseAllImmediately (waitingKeysToRemove)");
         }
     }
 
@@ -338,8 +342,9 @@
     }
 
     protected void setEntryPinned(
-            @NonNull BaseHeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned) {
-        mLogger.logSetEntryPinned(headsUpEntry.mEntry, isPinned);
+            @NonNull BaseHeadsUpManager.HeadsUpEntry headsUpEntry, boolean isPinned,
+            String reason) {
+        mLogger.logSetEntryPinned(headsUpEntry.mEntry, isPinned, reason);
         NotificationEntry entry = headsUpEntry.mEntry;
         if (!isPinned) {
             headsUpEntry.mWasUnpinned = true;
@@ -376,7 +381,7 @@
         entry.setHeadsUp(true);
 
         final boolean shouldPin = shouldHeadsUpBecomePinned(entry);
-        setEntryPinned(headsUpEntry, shouldPin);
+        setEntryPinned(headsUpEntry, shouldPin, "onEntryAdded");
         EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 1 /* visible */);
         for (OnHeadsUpChangedListener listener : mListeners) {
             listener.onHeadsUpStateChanged(entry, true);
@@ -387,17 +392,24 @@
      * Remove a notification from the alerting entries.
      * @param key key of notification to remove
      */
-    protected final void removeEntry(@NonNull String key) {
+    protected final void removeEntry(@NonNull String key, String reason) {
         HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
-        mLogger.logRemoveEntryRequest(key);
-
+        boolean isWaiting;
+        if (headsUpEntry == null) {
+            headsUpEntry = mAvalancheController.getWaitingEntry(key);
+            isWaiting = true;
+        } else {
+            isWaiting = false;
+        }
+        mLogger.logRemoveEntryRequest(key, reason, isWaiting);
+        HeadsUpEntry finalHeadsUpEntry = headsUpEntry;
         Runnable runnable = () -> {
-            mLogger.logRemoveEntry(key);
+            mLogger.logRemoveEntry(key, reason, isWaiting);
 
-            if (headsUpEntry == null) {
+            if (finalHeadsUpEntry == null) {
                 return;
             }
-            NotificationEntry entry = headsUpEntry.mEntry;
+            NotificationEntry entry = finalHeadsUpEntry.mEntry;
 
             // If the notification is animating, we will remove it at the end of the animation.
             if (entry != null && entry.isExpandAnimationRunning()) {
@@ -405,13 +417,13 @@
             }
             entry.demoteStickyHun();
             mHeadsUpEntryMap.remove(key);
-            onEntryRemoved(headsUpEntry);
+            onEntryRemoved(finalHeadsUpEntry);
             // TODO(b/328390331) move accessibility events to the view layer
             entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
             if (NotificationsHeadsUpRefactor.isEnabled()) {
-                headsUpEntry.cancelAutoRemovalCallbacks("removeEntry");
+                finalHeadsUpEntry.cancelAutoRemovalCallbacks("removeEntry");
             } else {
-                headsUpEntry.reset();
+                finalHeadsUpEntry.reset();
             }
         };
         mAvalancheController.delete(headsUpEntry, runnable, "removeEntry");
@@ -424,7 +436,7 @@
     protected void onEntryRemoved(HeadsUpEntry headsUpEntry) {
         NotificationEntry entry = headsUpEntry.mEntry;
         entry.setHeadsUp(false);
-        setEntryPinned(headsUpEntry, false /* isPinned */);
+        setEntryPinned(headsUpEntry, false /* isPinned */, "onEntryRemoved");
         EventLogTags.writeSysuiHeadsUpStatus(entry.getKey(), 0 /* visible */);
         mLogger.logNotificationActuallyRemoved(entry);
         for (OnHeadsUpChangedListener listener : mListeners) {
@@ -584,7 +596,7 @@
             Runnable runnable = () -> {
                 mLogger.logUnpinEntry(key);
 
-                setEntryPinned(headsUpEntry, false /* isPinned */);
+                setEntryPinned(headsUpEntry, false /* isPinned */, "unpinAll");
                 // maybe it got un sticky
                 headsUpEntry.updateEntry(false /* updatePostTime */, "unpinAll");
 
@@ -985,7 +997,7 @@
 
         /** Creates a runnable to remove this notification from the alerting entries. */
         protected Runnable createRemoveRunnable(NotificationEntry entry) {
-            return () -> removeEntry(entry.getKey());
+            return () -> removeEntry(entry.getKey(), "createRemoveRunnable");
         }
 
         /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index 11cbc9c..6ffb162 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -121,19 +121,23 @@
         })
     }
 
-    fun logRemoveEntryRequest(key: String) {
+    fun logRemoveEntryRequest(key: String, reason: String, isWaiting: Boolean) {
         buffer.log(TAG, INFO, {
             str1 = logKey(key)
+            str2 = reason
+            bool1 = isWaiting
         }, {
-            "request: remove entry $str1"
+            "request: $str2 => remove entry $str1 isWaiting: $isWaiting"
         })
     }
 
-    fun logRemoveEntry(key: String) {
+    fun logRemoveEntry(key: String, reason: String, isWaiting: Boolean) {
         buffer.log(TAG, INFO, {
             str1 = logKey(key)
+            str2 = reason
+            bool1 = isWaiting
         }, {
-            "remove entry $str1"
+            "$str2 => remove entry $str1 isWaiting: $isWaiting"
         })
     }
 
@@ -153,12 +157,13 @@
         })
     }
 
-    fun logRemoveNotification(key: String, releaseImmediately: Boolean) {
+    fun logRemoveNotification(key: String, releaseImmediately: Boolean, isWaiting: Boolean) {
         buffer.log(TAG, INFO, {
             str1 = logKey(key)
             bool1 = releaseImmediately
+            bool2 = isWaiting
         }, {
-            "remove notification $str1 releaseImmediately: $bool1"
+            "remove notification $str1 releaseImmediately: $bool1 isWaiting: $bool2"
         })
     }
 
@@ -208,12 +213,13 @@
         })
     }
 
-    fun logSetEntryPinned(entry: NotificationEntry, isPinned: Boolean) {
+    fun logSetEntryPinned(entry: NotificationEntry, isPinned: Boolean, reason: String) {
         buffer.log(TAG, VERBOSE, {
             str1 = entry.logKey
             bool1 = isPinned
+            str2 = reason
         }, {
-            "set entry pinned $str1 pinned: $bool1"
+            "$str2 => set entry pinned $str1 pinned: $bool1"
         })
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
index f4fc978..1ae5614 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AudioModule.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.volume.dagger
 
+import android.content.ContentResolver
 import android.content.Context
 import android.media.AudioManager
 import com.android.settingslib.bluetooth.LocalBluetoothManager
@@ -28,6 +29,7 @@
 import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiverImpl
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import dagger.Module
@@ -42,21 +44,31 @@
     companion object {
 
         @Provides
+        @SysUISingleton
         fun provideAudioManagerIntentsReceiver(
             @Application context: Context,
             @Application coroutineScope: CoroutineScope,
         ): AudioManagerEventsReceiver = AudioManagerEventsReceiverImpl(context, coroutineScope)
 
         @Provides
+        @SysUISingleton
         fun provideAudioRepository(
             intentsReceiver: AudioManagerEventsReceiver,
             audioManager: AudioManager,
+            contentResolver: ContentResolver,
             @Background coroutineContext: CoroutineContext,
             @Application coroutineScope: CoroutineScope,
         ): AudioRepository =
-            AudioRepositoryImpl(intentsReceiver, audioManager, coroutineContext, coroutineScope)
+            AudioRepositoryImpl(
+                intentsReceiver,
+                audioManager,
+                contentResolver,
+                coroutineContext,
+                coroutineScope,
+            )
 
         @Provides
+        @SysUISingleton
         fun provideAudioSharingRepository(
             localBluetoothManager: LocalBluetoothManager?,
             @Background coroutineContext: CoroutineContext,
@@ -64,10 +76,12 @@
             AudioSharingRepositoryImpl(localBluetoothManager, coroutineContext)
 
         @Provides
+        @SysUISingleton
         fun provideAudioModeInteractor(repository: AudioRepository): AudioModeInteractor =
             AudioModeInteractor(repository)
 
         @Provides
+        @SysUISingleton
         fun provideAudioVolumeInteractor(
             audioRepository: AudioRepository,
             notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
index ea67eea..73f5237 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt
@@ -20,6 +20,7 @@
 import com.android.settingslib.view.accessibility.data.repository.CaptioningRepository
 import com.android.settingslib.view.accessibility.data.repository.CaptioningRepositoryImpl
 import com.android.settingslib.view.accessibility.domain.interactor.CaptioningInteractor
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import dagger.Module
@@ -33,6 +34,7 @@
     companion object {
 
         @Provides
+        @SysUISingleton
         fun provideCaptioningRepository(
             captioningManager: CaptioningManager,
             @Background coroutineContext: CoroutineContext,
@@ -41,6 +43,7 @@
             CaptioningRepositoryImpl(captioningManager, coroutineContext, coroutineScope)
 
         @Provides
+        @SysUISingleton
         fun provideCaptioningInteractor(repository: CaptioningRepository): CaptioningInteractor =
             CaptioningInteractor(repository)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
index 3696108..efab199 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
@@ -18,7 +18,6 @@
 
 import android.media.session.MediaSessionManager
 import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.settingslib.volume.data.repository.LocalMediaRepository
 import com.android.settingslib.volume.data.repository.MediaControllerRepository
 import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
@@ -52,12 +51,6 @@
 
         @Provides
         @SysUISingleton
-        fun provideLocalMediaRepository(
-            factory: LocalMediaRepositoryFactory
-        ): LocalMediaRepository = factory.create(null)
-
-        @Provides
-        @SysUISingleton
         fun provideMediaDeviceSessionRepository(
             intentsReceiver: AudioManagerEventsReceiver,
             mediaSessionManager: MediaSessionManager,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt
index 4ba7cbb..a11997a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt
@@ -21,6 +21,7 @@
 import com.android.settingslib.media.data.repository.SpatializerRepository
 import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl
 import com.android.settingslib.media.domain.interactor.SpatializerInteractor
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import dagger.Module
 import dagger.Provides
@@ -33,17 +34,20 @@
     companion object {
 
         @Provides
+        @SysUISingleton
         fun provideSpatializer(
             audioManager: AudioManager,
         ): Spatializer = audioManager.spatializer
 
         @Provides
+        @SysUISingleton
         fun provdieSpatializerRepository(
             spatializer: Spatializer,
             @Background backgroundContext: CoroutineContext,
         ): SpatializerRepository = SpatializerRepositoryImpl(spatializer, backgroundContext)
 
         @Provides
+        @SysUISingleton
         fun provideSpatializerInetractor(repository: SpatializerRepository): SpatializerInteractor =
             SpatializerInteractor(repository)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
index e052f24..0dc2647 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
@@ -18,6 +18,7 @@
 import com.android.settingslib.volume.data.repository.LocalMediaRepository
 import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl
 import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.media.controls.util.LocalMediaManagerFactory
 import javax.inject.Inject
@@ -28,6 +29,7 @@
     fun create(packageName: String?): LocalMediaRepository
 }
 
+@SysUISingleton
 class LocalMediaRepositoryFactoryImpl
 @Inject
 constructor(
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt
new file mode 100644
index 0000000..e46ce26
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.data.repository
+
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.volume.panel.shared.model.VolumePanelGlobalState
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+private const val TAG = "VolumePanelGlobalState"
+
+@SysUISingleton
+class VolumePanelGlobalStateRepository @Inject constructor(dumpManager: DumpManager) : Dumpable {
+
+    private val mutableGlobalState =
+        MutableStateFlow(
+            VolumePanelGlobalState(
+                isVisible = false,
+            )
+        )
+    val globalState: StateFlow<VolumePanelGlobalState> = mutableGlobalState.asStateFlow()
+
+    init {
+        dumpManager.registerNormalDumpable(TAG, this)
+    }
+
+    fun updateVolumePanelState(
+        update: (currentState: VolumePanelGlobalState) -> VolumePanelGlobalState
+    ) {
+        mutableGlobalState.update(update)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        with(globalState.value) { pw.println("isVisible: $isVisible") }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/VolumePanelGlobalStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/VolumePanelGlobalStateInteractor.kt
new file mode 100644
index 0000000..e930aca
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/VolumePanelGlobalStateInteractor.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.volume.panel.data.repository.VolumePanelGlobalStateRepository
+import com.android.systemui.volume.panel.shared.model.VolumePanelGlobalState
+import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
+
+@SysUISingleton
+class VolumePanelGlobalStateInteractor
+@Inject
+constructor(
+    private val repository: VolumePanelGlobalStateRepository,
+) {
+
+    val globalState: StateFlow<VolumePanelGlobalState>
+        get() = repository.globalState
+
+    fun setVisible(isVisible: Boolean) {
+        repository.updateVolumePanelState { it.copy(isVisible = isVisible) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelGlobalState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelGlobalState.kt
new file mode 100644
index 0000000..fac49db
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelGlobalState.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.shared.model
+
+data class VolumePanelGlobalState(val isVisible: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt
index f57e293..dc43155 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt
@@ -29,7 +29,6 @@
 data class VolumePanelState(
     @Orientation val orientation: Int,
     val isLargeScreen: Boolean,
-    val isVisible: Boolean,
 ) {
     init {
         require(
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
index a30de1b..f495a02f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
@@ -29,23 +29,22 @@
 import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory
 import com.android.systemui.volume.panel.domain.VolumePanelStartable
 import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor
+import com.android.systemui.volume.panel.domain.interactor.VolumePanelGlobalStateInteractor
 import com.android.systemui.volume.panel.ui.composable.ComponentsFactory
 import com.android.systemui.volume.panel.ui.layout.ComponentsLayout
 import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.update
 
 // Can't inject a constructor here because VolumePanelComponent provides this view model for its
 // components.
@@ -55,6 +54,7 @@
     daggerComponentFactory: VolumePanelComponentFactory,
     configurationController: ConfigurationController,
     broadcastDispatcher: BroadcastDispatcher,
+    private val volumePanelGlobalStateInteractor: VolumePanelGlobalStateInteractor,
 ) {
 
     private val volumePanelComponent: VolumePanelComponent =
@@ -72,18 +72,12 @@
     private val componentsLayoutManager: ComponentsLayoutManager
         get() = volumePanelComponent.componentsLayoutManager()
 
-    private val mutablePanelVisibility = MutableStateFlow(true)
-
     val volumePanelState: StateFlow<VolumePanelState> =
-        combine(
-                configurationController.onConfigChanged
-                    .onStart { emit(resources.configuration) }
-                    .distinctUntilChanged(),
-                mutablePanelVisibility,
-            ) { configuration, isVisible ->
+        configurationController.onConfigChanged
+            .onStart { emit(resources.configuration) }
+            .map { configuration ->
                 VolumePanelState(
                     orientation = configuration.orientation,
-                    isVisible = isVisible,
                     isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen),
                 )
             }
@@ -92,7 +86,6 @@
                 SharingStarted.Eagerly,
                 VolumePanelState(
                     orientation = resources.configuration.orientation,
-                    isVisible = mutablePanelVisibility.value,
                     isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen)
                 ),
             )
@@ -126,7 +119,7 @@
     }
 
     fun dismissPanel() {
-        mutablePanelVisibility.update { false }
+        volumePanelGlobalStateInteractor.setVisible(false)
     }
 
     class Factory
@@ -136,6 +129,7 @@
         private val daggerComponentFactory: VolumePanelComponentFactory,
         private val configurationController: ConfigurationController,
         private val broadcastDispatcher: BroadcastDispatcher,
+        private val volumePanelGlobalStateInteractor: VolumePanelGlobalStateInteractor,
     ) {
 
         fun create(coroutineScope: CoroutineScope): VolumePanelViewModel {
@@ -144,7 +138,8 @@
                 coroutineScope,
                 daggerComponentFactory,
                 configurationController,
-                broadcastDispatcher
+                broadcastDispatcher,
+                volumePanelGlobalStateInteractor,
             )
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt
index 905a749..03c8af90 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt
@@ -16,26 +16,40 @@
 
 package com.android.systemui.volume.ui.navigation
 
+import android.app.Dialog
 import android.content.Intent
 import android.provider.Settings
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
 import com.android.systemui.statusbar.phone.createBottomSheet
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
 import com.android.systemui.volume.VolumePanelFactory
 import com.android.systemui.volume.domain.model.VolumePanelRoute
+import com.android.systemui.volume.panel.domain.interactor.VolumePanelGlobalStateInteractor
 import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
 import com.android.systemui.volume.panel.ui.composable.VolumePanelRoot
 import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
 
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
 class VolumeNavigator
 @Inject
 constructor(
@@ -46,8 +60,29 @@
     private val viewModelFactory: VolumePanelViewModel.Factory,
     private val dialogFactory: SystemUIDialogFactory,
     private val uiEventLogger: UiEventLogger,
+    private val volumePanelGlobalStateInteractor: VolumePanelGlobalStateInteractor,
 ) {
 
+    init {
+        volumePanelGlobalStateInteractor.globalState
+            .map { it.isVisible }
+            .distinctUntilChanged()
+            .flatMapLatest { isVisible ->
+                if (isVisible) {
+                    conflatedCallbackFlow<Unit> {
+                            val dialog = createNewVolumePanelDialog()
+                            uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_SHOWN)
+                            dialog.show()
+                            awaitClose { dialog.dismiss() }
+                        }
+                        .flowOn(mainContext)
+                } else {
+                    emptyFlow()
+                }
+            }
+            .launchIn(applicationScope)
+    }
+
     fun openVolumePanel(route: VolumePanelRoute) {
         when (route) {
             VolumePanelRoute.COMPOSE_VOLUME_PANEL -> showNewVolumePanel()
@@ -62,24 +97,24 @@
     }
 
     private fun showNewVolumePanel() {
-        applicationScope.launch(mainContext) {
-            uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_SHOWN)
-            dialogFactory
-                .createBottomSheet(
-                    content = { dialog ->
-                        LaunchedEffect(dialog) {
-                            dialog.setOnDismissListener {
-                                uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_GONE)
-                            }
-                        }
+        volumePanelGlobalStateInteractor.setVisible(true)
+    }
 
-                        VolumePanelRoot(
-                            viewModel = viewModelFactory.create(rememberCoroutineScope()),
-                            onDismiss = { dialog.dismiss() },
-                        )
-                    },
+    private fun createNewVolumePanelDialog(): Dialog {
+        return dialogFactory.createBottomSheet(
+            content = { dialog ->
+                LaunchedEffect(dialog) {
+                    dialog.setOnDismissListener {
+                        uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_GONE)
+                        volumePanelGlobalStateInteractor.setVisible(false)
+                    }
+                }
+
+                val coroutineScope = rememberCoroutineScope()
+                VolumePanelRoot(
+                    remember(coroutineScope) { viewModelFactory.create(coroutineScope) }
                 )
-                .show()
-        }
+            },
+        )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
index 190babd..0ed84ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
@@ -4,7 +4,9 @@
 import android.window.BackEvent
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.dpToPx
 import com.google.common.truth.Truth.assertThat
+import junit.framework.TestCase.assertEquals
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -60,6 +62,58 @@
             expected = BackTransformation(translateX = 0f, translateY = maxY, scale = 1f),
         )
     }
+
+    @Test
+    fun sysUi_bottomsheet_animationValues() {
+        val minScale = 1 - 48.dpToPx(displayMetrics) / displayMetrics.widthPixels
+
+        val backAnimationSpec = BackAnimationSpec.bottomSheetForSysUi { displayMetrics }
+
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 0f, progressY = 0f, edge = BackEvent.EDGE_LEFT),
+            expected =
+                BackTransformation(
+                    translateX = Float.NaN,
+                    translateY = Float.NaN,
+                    scale = 1f,
+                    scalePivotPosition = ScalePivotPosition.BOTTOM_CENTER
+                ),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 1f, progressY = 0f, edge = BackEvent.EDGE_LEFT),
+            expected =
+                BackTransformation(
+                    translateX = Float.NaN,
+                    translateY = Float.NaN,
+                    scale = minScale,
+                    scalePivotPosition = ScalePivotPosition.BOTTOM_CENTER
+                ),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 1f, progressY = 0f, edge = BackEvent.EDGE_RIGHT),
+            expected =
+                BackTransformation(
+                    translateX = Float.NaN,
+                    translateY = Float.NaN,
+                    scale = minScale,
+                    scalePivotPosition = ScalePivotPosition.BOTTOM_CENTER
+                ),
+        )
+        assertBackTransformation(
+            backAnimationSpec = backAnimationSpec,
+            backInput = BackInput(progressX = 1f, progressY = 1f, edge = BackEvent.EDGE_LEFT),
+            expected =
+                BackTransformation(
+                    translateX = Float.NaN,
+                    translateY = Float.NaN,
+                    scale = minScale,
+                    scalePivotPosition = ScalePivotPosition.BOTTOM_CENTER
+                ),
+        )
+    }
 }
 
 private fun assertBackTransformation(
@@ -81,7 +135,16 @@
     )
 
     val tolerance = 0f
-    assertThat(actual.translateX).isWithin(tolerance).of(expected.translateX)
-    assertThat(actual.translateY).isWithin(tolerance).of(expected.translateY)
+    if (expected.translateX.isNaN()) {
+        assertEquals(expected.translateX, actual.translateX)
+    } else {
+        assertThat(actual.translateX).isWithin(tolerance).of(expected.translateX)
+    }
+    if (expected.translateY.isNaN()) {
+        assertEquals(expected.translateY, actual.translateY)
+    } else {
+        assertThat(actual.translateY).isWithin(tolerance).of(expected.translateY)
+    }
     assertThat(actual.scale).isWithin(tolerance).of(expected.scale)
+    assertEquals(expected.scalePivotPosition, actual.scalePivotPosition)
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
index 190b3d2..44a5467 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackTransformationTest.kt
@@ -5,17 +5,25 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @RunWith(JUnit4::class)
 class BackTransformationTest : SysuiTestCase() {
     private val targetView: View = mock()
 
+    @Before
+    fun setup() {
+        whenever(targetView.width).thenReturn(TARGET_VIEW_WIDTH)
+        whenever(targetView.height).thenReturn(TARGET_VIEW_HEIGHT)
+    }
+
     @Test
     fun defaultValue_noTransformation() {
         val transformation = BackTransformation()
@@ -70,6 +78,16 @@
     }
 
     @Test
+    fun applyTo_targetView_scale_pivot() {
+        val transformation = BackTransformation(scalePivotPosition = ScalePivotPosition.CENTER)
+
+        transformation.applyTo(targetView = targetView)
+
+        verify(targetView).pivotX = TARGET_VIEW_WIDTH / 2f
+        verify(targetView).pivotY = TARGET_VIEW_HEIGHT / 2f
+    }
+
+    @Test
     fun applyTo_targetView_noTransformation() {
         val transformation = BackTransformation()
 
@@ -77,4 +95,9 @@
 
         verifyNoMoreInteractions(targetView)
     }
+
+    companion object {
+        private const val TARGET_VIEW_WIDTH = 100
+        private const val TARGET_VIEW_HEIGHT = 50
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt
index 472d045..8be1e7a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/DeviceEntrySideFpsOverlayInteractorTest.kt
@@ -115,6 +115,7 @@
                 { mock(DeviceEntryFingerprintAuthInteractor::class.java) },
                 { mock(KeyguardInteractor::class.java) },
                 { mock(KeyguardTransitionInteractor::class.java) },
+                { kosmos.sceneInteractor },
                 testScope.backgroundScope,
             )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index fa3fe5c..e02fb29 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.setCommunalAvailable
 import com.android.systemui.communal.shared.model.CommunalScenes
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
 import com.android.systemui.dock.fakeDockManager
 import com.android.systemui.flags.BrokenWithSceneContainer
 import com.android.systemui.flags.DisableSceneContainer
@@ -511,7 +512,6 @@
                 .startedTransition(
                     to = KeyguardState.LOCKSCREEN,
                     from = KeyguardState.DOZING,
-                    ownerName = "FromDozingTransitionInteractor",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -600,7 +600,6 @@
                 .startedTransition(
                     to = KeyguardState.PRIMARY_BOUNCER,
                     from = KeyguardState.DOZING,
-                    ownerName = "FromDozingTransitionInteractor",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -618,6 +617,7 @@
             // WHEN the device wakes up without a keyguard
             keyguardRepository.setKeyguardShowing(false)
             keyguardRepository.setKeyguardDismissible(true)
+            kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(false)
             powerInteractor.setAwakeForTest()
             advanceTimeBy(60L)
 
@@ -625,7 +625,6 @@
                 .startedTransition(
                     to = KeyguardState.GONE,
                     from = KeyguardState.DOZING,
-                    ownerName = "FromDozingTransitionInteractor",
                     animatorAssertion = { it.isNotNull() }
                 )
 
@@ -683,7 +682,6 @@
                 .startedTransition(
                     to = KeyguardState.GLANCEABLE_HUB,
                     from = KeyguardState.DOZING,
-                    ownerName = FromDozingTransitionInteractor::class.simpleName,
                     animatorAssertion = { it.isNotNull() }
                 )
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
index 6043ede..ef73e2e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.mediaprojection.taskswitcher.data.repository
+package com.android.systemui.mediaprojection.data.repository
 
 import android.os.Binder
 import android.testing.AndroidTestingRunner
@@ -23,9 +23,9 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
-import com.android.systemui.mediaprojection.taskswitcher.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
 import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
 import com.android.systemui.mediaprojection.taskswitcher.mediaProjectionManagerRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
index 9382c58..ad18099 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
@@ -40,6 +40,7 @@
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 
 @RunWith(AndroidTestingRunner::class)
@@ -66,6 +67,10 @@
                 fakeBroadcastDispatcher,
             )
         coordinator.start()
+        // When the coordinator starts up, the view model will immediately emit a NotShowing event
+        // and hide the notification. That's fine, but we should reset the notification manager so
+        // that the initial emission isn't part of the tests.
+        reset(notificationManager)
     }
 
     @Test
@@ -82,8 +87,13 @@
     @Test
     fun hideNotification() {
         testScope.runTest {
+            // First, show a notification
+            switchTask()
+
+            // WHEN the projection is stopped
             fakeMediaProjectionManager.dispatchOnStop()
 
+            // THEN the notification is hidden
             verify(notificationManager).cancel(any(), any())
         }
     }
@@ -91,14 +101,16 @@
     @Test
     fun notificationIdIsConsistent() {
         testScope.runTest {
-            fakeMediaProjectionManager.dispatchOnStop()
-            val idCancel = argumentCaptor<Int>()
-            verify(notificationManager).cancel(any(), idCancel.capture())
-
+            // First, show a notification
             switchTask()
             val idNotify = argumentCaptor<Int>()
             verify(notificationManager).notify(any(), idNotify.capture(), any())
 
+            // Then, hide the notification
+            fakeMediaProjectionManager.dispatchOnStop()
+            val idCancel = argumentCaptor<Int>()
+            verify(notificationManager).cancel(any(), idCancel.capture())
+
             assertEquals(idCancel.value, idNotify.value)
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt
new file mode 100644
index 0000000..07ec38e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseSceneContainerTest.kt
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs
+
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.testing.TestableLooper.RunWithLooper
+import android.view.ViewTreeObserver
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.haptics.qs.QSLongPressEffect
+import com.android.systemui.haptics.qs.qsLongPressEffect
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.InstantTaskExecutorRule
+import com.android.systemui.media.controls.ui.view.MediaHost
+import com.android.systemui.qs.customize.QSCustomizerController
+import com.android.systemui.qs.logging.QSLogger
+import com.android.systemui.statusbar.policy.SplitShadeStateController
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import javax.inject.Provider
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@RunWithLooper
+@OptIn(ExperimentalCoroutinesApi::class)
+@EnableSceneContainer
+class QSPanelControllerBaseSceneContainerTest : SysuiTestCase() {
+
+    @Rule @JvmField val mInstantTaskExecutor = InstantTaskExecutorRule()
+
+    private val kosmos = testKosmos()
+
+    @Mock private lateinit var qsPanel: QSPanel
+    @Mock private lateinit var qsHost: QSHost
+    @Mock private lateinit var qsCustomizerController: QSCustomizerController
+    @Mock private lateinit var metricsLogger: MetricsLogger
+    private val uiEventLogger = UiEventLoggerFake()
+    @Mock private lateinit var qsLogger: QSLogger
+    private val dumpManager = DumpManager()
+    @Mock private lateinit var tileLayout: PagedTileLayout
+    @Mock private lateinit var resources: Resources
+    private val configuration = Configuration()
+    @Mock private lateinit var viewTreeObserver: ViewTreeObserver
+    @Mock private lateinit var mediaHost: MediaHost
+
+    private var isSplitShade = false
+    private val splitShadeStateController =
+        object : SplitShadeStateController {
+            override fun shouldUseSplitNotificationShade(resources: Resources): Boolean {
+                return isSplitShade
+            }
+        }
+    private val longPressEffectProvider: Provider<QSLongPressEffect> = Provider {
+        kosmos.qsLongPressEffect
+    }
+
+    private val mediaVisible = MutableStateFlow(false)
+
+    private lateinit var underTest: TestableQSPanelControllerBase
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        allowTestableLooperAsMainThread()
+        Dispatchers.setMain(kosmos.testDispatcher)
+
+        whenever(qsPanel.isAttachedToWindow).thenReturn(true)
+        whenever(qsPanel.orCreateTileLayout).thenReturn(tileLayout)
+        whenever(qsPanel.tileLayout).thenReturn(tileLayout)
+        whenever(qsPanel.resources).thenReturn(resources)
+        whenever(qsPanel.viewTreeObserver).thenReturn(viewTreeObserver)
+        whenever(qsHost.tiles).thenReturn(emptyList())
+        whenever(resources.configuration).thenReturn(configuration)
+
+        underTest = createUnderTest()
+        underTest.init()
+    }
+
+    @After
+    fun tearDown() {
+        disallowTestableLooperAsMainThread()
+        Dispatchers.resetMain()
+    }
+
+    @Test
+    fun configurationChange_onlySplitShadeConfigChanges_horizontalInSceneUpdated() =
+        with(kosmos) {
+            testScope.runTest {
+                clearInvocations(qsPanel)
+
+                mediaVisible.value = true
+                runCurrent()
+                isSplitShade = false
+                configuration.orientation = Configuration.ORIENTATION_LANDSCAPE
+                configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES
+                underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+
+                assertThat(underTest.shouldUseHorizontalInScene()).isTrue()
+                verify(qsPanel).setColumnRowLayout(true)
+                clearInvocations(qsPanel)
+
+                isSplitShade = true
+                underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+
+                assertThat(underTest.shouldUseHorizontalInScene()).isFalse()
+                verify(qsPanel).setColumnRowLayout(false)
+            }
+        }
+
+    @Test
+    fun configurationChange_shouldUseHorizontalInSceneInLongDevices() =
+        with(kosmos) {
+            testScope.runTest {
+                clearInvocations(qsPanel)
+
+                mediaVisible.value = true
+                runCurrent()
+                isSplitShade = false
+                // When device is rotated to landscape and is long
+                configuration.orientation = Configuration.ORIENTATION_LANDSCAPE
+                configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES
+                underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+
+                // Then the layout changes
+                assertThat(underTest.shouldUseHorizontalInScene()).isTrue()
+                verify(qsPanel).setColumnRowLayout(true)
+                clearInvocations(qsPanel)
+
+                // When device changes to not-long
+                configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_NO
+                underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+
+                // Then the layout changes back
+                assertThat(underTest.shouldUseHorizontalInScene()).isFalse()
+                verify(qsPanel).setColumnRowLayout(false)
+            }
+        }
+
+    @Test
+    fun configurationChange_horizontalInScene_onlyInLandscape() =
+        with(kosmos) {
+            testScope.runTest {
+                clearInvocations(qsPanel)
+
+                mediaVisible.value = true
+                runCurrent()
+                isSplitShade = false
+
+                // When device is rotated to landscape and is long
+                configuration.orientation = Configuration.ORIENTATION_LANDSCAPE
+                configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES
+                underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+
+                // Then the layout changes
+                assertThat(underTest.shouldUseHorizontalInScene()).isTrue()
+                verify(qsPanel).setColumnRowLayout(true)
+                clearInvocations(qsPanel)
+
+                // When it is rotated back to portrait
+                configuration.orientation = Configuration.ORIENTATION_PORTRAIT
+                underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+
+                // Then the layout changes back
+                assertThat(underTest.shouldUseHorizontalInScene()).isFalse()
+                verify(qsPanel).setColumnRowLayout(false)
+            }
+        }
+
+    @Test
+    fun changeMediaVisible_changesHorizontalInScene() =
+        with(kosmos) {
+            testScope.runTest {
+                mediaVisible.value = false
+                runCurrent()
+                isSplitShade = false
+                configuration.orientation = Configuration.ORIENTATION_LANDSCAPE
+                configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES
+                underTest.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+
+                assertThat(underTest.shouldUseHorizontalInScene()).isFalse()
+                clearInvocations(qsPanel)
+
+                mediaVisible.value = true
+                runCurrent()
+
+                assertThat(underTest.shouldUseHorizontalInScene()).isTrue()
+                verify(qsPanel).setColumnRowLayout(true)
+            }
+        }
+
+    @Test
+    fun startFromMediaHorizontalLong_shouldUseHorizontal() =
+        with(kosmos) {
+            testScope.runTest {
+                mediaVisible.value = true
+                runCurrent()
+                isSplitShade = false
+                configuration.orientation = Configuration.ORIENTATION_LANDSCAPE
+                configuration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES
+
+                underTest = createUnderTest()
+                underTest.init()
+                runCurrent()
+
+                assertThat(underTest.shouldUseHorizontalInScene()).isTrue()
+                verify(qsPanel).setColumnRowLayout(true)
+            }
+        }
+
+    private fun createUnderTest(): TestableQSPanelControllerBase {
+        return TestableQSPanelControllerBase(
+            qsPanel,
+            qsHost,
+            qsCustomizerController,
+            mediaHost,
+            metricsLogger,
+            uiEventLogger,
+            qsLogger,
+            dumpManager,
+            splitShadeStateController,
+            longPressEffectProvider,
+            mediaVisible,
+        )
+    }
+
+    private class TestableQSPanelControllerBase(
+        view: QSPanel,
+        qsHost: QSHost,
+        qsCustomizerController: QSCustomizerController,
+        mediaHost: MediaHost,
+        metricsLogger: MetricsLogger,
+        uiEventLogger: UiEventLogger,
+        qsLogger: QSLogger,
+        dumpManager: DumpManager,
+        splitShadeStateController: SplitShadeStateController,
+        longPressEffectProvider: Provider<QSLongPressEffect>,
+        private val mediaVisibleFlow: StateFlow<Boolean>
+    ) :
+        QSPanelControllerBase<QSPanel>(
+            view,
+            qsHost,
+            qsCustomizerController,
+            /* usingMediaPlayer= */ false,
+            mediaHost,
+            metricsLogger,
+            uiEventLogger,
+            qsLogger,
+            dumpManager,
+            splitShadeStateController,
+            longPressEffectProvider
+        ) {
+
+        init {
+            whenever(view.dumpableTag).thenReturn(hashCode().toString())
+        }
+        override fun getMediaVisibleFlow(): StateFlow<Boolean> {
+            return mediaVisibleFlow
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index 542bfaa..225adab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -17,9 +17,13 @@
 package com.android.systemui.qs;
 
 import static com.android.systemui.Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS;
+import static com.android.systemui.flags.SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static kotlinx.coroutines.flow.FlowKt.asStateFlow;
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
+
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -37,9 +41,10 @@
 import android.content.res.Resources;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
-import android.testing.AndroidTestingRunner;
+import android.platform.test.flag.junit.FlagsParameterization;
 import android.testing.TestableLooper.RunWithLooper;
 import android.view.ContextThemeWrapper;
+import android.view.ViewTreeObserver;
 
 import androidx.test.filters.SmallTest;
 
@@ -49,18 +54,26 @@
 import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.DisableSceneContainer;
 import com.android.systemui.haptics.qs.QSLongPressEffect;
 import com.android.systemui.kosmos.KosmosJavaAdapter;
+import com.android.systemui.lifecycle.InstantTaskExecutorRule;
 import com.android.systemui.media.controls.ui.view.MediaHost;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.customize.QSCustomizerController;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
 import com.android.systemui.util.animation.DisappearParameters;
 
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -73,11 +86,22 @@
 
 import javax.inject.Provider;
 
-@RunWith(AndroidTestingRunner.class)
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+@RunWith(ParameterizedAndroidJunit4.class)
 @RunWithLooper
 @SmallTest
 public class QSPanelControllerBaseTest extends SysuiTestCase {
 
+    @Rule
+    public final InstantTaskExecutorRule mInstantTaskExecutor = new InstantTaskExecutorRule();
+
+    @Parameters(name = "{0}")
+    public static List<FlagsParameterization> getParams() {
+        return parameterizeSceneContainerFlag();
+    }
+
     private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
     @Mock
     private QSPanel mQSPanel;
@@ -109,10 +133,13 @@
     Configuration mConfiguration;
     @Mock
     Runnable mHorizontalLayoutListener;
+    @Mock
+    private ViewTreeObserver mViewTreeObserver;
+
     private TestableLongPressEffectProvider mLongPressEffectProvider =
             new TestableLongPressEffectProvider();
 
-    private QSPanelControllerBase<QSPanel> mController;
+    private TestableQSPanelControllerBase mController;
 
     /** Implementation needed to ensure we have a reflectively-available class name. */
     private class TestableQSPanelControllerBase extends QSPanelControllerBase<QSPanel> {
@@ -120,15 +147,27 @@
                 QSCustomizerController qsCustomizerController, MediaHost mediaHost,
                 MetricsLogger metricsLogger, UiEventLogger uiEventLogger, QSLogger qsLogger,
                 DumpManager dumpManager) {
-            super(view, host, qsCustomizerController, true, mediaHost, metricsLogger, uiEventLogger,
+            super(view, host, qsCustomizerController, usingMediaPlayer(),
+                    mediaHost, metricsLogger, uiEventLogger,
                     qsLogger, dumpManager, new ResourcesSplitShadeStateController(),
                     mLongPressEffectProvider);
         }
 
+        private MutableStateFlow<Boolean> mMediaVisible = MutableStateFlow(false);
+
         @Override
         protected QSTileRevealController createTileRevealController() {
             return mQSTileRevealController;
         }
+
+        @Override
+        StateFlow<Boolean> getMediaVisibleFlow() {
+            return asStateFlow(mMediaVisible);
+        }
+
+        void setMediaVisible(boolean visible) {
+            mMediaVisible.tryEmit(visible);
+        }
     }
 
     private class TestableLongPressEffectProvider implements Provider<QSLongPressEffect> {
@@ -142,16 +181,24 @@
         }
     }
 
+    public QSPanelControllerBaseTest(FlagsParameterization flags) {
+        super();
+        mSetFlagsRule.setFlagsParameterization(flags);
+    }
+
     @Before
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
 
+        allowTestableLooperAsMainThread();
+
         when(mQSPanel.isAttachedToWindow()).thenReturn(true);
         when(mQSPanel.getDumpableTag()).thenReturn("QSPanel");
         when(mQSPanel.openPanelEvent()).thenReturn(QSEvent.QS_PANEL_EXPANDED);
         when(mQSPanel.closePanelEvent()).thenReturn(QSEvent.QS_PANEL_COLLAPSED);
         when(mQSPanel.getOrCreateTileLayout()).thenReturn(mPagedTileLayout);
         when(mQSPanel.getTileLayout()).thenReturn(mPagedTileLayout);
+        when(mQSPanel.getViewTreeObserver()).thenReturn(mViewTreeObserver);
         when(mQSTile.getTileSpec()).thenReturn("dnd");
         when(mQSHost.getTiles()).thenReturn(Collections.singleton(mQSTile));
         when(mQSTileRevealControllerFactory.create(any(), any()))
@@ -174,6 +221,11 @@
         reset(mQSTileRevealController);
     }
 
+    @After
+    public void tearDown() {
+        disallowTestableLooperAsMainThread();
+    }
+
     @Test
     public void testSetRevealExpansion_preAttach() {
         mController.onViewDetached();
@@ -269,6 +321,7 @@
 
 
     @Test
+    @DisableSceneContainer
     public void testShouldUseHorizontalLayout_falseForSplitShade() {
         mConfiguration.orientation = Configuration.ORIENTATION_LANDSCAPE;
         mConfiguration.screenLayout = Configuration.SCREENLAYOUT_LONG_YES;
@@ -294,6 +347,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testChangeConfiguration_shouldUseHorizontalLayoutInLandscape_true() {
         when(mMediaHost.getVisible()).thenReturn(true);
         mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener);
@@ -317,6 +371,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void testChangeConfiguration_shouldUseHorizontalLayoutInLongDevices_true() {
         when(mMediaHost.getVisible()).thenReturn(true);
         mController.setUsingHorizontalLayoutChangeListener(mHorizontalLayoutListener);
@@ -353,6 +408,7 @@
     }
 
     @Test
+    @DisableSceneContainer
     public void configurationChange_onlySplitShadeConfigChanges_horizontalLayoutStatusUpdated() {
         // Preconditions for horizontal layout
         when(mMediaHost.getVisible()).thenReturn(true);
@@ -502,4 +558,20 @@
         verify(mQSPanel, times(2)).removeTile(any());
         verify(mQSPanel, times(2)).addTile(any());
     }
+
+    @Test
+    public void dettach_destroy_attach_tilesAreNotReadded() {
+        when(mQSHost.getTiles()).thenReturn(List.of(mQSTile, mOtherTile));
+        mController.setTiles();
+
+        mController.onViewDetached();
+        mController.destroy();
+        mController.onViewAttached();
+
+        assertThat(mController.mRecords).isEmpty();
+    }
+
+    private boolean usingMediaPlayer() {
+        return !SceneContainerFlag.isEnabled();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index e50320d..545d19d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -10,6 +10,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.haptics.qs.QSLongPressEffect
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
 import com.android.systemui.media.controls.ui.view.MediaHost
 import com.android.systemui.media.controls.ui.view.MediaHostState
 import com.android.systemui.plugins.FalsingManager
@@ -17,6 +18,7 @@
 import com.android.systemui.qs.customize.QSCustomizerController
 import com.android.systemui.qs.logging.QSLogger
 import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.settings.brightness.BrightnessController
 import com.android.systemui.settings.brightness.BrightnessSliderController
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
@@ -63,6 +65,9 @@
     @Mock private lateinit var configuration: Configuration
     @Mock private lateinit var pagedTileLayout: PagedTileLayout
     @Mock private lateinit var longPressEffectProvider: Provider<QSLongPressEffect>
+    @Mock private lateinit var mediaCarouselInteractor: MediaCarouselInteractor
+
+    private val usingMediaPlayer: Boolean by lazy { !SceneContainerFlag.isEnabled }
 
     private lateinit var controller: QSPanelController
     private val testableResources: TestableResources = mContext.orCreateTestableResources
@@ -88,7 +93,7 @@
             tunerService,
             qsHost,
             qsCustomizerController,
-            /* usingMediaPlayer= */ true,
+            /* usingMediaPlayer= */ usingMediaPlayer,
             mediaHost,
             qsTileRevealControllerFactory,
             dumpManager,
@@ -101,6 +106,7 @@
             statusBarKeyguardViewManager,
             ResourcesSplitShadeStateController(),
             longPressEffectProvider,
+            mediaCarouselInteractor,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index 5c6ed70..e2a4d67 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -275,6 +275,19 @@
         ViewUtils.detachView(panel)
     }
 
+    @Test
+    fun setRowColumnLayout() {
+        qsPanel.setColumnRowLayout(/* withMedia= */ false)
+
+        assertThat(qsPanel.tileLayout!!.minRows).isEqualTo(1)
+        assertThat(qsPanel.tileLayout!!.maxColumns).isEqualTo(4)
+
+        qsPanel.setColumnRowLayout(/* withMedia= */ true)
+
+        assertThat(qsPanel.tileLayout!!.minRows).isEqualTo(2)
+        assertThat(qsPanel.tileLayout!!.maxColumns).isEqualTo(2)
+    }
+
     private infix fun View.isLeftOf(other: View): Boolean {
         val rect = Rect()
         getBoundsOnScreen(rect)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
index 1eb0a51..fee4b53 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.haptics.qs.QSLongPressEffect
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
 import com.android.systemui.media.controls.ui.view.MediaHost
 import com.android.systemui.media.controls.ui.view.MediaHostState
 import com.android.systemui.plugins.qs.QSTile
@@ -62,6 +63,10 @@
     @Mock private lateinit var tileLayout: TileLayout
     @Captor private lateinit var captor: ArgumentCaptor<QSPanel.OnConfigurationChangedListener>
     @Mock private lateinit var longPressEffectProvider: Provider<QSLongPressEffect>
+    @Mock private lateinit var mediaCarouselInteractor: MediaCarouselInteractor
+
+    private val usingMediaPlayer: Boolean
+        get() = false
 
     private val uiEventLogger = UiEventLoggerFake()
     private val dumpManager = DumpManager()
@@ -86,7 +91,7 @@
                 quickQSPanel,
                 qsHost,
                 qsCustomizerController,
-                /* usingMediaPlayer = */ false,
+                usingMediaPlayer,
                 mediaHost,
                 { usingCollapsedLandscapeMedia },
                 metricsLogger,
@@ -94,6 +99,7 @@
                 qsLogger,
                 dumpManager,
                 longPressEffectProvider,
+                mediaCarouselInteractor,
             )
 
         controller.init()
@@ -163,6 +169,7 @@
         qsLogger: QSLogger,
         dumpManager: DumpManager,
         longPressEffectProvider: Provider<QSLongPressEffect>,
+        mediaCarouselInteractor: MediaCarouselInteractor,
     ) :
         QuickQSPanelController(
             view,
@@ -177,6 +184,7 @@
             dumpManager,
             ResourcesSplitShadeStateController(),
             longPressEffectProvider,
+            mediaCarouselInteractor
         ) {
 
         private var rotation = RotationUtils.ROTATION_NONE
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
index 0ec8552..42b81de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
@@ -116,6 +116,7 @@
                     "test_spec:\n" +
                         "    QSTileState(" +
                         "icon=() -> com.android.systemui.common.shared.model.Icon?, " +
+                        "iconRes=null, " +
                         "label=test_data, " +
                         "activationState=INACTIVE, " +
                         "secondaryLabel=null, " +
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
index 4215b8c..e7bde681 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt
@@ -109,15 +109,10 @@
         activationState: QSTileState.ActivationState,
     ): QSTileState {
         val label = testLabel
+        val iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status
         return QSTileState(
-            icon = {
-                Icon.Loaded(
-                    context.getDrawable(
-                        com.android.internal.R.drawable.stat_sys_managed_profile_status
-                    )!!,
-                    null
-                )
-            },
+            icon = { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+            iconRes = iconRes,
             label = label,
             activationState = activationState,
             secondaryLabel =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index deecc5b..0d7a9e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -146,7 +146,8 @@
 
     @Test
     public void testLogStartPartialRecording() {
-        MediaProjectionCaptureTarget target = new MediaProjectionCaptureTarget(new LaunchCookie());
+        MediaProjectionCaptureTarget target =
+                new MediaProjectionCaptureTarget(new LaunchCookie(), 12345);
         Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false, target);
         mRecordingService.onStartCommand(startIntent, 0, 0);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
index 896c3bf..6f5c56e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
@@ -21,41 +21,38 @@
 import android.os.Process
 import android.os.UserHandle
 import android.testing.AndroidTestingRunner
-import android.view.accessibility.AccessibilityManager
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
+import java.util.UUID
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
-import org.junit.Assert.assertNotNull
 import org.junit.Before
 import org.junit.runner.RunWith
 import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
+import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
 class DefaultScreenshotActionsProviderTest : SysuiTestCase() {
     private val actionExecutor = mock<ActionExecutor>()
-    private val accessibilityManager = mock<AccessibilityManager>()
     private val uiEventLogger = mock<UiEventLogger>()
+    private val actionsCallback = mock<ScreenshotActionsController.ActionsCallback>()
 
     private val request = ScreenshotData.forTesting()
     private val validResult = ScreenshotSavedResult(Uri.EMPTY, Process.myUserHandle(), 0)
 
-    private lateinit var viewModel: ScreenshotViewModel
     private lateinit var actionsProvider: ScreenshotActionsProvider
 
     @Before
     fun setUp() {
-        viewModel = ScreenshotViewModel(accessibilityManager)
         request.userHandle = UserHandle.OWNER
     }
 
@@ -63,8 +60,9 @@
     fun previewActionAccessed_beforeScreenshotCompleted_doesNothing() {
         actionsProvider = createActionsProvider()
 
-        assertNotNull(viewModel.previewAction.value)
-        viewModel.previewAction.value!!.invoke()
+        val previewActionCaptor = argumentCaptor<() -> Unit>()
+        verify(actionsCallback).providePreviewAction(previewActionCaptor.capture())
+        previewActionCaptor.firstValue.invoke()
         verifyNoMoreInteractions(actionExecutor)
     }
 
@@ -72,13 +70,13 @@
     fun actionButtonsAccessed_beforeScreenshotCompleted_doesNothing() {
         actionsProvider = createActionsProvider()
 
-        assertThat(viewModel.actions.value.size).isEqualTo(2)
-        val firstAction = viewModel.actions.value[0]
-        assertThat(firstAction.onClicked).isNotNull()
-        val secondAction = viewModel.actions.value[1]
-        assertThat(secondAction.onClicked).isNotNull()
-        firstAction.onClicked!!.invoke()
-        secondAction.onClicked!!.invoke()
+        val actionButtonCaptor = argumentCaptor<() -> Unit>()
+        verify(actionsCallback, times(2))
+            .provideActionButton(any(), any(), actionButtonCaptor.capture())
+        val firstAction = actionButtonCaptor.firstValue
+        val secondAction = actionButtonCaptor.secondValue
+        firstAction.invoke()
+        secondAction.invoke()
         verifyNoMoreInteractions(actionExecutor)
     }
 
@@ -87,29 +85,39 @@
         actionsProvider = createActionsProvider()
 
         actionsProvider.setCompletedScreenshot(validResult)
-        viewModel.actions.value[0].onClicked!!.invoke()
 
-        verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED), eq(0), eq(""))
+        val actionButtonCaptor = argumentCaptor<() -> Unit>()
+        verify(actionsCallback, times(2))
+            .provideActionButton(any(), any(), actionButtonCaptor.capture())
+        actionButtonCaptor.firstValue.invoke()
+
+        verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED), eq(0), eq(""))
         val intentCaptor = argumentCaptor<Intent>()
         verify(actionExecutor)
-            .startSharedTransition(capture(intentCaptor), eq(Process.myUserHandle()), eq(true))
-        assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_EDIT)
+            .startSharedTransition(intentCaptor.capture(), eq(Process.myUserHandle()), eq(false))
+        assertThat(intentCaptor.firstValue.action).isEqualTo(Intent.ACTION_CHOOSER)
     }
 
     @Test
     fun actionAccessed_whilePending_launchesMostRecentAction() = runTest {
         actionsProvider = createActionsProvider()
 
-        viewModel.actions.value[0].onClicked!!.invoke()
-        viewModel.previewAction.value!!.invoke()
-        viewModel.actions.value[1].onClicked!!.invoke()
+        val previewActionCaptor = argumentCaptor<() -> Unit>()
+        verify(actionsCallback).providePreviewAction(previewActionCaptor.capture())
+        val actionButtonCaptor = argumentCaptor<() -> Unit>()
+        verify(actionsCallback, times(2))
+            .provideActionButton(any(), any(), actionButtonCaptor.capture())
+
+        actionButtonCaptor.firstValue.invoke()
+        previewActionCaptor.firstValue.invoke()
+        actionButtonCaptor.secondValue.invoke()
         actionsProvider.setCompletedScreenshot(validResult)
 
-        verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED), eq(0), eq(""))
+        verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED), eq(0), eq(""))
         val intentCaptor = argumentCaptor<Intent>()
         verify(actionExecutor)
-            .startSharedTransition(capture(intentCaptor), eq(Process.myUserHandle()), eq(false))
-        assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_CHOOSER)
+            .startSharedTransition(intentCaptor.capture(), eq(Process.myUserHandle()), eq(true))
+        assertThat(intentCaptor.firstValue.action).isEqualTo(Intent.ACTION_EDIT)
     }
 
     @Test
@@ -117,9 +125,12 @@
         actionsProvider = createActionsProvider()
 
         val onScrollClick = mock<Runnable>()
-        val numActions = viewModel.actions.value.size
         actionsProvider.onScrollChipReady(onScrollClick)
-        viewModel.actions.value[numActions].onClicked!!.invoke()
+        val actionButtonCaptor = argumentCaptor<() -> Unit>()
+        // share, edit, scroll
+        verify(actionsCallback, times(3))
+            .provideActionButton(any(), any(), actionButtonCaptor.capture())
+        actionButtonCaptor.thirdValue.invoke()
 
         verify(onScrollClick).run()
     }
@@ -129,10 +140,13 @@
         actionsProvider = createActionsProvider()
 
         val onScrollClick = mock<Runnable>()
-        val numActions = viewModel.actions.value.size
         actionsProvider.onScrollChipReady(onScrollClick)
+        val actionButtonCaptor = argumentCaptor<() -> Unit>()
         actionsProvider.onScrollChipInvalidated()
-        viewModel.actions.value[numActions].onClicked!!.invoke()
+        // share, edit, scroll
+        verify(actionsCallback, times(3))
+            .provideActionButton(any(), any(), actionButtonCaptor.capture())
+        actionButtonCaptor.thirdValue.invoke()
 
         verify(onScrollClick, never()).run()
     }
@@ -143,11 +157,15 @@
 
         val onScrollClick = mock<Runnable>()
         val onScrollClick2 = mock<Runnable>()
-        val numActions = viewModel.actions.value.size
+
         actionsProvider.onScrollChipReady(onScrollClick)
         actionsProvider.onScrollChipInvalidated()
         actionsProvider.onScrollChipReady(onScrollClick2)
-        viewModel.actions.value[numActions].onClicked!!.invoke()
+        val actionButtonCaptor = argumentCaptor<() -> Unit>()
+        // share, edit, scroll
+        verify(actionsCallback, times(3))
+            .provideActionButton(any(), any(), actionButtonCaptor.capture())
+        actionButtonCaptor.thirdValue.invoke()
 
         verify(onScrollClick2).run()
         verify(onScrollClick, never()).run()
@@ -156,11 +174,11 @@
     private fun createActionsProvider(): ScreenshotActionsProvider {
         return DefaultScreenshotActionsProvider(
             context,
-            viewModel,
             uiEventLogger,
+            UUID.randomUUID(),
             request,
-            "testid",
-            actionExecutor
+            actionExecutor,
+            actionsCallback,
         )
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotActionsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotActionsControllerTest.kt
new file mode 100644
index 0000000..2a3c31a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotActionsControllerTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import java.util.UUID
+import kotlin.test.Test
+import org.junit.Before
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ScreenshotActionsControllerTest : SysuiTestCase() {
+    private val screenshotData = mock<ScreenshotData>()
+    private val actionExecutor = mock<ActionExecutor>()
+    private val viewModel = mock<ScreenshotViewModel>()
+    private val onClick = mock<() -> Unit>()
+
+    private lateinit var actionsController: ScreenshotActionsController
+    private lateinit var fakeActionsProvider1: FakeActionsProvider
+    private lateinit var fakeActionsProvider2: FakeActionsProvider
+    private val actionsProviderFactory =
+        object : ScreenshotActionsProvider.Factory {
+            var isFirstCall = true
+            override fun create(
+                requestId: UUID,
+                request: ScreenshotData,
+                actionExecutor: ActionExecutor,
+                actionsCallback: ScreenshotActionsController.ActionsCallback
+            ): ScreenshotActionsProvider {
+                return if (isFirstCall) {
+                    isFirstCall = false
+                    fakeActionsProvider1 = FakeActionsProvider(actionsCallback)
+                    fakeActionsProvider1
+                } else {
+                    fakeActionsProvider2 = FakeActionsProvider(actionsCallback)
+                    fakeActionsProvider2
+                }
+            }
+        }
+
+    @Before
+    fun setUp() {
+        actionsController =
+            ScreenshotActionsController(viewModel, actionsProviderFactory, actionExecutor)
+    }
+
+    @Test
+    fun setPreview_onCurrentScreenshot_updatesViewModel() {
+        actionsController.setCurrentScreenshot(screenshotData)
+        fakeActionsProvider1.callPreview(onClick)
+
+        verify(viewModel).setPreviewAction(onClick)
+    }
+
+    @Test
+    fun setPreview_onNonCurrentScreenshot_doesNotUpdateViewModel() {
+        actionsController.setCurrentScreenshot(screenshotData)
+        actionsController.setCurrentScreenshot(screenshotData)
+        fakeActionsProvider1.callPreview(onClick)
+
+        verify(viewModel, never()).setPreviewAction(any())
+    }
+
+    class FakeActionsProvider(
+        private val actionsCallback: ScreenshotActionsController.ActionsCallback
+    ) : ScreenshotActionsProvider {
+
+        fun callPreview(onClick: () -> Unit) {
+            actionsCallback.providePreviewAction(onClick)
+        }
+
+        override fun onScrollChipReady(onClick: Runnable) {}
+
+        override fun onScrollChipInvalidated() {}
+
+        override fun setCompletedScreenshot(result: ScreenshotSavedResult) {}
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 49a467e..1ef44f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -35,7 +35,7 @@
 import com.android.systemui.ambient.touch.TouchMonitor
 import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent
 import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.communal.data.repository.FakeCommunalSceneRepository
 import com.android.systemui.communal.data.repository.fakeCommunalRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.domain.interactor.setCommunalAvailable
@@ -92,7 +92,7 @@
     private lateinit var containerView: View
     private lateinit var testableLooper: TestableLooper
 
-    private lateinit var communalRepository: FakeCommunalRepository
+    private lateinit var communalRepository: FakeCommunalSceneRepository
     private lateinit var underTest: GlanceableHubContainerController
 
     @Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java
index 4c2d908..5363c57 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java
@@ -46,6 +46,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.CarrierTextManager;
+import com.android.systemui.log.core.FakeLogBuffer;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.connectivity.IconState;
 import com.android.systemui.statusbar.connectivity.MobileDataIndicators;
@@ -169,6 +170,7 @@
                 mActivityStarter,
                 handler,
                 TestableLooper.get(this).getLooper(),
+                new ShadeCarrierGroupControllerLogger(FakeLogBuffer.Factory.Companion.create()),
                 mNetworkController,
                 mCarrierTextControllerBuilder,
                 mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java
index fe066ca2..59678a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java
@@ -20,14 +20,19 @@
 
 import static com.android.systemui.flags.Flags.KEYGUARD_TALKBACK_FIX;
 import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED;
+import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_TRANSIENT;
 import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -65,6 +70,8 @@
 import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor;
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor;
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.KeyguardIndication;
@@ -150,6 +157,10 @@
     @Mock
     protected BiometricMessageInteractor mBiometricMessageInteractor;
     @Mock
+    protected DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor;
+    @Mock
+    protected DeviceEntryFingerprintAuthInteractor mDeviceEntryFingerprintAuthInteractor;
+    @Mock
     protected ScreenLifecycle mScreenLifecycle;
     @Mock
     protected AuthController mAuthController;
@@ -237,6 +248,7 @@
                 .thenReturn(mock(StateFlow.class));
 
         when(mFaceHelpMessageDeferralFactory.create()).thenReturn(mFaceHelpMessageDeferral);
+        when(mDeviceEntryFingerprintAuthInteractor.isEngaged()).thenReturn(mock(StateFlow.class));
 
         mIndicationHelper = new IndicationHelper(mKeyguardUpdateMonitor);
 
@@ -279,7 +291,9 @@
                 mFlags,
                 mIndicationHelper,
                 KeyguardInteractorFactory.create(mFlags).getKeyguardInteractor(),
-                mBiometricMessageInteractor
+                mBiometricMessageInteractor,
+                mDeviceEntryFingerprintAuthInteractor,
+                mDeviceEntryFaceAuthInteractor
         );
         mController.init();
         mController.setIndicationArea(mIndicationArea);
@@ -306,4 +320,22 @@
         mExecutor.runAllReady();
         reset(mRotateTextViewController);
     }
+
+    void verifyNoMessage(int type) {
+        if (type == INDICATION_TYPE_TRANSIENT) {
+            verify(mRotateTextViewController, never()).showTransient(anyString());
+        } else {
+            verify(mRotateTextViewController, never()).updateIndication(eq(type),
+                    any(KeyguardIndication.class), anyBoolean());
+        }
+    }
+
+    void verifyIndicationShown(int indicationType, String message) {
+        verify(mRotateTextViewController)
+                .updateIndication(eq(indicationType),
+                        mKeyguardIndicationCaptor.capture(),
+                        eq(true));
+        assertThat(mKeyguardIndicationCaptor.getValue().getMessage().toString())
+                .isEqualTo(message);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index cfe9e2a..80011dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -1514,19 +1514,6 @@
     }
 
     @Test
-    public void onTrustAgentErrorMessageDroppedBecauseFingerprintMessageShowing() {
-        createController();
-        mController.setVisible(true);
-        mController.getKeyguardCallback().onBiometricHelp(BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
-                "fp not recognized", BiometricSourceType.FINGERPRINT);
-        clearInvocations(mRotateTextViewController);
-
-        mKeyguardUpdateMonitorCallback.onTrustAgentErrorMessage("testMessage");
-        verifyNoMessage(INDICATION_TYPE_TRUST);
-        verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);
-    }
-
-    @Test
     public void trustGrantedMessageShowsEvenWhenFingerprintMessageShowing() {
         createController();
         mController.setVisible(true);
@@ -1591,24 +1578,6 @@
         verify(mRotateTextViewController).showTransient(eq(message));
     }
 
-    private void verifyNoMessage(int type) {
-        if (type == INDICATION_TYPE_TRANSIENT) {
-            verify(mRotateTextViewController, never()).showTransient(anyString());
-        } else {
-            verify(mRotateTextViewController, never()).updateIndication(eq(type),
-                    anyObject(), anyBoolean());
-        }
-    }
-
-    private void verifyIndicationShown(int indicationType, String message) {
-        verify(mRotateTextViewController)
-                .updateIndication(eq(indicationType),
-                        mKeyguardIndicationCaptor.capture(),
-                        eq(true));
-        assertThat(mKeyguardIndicationCaptor.getValue().getMessage().toString())
-                .isEqualTo(message);
-    }
-
     private void fingerprintUnlockIsNotPossible() {
         setupFingerprintUnlockPossible(false);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerWithCoroutinesTest.kt
index 4a14f88..a68ba06 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerWithCoroutinesTest.kt
@@ -21,12 +21,17 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED
+import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BIOMETRIC_MESSAGE
+import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP
+import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_TRUST
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.runBlocking
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -62,6 +67,33 @@
             verify(mIndicationArea, times(3)).visibility = View.VISIBLE
         }
 
+    @Test
+    fun onTrustAgentErrorMessageDelayed_fingerprintEngaged() {
+        createController()
+        mController.setVisible(true)
+
+        // GIVEN fingerprint is engaged
+        whenever(mDeviceEntryFingerprintAuthInteractor.isEngaged).thenReturn(MutableStateFlow(true))
+
+        // WHEN a trust agent error message arrives
+        mKeyguardUpdateMonitorCallback.onTrustAgentErrorMessage("testMessage")
+        mExecutor.runAllReady()
+
+        // THEN no message shows immediately since fingerprint is engaged
+        verifyNoMessage(INDICATION_TYPE_TRUST)
+        verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE)
+        verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP)
+
+        // WHEN fingerprint is no longer engaged
+        whenever(mDeviceEntryFingerprintAuthInteractor.isEngaged)
+            .thenReturn(MutableStateFlow(false))
+        mController.mIsFingerprintEngagedCallback.accept(false)
+        mExecutor.runAllReady()
+
+        // THEN the message will show
+        verifyIndicationShown(INDICATION_TYPE_BIOMETRIC_MESSAGE, "testMessage")
+    }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
new file mode 100644
index 0000000..25efaf1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.screenrecord.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel
+import com.android.systemui.screenrecord.data.repository.screenRecordRepository
+import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.viewmodel.screenRecordChipInteractor
+import com.android.systemui.util.time.fakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.runTest
+
+@SmallTest
+class ScreenRecordChipInteractorTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val testScope = kosmos.testScope
+    private val screenRecordRepo = kosmos.screenRecordRepository
+    private val systemClock = kosmos.fakeSystemClock
+
+    private val underTest = kosmos.screenRecordChipInteractor
+
+    @Test
+    fun chip_doingNothingState_isHidden() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+        }
+
+    @Test
+    fun chip_startingState_isHidden() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(400)
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+        }
+
+    @Test
+    fun chip_recordingState_isShownWithIcon() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            val icon = (latest as OngoingActivityChipModel.Shown).icon
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.stat_sys_screen_record)
+        }
+
+    @Test
+    fun chip_timeResetsOnEachNewRecording() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            systemClock.setElapsedRealtime(1234)
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+
+            systemClock.setElapsedRealtime(5678)
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index fa2b343..1260f07 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -24,7 +24,9 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
-import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel
+import com.android.systemui.screenrecord.data.repository.screenRecordRepository
+import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -38,7 +40,7 @@
     @Test
     fun chip_allHidden_hidden() =
         kosmos.testScope.runTest {
-            kosmos.screenRecordChipInteractor.chip.value = OngoingActivityChipModel.Hidden
+            kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing
             kosmos.callChipInteractor.chip.value = OngoingActivityChipModel.Hidden
 
             val latest by collectLastValue(underTest.chip)
@@ -49,28 +51,18 @@
     @Test
     fun chip_screenRecordShow_restHidden_screenRecordShown() =
         kosmos.testScope.runTest {
-            val screenRecordChip =
-                OngoingActivityChipModel.Shown(
-                    Icon.Resource(R.drawable.ic_cake, ContentDescription.Loaded("icon")),
-                    startTimeMs = 500L,
-                ) {}
-            kosmos.screenRecordChipInteractor.chip.value = screenRecordChip
+            kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
             kosmos.callChipInteractor.chip.value = OngoingActivityChipModel.Hidden
 
             val latest by collectLastValue(underTest.chip)
 
-            assertThat(latest).isEqualTo(screenRecordChip)
+            assertIsScreenRecordChip(latest)
         }
 
     @Test
     fun chip_screenRecordShowAndCallShow_screenRecordShown() =
         kosmos.testScope.runTest {
-            val screenRecordChip =
-                OngoingActivityChipModel.Shown(
-                    Icon.Resource(R.drawable.ic_cake, ContentDescription.Loaded("icon")),
-                    startTimeMs = 500L,
-                ) {}
-            kosmos.screenRecordChipInteractor.chip.value = screenRecordChip
+            kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
 
             val callChip =
                 OngoingActivityChipModel.Shown(
@@ -81,13 +73,13 @@
 
             val latest by collectLastValue(underTest.chip)
 
-            assertThat(latest).isEqualTo(screenRecordChip)
+            assertIsScreenRecordChip(latest)
         }
 
     @Test
     fun chip_screenRecordHideAndCallShown_callShown() =
         kosmos.testScope.runTest {
-            kosmos.screenRecordChipInteractor.chip.value = OngoingActivityChipModel.Hidden
+            kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing
 
             val callChip =
                 OngoingActivityChipModel.Shown(
@@ -111,34 +103,24 @@
                     startTimeMs = 600L,
                 ) {}
             kosmos.callChipInteractor.chip.value = callChip
-            kosmos.screenRecordChipInteractor.chip.value = OngoingActivityChipModel.Hidden
+            kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing
 
             val latest by collectLastValue(underTest.chip)
 
             assertThat(latest).isEqualTo(callChip)
 
             // WHEN the higher priority screen record chip is added
-            val screenRecordChip =
-                OngoingActivityChipModel.Shown(
-                    Icon.Resource(R.drawable.ic_cake, ContentDescription.Loaded("icon")),
-                    startTimeMs = 500L,
-                ) {}
-            kosmos.screenRecordChipInteractor.chip.value = screenRecordChip
+            kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
 
             // THEN the higher priority screen record chip is used
-            assertThat(latest).isEqualTo(screenRecordChip)
+            assertIsScreenRecordChip(latest)
         }
 
     @Test
     fun chip_highestPriorityChipRemoved_showsNextPriorityChip() =
         kosmos.testScope.runTest {
             // Start with both the higher priority screen record chip and lower priority call chip
-            val screenRecordChip =
-                OngoingActivityChipModel.Shown(
-                    Icon.Resource(R.drawable.ic_cake, ContentDescription.Loaded("icon")),
-                    startTimeMs = 500L,
-                ) {}
-            kosmos.screenRecordChipInteractor.chip.value = screenRecordChip
+            kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.Recording
 
             val callChip =
                 OngoingActivityChipModel.Shown(
@@ -149,12 +131,18 @@
 
             val latest by collectLastValue(underTest.chip)
 
-            assertThat(latest).isEqualTo(screenRecordChip)
+            assertIsScreenRecordChip(latest)
 
             // WHEN the higher priority screen record is removed
-            kosmos.screenRecordChipInteractor.chip.value = OngoingActivityChipModel.Hidden
+            kosmos.screenRecordRepository.screenRecordState.value = ScreenRecordModel.DoingNothing
 
             // THEN the lower priority call is used
             assertThat(latest).isEqualTo(callChip)
         }
+
+    private fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) {
+        assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+        val icon = (latest as OngoingActivityChipModel.Shown).icon
+        assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.stat_sys_screen_record)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
index 0906d8e..9f752a8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
@@ -22,7 +22,7 @@
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.AnimatorTestRule
-import com.android.systemui.communal.data.repository.communalRepository
+import com.android.systemui.communal.data.repository.communalSceneRepository
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.dump.DumpManager
@@ -181,7 +181,7 @@
                 MutableStateFlow<ObservableTransitionState>(
                     ObservableTransitionState.Idle(CommunalScenes.Communal)
                 )
-            kosmos.communalRepository.setTransitionState(transitionState)
+            kosmos.communalSceneRepository.setTransitionState(transitionState)
             runCurrent()
             setDozeAmount(0f)
             verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f)
@@ -195,7 +195,7 @@
                 MutableStateFlow<ObservableTransitionState>(
                     ObservableTransitionState.Idle(CommunalScenes.Communal)
                 )
-            kosmos.communalRepository.setTransitionState(transitionState)
+            kosmos.communalSceneRepository.setTransitionState(transitionState)
             runCurrent()
             setDozeAmount(0f)
             verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 3e8461a..bfe5c6e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -58,7 +58,6 @@
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.os.Handler;
 import android.os.PowerManager;
-import android.os.RemoteException;
 import android.platform.test.annotations.DisableFlags;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -81,6 +80,7 @@
 import com.android.systemui.util.FakeEventLog;
 import com.android.systemui.util.settings.FakeGlobalSettings;
 import com.android.systemui.util.time.FakeSystemClock;
+import com.android.wm.shell.bubbles.Bubbles;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -90,6 +90,7 @@
 
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -127,6 +128,8 @@
     UserTracker mUserTracker;
     @Mock
     DeviceProvisionedController mDeviceProvisionedController;
+    @Mock
+    Bubbles mBubbles;
     FakeSystemClock mSystemClock;
     FakeGlobalSettings mGlobalSettings;
     FakeEventLog mEventLog;
@@ -137,6 +140,7 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         when(mUserTracker.getUserId()).thenReturn(ActivityManager.getCurrentUser());
+        when(mBubbles.canShowBubbleNotification()).thenReturn(true);
 
         mUiEventLoggerFake = new UiEventLoggerFake();
         mSystemClock = new FakeSystemClock();
@@ -161,7 +165,8 @@
                         mDeviceProvisionedController,
                         mSystemClock,
                         mGlobalSettings,
-                        mEventLog);
+                        mEventLog,
+                        Optional.of(mBubbles));
         mNotifInterruptionStateProvider.mUseHeadsUp = true;
     }
 
@@ -170,7 +175,7 @@
      * {@link NotificationInterruptStateProviderImpl#shouldHeadsUp(NotificationEntry)} will
      * pass as long its provided NotificationEntry fulfills importance & DND checks.
      */
-    private void ensureStateForHeadsUpWhenAwake() throws RemoteException {
+    private void ensureStateForHeadsUpWhenAwake() {
         when(mHeadsUpManager.isSnoozed(any())).thenReturn(false);
 
         when(mStatusBarStateController.isDozing()).thenReturn(false);
@@ -208,7 +213,7 @@
     }
 
     @Test
-    public void testShouldHeadsUpAwake() throws RemoteException {
+    public void testShouldHeadsUpAwake() {
         ensureStateForHeadsUpWhenAwake();
 
         NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
@@ -216,7 +221,7 @@
     }
 
     @Test
-    public void testShouldNotHeadsUp_suppressedForGroups() throws RemoteException {
+    public void testShouldNotHeadsUp_suppressedForGroups() {
         // GIVEN state for "heads up when awake" is true
         ensureStateForHeadsUpWhenAwake();
 
@@ -315,7 +320,7 @@
     }
 
     @Test
-    public void testShouldHeadsUp() throws RemoteException {
+    public void testShouldHeadsUp() {
         ensureStateForHeadsUpWhenAwake();
 
         NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
@@ -327,7 +332,7 @@
      * the bubble is shown rather than the heads up.
      */
     @Test
-    public void testShouldNotHeadsUp_bubble() throws RemoteException {
+    public void testShouldNotHeadsUp_bubble() {
         ensureStateForHeadsUpWhenAwake();
 
         // Bubble bit only applies to interruption when we're in the shade
@@ -337,10 +342,26 @@
     }
 
     /**
+     * If the notification is a bubble, and the user is not on AOD / lockscreen, but a bubble
+     * notification can't be shown, then show the heads up.
+     */
+    @Test
+    public void testShouldHeadsUp_bubble_bubblesCannotShowNotification() {
+        ensureStateForHeadsUpWhenAwake();
+
+        // Bubble bit only applies to interruption when we're in the shade
+        when(mStatusBarStateController.getState()).thenReturn(SHADE);
+
+        when(mBubbles.canShowBubbleNotification()).thenReturn(false);
+
+        assertThat(mNotifInterruptionStateProvider.shouldHeadsUp(createBubble())).isTrue();
+    }
+
+    /**
      * If we're not allowed to alert in general, we shouldn't be shown as heads up.
      */
     @Test
-    public void testShouldNotHeadsUp_filtered() throws RemoteException {
+    public void testShouldNotHeadsUp_filtered() {
         ensureStateForHeadsUpWhenAwake();
         // Make canAlertCommon false by saying it's filtered out
         when(mKeyguardNotificationVisibilityProvider.shouldHideNotification(any()))
@@ -355,7 +376,7 @@
      * {@link android.app.NotificationManager.Policy#SUPPRESSED_EFFECT_PEEK}.
      */
     @Test
-    public void testShouldNotHeadsUp_suppressPeek() throws RemoteException {
+    public void testShouldNotHeadsUp_suppressPeek() {
         ensureStateForHeadsUpWhenAwake();
 
         NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
@@ -371,7 +392,7 @@
      * to show as a heads up.
      */
     @Test
-    public void testShouldNotHeadsUp_lessImportant() throws RemoteException {
+    public void testShouldNotHeadsUp_lessImportant() {
         ensureStateForHeadsUpWhenAwake();
 
         NotificationEntry entry = createNotification(IMPORTANCE_DEFAULT);
@@ -382,7 +403,7 @@
      * If the device is not in use then we shouldn't be shown as heads up.
      */
     @Test
-    public void testShouldNotHeadsUp_deviceNotInUse() throws RemoteException {
+    public void testShouldNotHeadsUp_deviceNotInUse() {
         ensureStateForHeadsUpWhenAwake();
         NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
 
@@ -397,7 +418,7 @@
     }
 
     @Test
-    public void testShouldNotHeadsUp_headsUpSuppressed() throws RemoteException {
+    public void testShouldNotHeadsUp_headsUpSuppressed() {
         ensureStateForHeadsUpWhenAwake();
 
         // If a suppressor is suppressing heads up, then it shouldn't be shown as a heads up.
@@ -408,7 +429,7 @@
     }
 
     @Test
-    public void testShouldNotHeadsUpAwake_awakeInterruptsSuppressed() throws RemoteException {
+    public void testShouldNotHeadsUpAwake_awakeInterruptsSuppressed() {
         ensureStateForHeadsUpWhenAwake();
 
         // If a suppressor is suppressing heads up, then it shouldn't be shown as a heads up.
@@ -446,7 +467,7 @@
     }
 
     @Test
-    public void testShouldHeadsUp_oldWhen_whenNow() throws Exception {
+    public void testShouldHeadsUp_oldWhen_whenNow() {
         ensureStateForHeadsUpWhenAwake();
 
         NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
@@ -458,7 +479,7 @@
     }
 
     @Test
-    public void testShouldHeadsUp_oldWhen_whenRecent() throws Exception {
+    public void testShouldHeadsUp_oldWhen_whenRecent() {
         ensureStateForHeadsUpWhenAwake();
 
         NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
@@ -472,7 +493,7 @@
 
     @Test
     @DisableFlags(Flags.FLAG_SORT_SECTION_BY_TIME)
-    public void testShouldHeadsUp_oldWhen_whenZero() throws Exception {
+    public void testShouldHeadsUp_oldWhen_whenZero() {
         ensureStateForHeadsUpWhenAwake();
 
         NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
@@ -486,7 +507,7 @@
     }
 
     @Test
-    public void testShouldHeadsUp_oldWhen_whenNegative() throws Exception {
+    public void testShouldHeadsUp_oldWhen_whenNegative() {
         ensureStateForHeadsUpWhenAwake();
 
         NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
@@ -499,7 +520,7 @@
     }
 
     @Test
-    public void testShouldHeadsUp_oldWhen_hasFullScreenIntent() throws Exception {
+    public void testShouldHeadsUp_oldWhen_hasFullScreenIntent() {
         ensureStateForHeadsUpWhenAwake();
         long when = makeWhenHoursAgo(25);
 
@@ -514,7 +535,7 @@
     }
 
     @Test
-    public void testShouldHeadsUp_oldWhen_isForegroundService() throws Exception {
+    public void testShouldHeadsUp_oldWhen_isForegroundService() {
         ensureStateForHeadsUpWhenAwake();
         long when = makeWhenHoursAgo(25);
 
@@ -529,7 +550,7 @@
     }
 
     @Test
-    public void testShouldNotHeadsUp_oldWhen() throws Exception {
+    public void testShouldNotHeadsUp_oldWhen() {
         ensureStateForHeadsUpWhenAwake();
         long when = makeWhenHoursAgo(25);
 
@@ -543,7 +564,7 @@
     }
 
     @Test
-    public void testShouldNotFullScreen_notPendingIntent() throws RemoteException {
+    public void testShouldNotFullScreen_notPendingIntent() {
         NotificationEntry entry = createNotification(IMPORTANCE_HIGH);
         when(mPowerManager.isInteractive()).thenReturn(true);
         when(mStatusBarStateController.isDreaming()).thenReturn(false);
@@ -559,7 +580,7 @@
     }
 
     @Test
-    public void testShouldNotFullScreen_suppressedOnlyByDND() throws RemoteException {
+    public void testShouldNotFullScreen_suppressedOnlyByDND() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         modifyRanking(entry)
                 .setSuppressedVisualEffects(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT)
@@ -578,7 +599,7 @@
     }
 
     @Test
-    public void testShouldNotFullScreen_suppressedByDNDAndOther() throws RemoteException {
+    public void testShouldNotFullScreen_suppressedByDNDAndOther() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_LOW, /* silenced */ false);
         modifyRanking(entry)
                 .setSuppressedVisualEffects(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT)
@@ -597,7 +618,7 @@
     }
 
     @Test
-    public void testShouldNotFullScreen_notHighImportance() throws RemoteException {
+    public void testShouldNotFullScreen_notHighImportance() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_DEFAULT, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
         when(mStatusBarStateController.isDreaming()).thenReturn(false);
@@ -613,7 +634,7 @@
     }
 
     @Test
-    public void testShouldNotFullScreen_isGroupAlertSilenced() throws RemoteException {
+    public void testShouldNotFullScreen_isGroupAlertSilenced() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ true);
         when(mPowerManager.isInteractive()).thenReturn(false);
         when(mStatusBarStateController.isDreaming()).thenReturn(true);
@@ -664,7 +685,7 @@
     }
 
     @Test
-    public void testShouldFullScreen_notInteractive() throws RemoteException {
+    public void testShouldFullScreen_notInteractive() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder("foo")
                 .setSuppressNotification(false).build();
@@ -683,7 +704,7 @@
     }
 
     @Test
-    public void testShouldFullScreen_isDreaming() throws RemoteException {
+    public void testShouldFullScreen_isDreaming() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
         when(mStatusBarStateController.isDreaming()).thenReturn(true);
@@ -699,7 +720,7 @@
     }
 
     @Test
-    public void testShouldFullScreen_onKeyguard() throws RemoteException {
+    public void testShouldFullScreen_onKeyguard() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
         when(mStatusBarStateController.isDreaming()).thenReturn(false);
@@ -734,7 +755,7 @@
     }
 
     @Test
-    public void testShouldNotFullScreen_willHun() throws RemoteException {
+    public void testShouldNotFullScreen_willHun() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
         when(mPowerManager.isScreenOn()).thenReturn(true);
@@ -751,7 +772,7 @@
     }
 
     @Test
-    public void testShouldNotFullScreen_snoozed_occluding() throws Exception {
+    public void testShouldNotFullScreen_snoozed_occluding() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
         when(mPowerManager.isScreenOn()).thenReturn(true);
@@ -771,7 +792,7 @@
     }
 
     @Test
-    public void testShouldHeadsUp_snoozed_occluding() throws Exception {
+    public void testShouldHeadsUp_snoozed_occluding() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
         when(mPowerManager.isScreenOn()).thenReturn(true);
@@ -795,7 +816,7 @@
     }
 
     @Test
-    public void testShouldNotFullScreen_snoozed_lockedShade() throws Exception {
+    public void testShouldNotFullScreen_snoozed_lockedShade() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
         when(mPowerManager.isScreenOn()).thenReturn(true);
@@ -815,7 +836,7 @@
     }
 
     @Test
-    public void testShouldHeadsUp_snoozed_lockedShade() throws Exception {
+    public void testShouldHeadsUp_snoozed_lockedShade() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
         when(mPowerManager.isScreenOn()).thenReturn(true);
@@ -839,7 +860,7 @@
     }
 
     @Test
-    public void testShouldNotFullScreen_snoozed_unlocked() throws Exception {
+    public void testShouldNotFullScreen_snoozed_unlocked() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
         when(mPowerManager.isScreenOn()).thenReturn(true);
@@ -859,7 +880,7 @@
     }
 
     @Test
-    public void testShouldNotScreen_appSuspended() throws RemoteException {
+    public void testShouldNotScreen_appSuspended() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(false);
         when(mStatusBarStateController.isDreaming()).thenReturn(false);
@@ -902,7 +923,7 @@
     }
 
     @Test
-    public void testShouldHeadsUp_snoozed_unlocked() throws Exception {
+    public void testShouldHeadsUp_snoozed_unlocked() {
         NotificationEntry entry = createFsiNotification(IMPORTANCE_HIGH, /* silenced */ false);
         when(mPowerManager.isInteractive()).thenReturn(true);
         when(mPowerManager.isScreenOn()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
index a6177e8..6c7a95f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderWrapperTest.kt
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.DecisionImpl
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl
+import java.util.Optional
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -55,7 +56,8 @@
                 deviceProvisionedController,
                 systemClock,
                 globalSettings,
-                eventLog
+                eventLog,
+                Optional.of(bubbles)
             )
         )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
index eeb51a6..7903a73 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImplTest.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE
+import java.util.Optional
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.anyString
@@ -56,7 +57,8 @@
             userTracker,
             avalancheProvider,
             systemSettings,
-            packageManager
+            packageManager,
+            Optional.of(bubbles)
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index 71e7dc5..a457405 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -75,8 +75,6 @@
 import com.android.systemui.statusbar.policy.FakeDeviceProvisionedController
 import com.android.systemui.statusbar.policy.HeadsUpManager
 import com.android.systemui.util.FakeEventLog
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.FakeGlobalSettings
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.settings.SystemSettings
@@ -85,12 +83,15 @@
 import com.android.systemui.utils.leaks.FakeKeyguardStateController
 import com.android.systemui.utils.leaks.LeakCheckedTest
 import com.android.systemui.utils.os.FakeHandler
+import com.android.wm.shell.bubbles.Bubbles
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
-import org.mockito.Mockito.`when` as whenever
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 abstract class VisualInterruptionDecisionProviderTestBase : SysuiTestCase() {
     private val fakeLogBuffer =
@@ -129,6 +130,7 @@
     protected val uiEventLogger = UiEventLoggerFake()
     protected val userTracker = FakeUserTracker()
     protected val avalancheProvider: AvalancheProvider = mock()
+    protected val bubbles: Bubbles = mock()
     lateinit var systemSettings: SystemSettings
     protected val packageManager: PackageManager = mock()
 
@@ -159,6 +161,7 @@
         deviceProvisionedController.currentUser = userId
         userTracker.set(listOf(user), /* currentUserIndex = */ 0)
         systemSettings = FakeSettings()
+        whenever(bubbles.canShowBubbleNotification()).thenReturn(true)
 
         provider.start()
     }
@@ -208,6 +211,14 @@
     }
 
     @Test
+    fun testShouldPeek_bubblesCannotShowNotification() {
+        whenever(bubbles.canShowBubbleNotification()).thenReturn(false)
+        ensurePeekState { statusBarState = SHADE }
+        assertShouldHeadsUp(buildPeekEntry { isBubble = true })
+        assertNoEventsLogged()
+    }
+
+    @Test
     fun testShouldPeek_isBubble_shadeLocked() {
         ensurePeekState { statusBarState = SHADE_LOCKED }
         assertShouldHeadsUp(buildPeekEntry { isBubble = true })
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
index 61c008b..01e638b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestUtil.kt
@@ -32,6 +32,8 @@
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.settings.SystemSettings
 import com.android.systemui.util.time.SystemClock
+import com.android.wm.shell.bubbles.Bubbles
+import java.util.Optional
 
 object VisualInterruptionDecisionProviderTestUtil {
     fun createProviderByFlag(
@@ -55,6 +57,7 @@
         avalancheProvider: AvalancheProvider,
         systemSettings: SystemSettings,
         packageManager: PackageManager,
+        bubbles: Optional<Bubbles>,
     ): VisualInterruptionDecisionProvider {
         return if (VisualInterruptionRefactor.isEnabled) {
             VisualInterruptionDecisionProviderImpl(
@@ -75,7 +78,8 @@
                 userTracker,
                 avalancheProvider,
                 systemSettings,
-                packageManager
+                packageManager,
+                bubbles
             )
         } else {
             NotificationInterruptStateProviderWrapper(
@@ -95,7 +99,8 @@
                     deviceProvisionedController,
                     systemClock,
                     globalSettings,
-                    eventLog
+                    eventLog,
+                    bubbles
                 )
             )
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index cde241b..62804ed1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -172,7 +172,6 @@
 import com.android.systemui.statusbar.notification.interruption.AvalancheProvider;
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
-import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl;
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionLogger;
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider;
 import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProviderTestUtil;
@@ -189,7 +188,6 @@
 import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
-import com.android.systemui.util.EventLog;
 import com.android.systemui.util.FakeEventLog;
 import com.android.systemui.util.WallpaperController;
 import com.android.systemui.util.concurrency.FakeExecutor;
@@ -197,10 +195,8 @@
 import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.settings.FakeGlobalSettings;
 import com.android.systemui.util.settings.FakeSettings;
-import com.android.systemui.util.settings.GlobalSettings;
 import com.android.systemui.util.settings.SystemSettings;
 import com.android.systemui.util.time.FakeSystemClock;
-import com.android.systemui.util.time.SystemClock;
 import com.android.systemui.volume.VolumeComponent;
 import com.android.wm.shell.bubbles.Bubbles;
 import com.android.wm.shell.startingsurface.StartingSurface;
@@ -374,6 +370,8 @@
 
         mFakeGlobalSettings.putInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_ON);
 
+        when(mBubbles.canShowBubbleNotification()).thenReturn(true);
+
         mVisualInterruptionDecisionProvider =
                 VisualInterruptionDecisionProviderTestUtil.INSTANCE.createProviderByFlag(
                         mAmbientDisplayConfiguration,
@@ -395,7 +393,8 @@
                         mUserTracker,
                         mAvalancheProvider,
                         mSystemSettings,
-                        mPackageManager);
+                        mPackageManager,
+                        Optional.of(mBubbles));
         mVisualInterruptionDecisionProvider.start();
 
         mContext.addMockSystemService(TrustManager.class, mock(TrustManager.class));
@@ -1418,46 +1417,4 @@
         verify(mStatusBarStateController).addCallback(callbackCaptor.capture(), anyInt());
         callbackCaptor.getValue().onDozingChanged(isDozing);
     }
-
-    public static class TestableNotificationInterruptStateProviderImpl extends
-            NotificationInterruptStateProviderImpl {
-
-        TestableNotificationInterruptStateProviderImpl(
-                PowerManager powerManager,
-                AmbientDisplayConfiguration ambientDisplayConfiguration,
-                StatusBarStateController controller,
-                KeyguardStateController keyguardStateController,
-                BatteryController batteryController,
-                HeadsUpManager headsUpManager,
-                NotificationInterruptLogger logger,
-                Handler mainHandler,
-                NotifPipelineFlags flags,
-                KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
-                UiEventLogger uiEventLogger,
-                UserTracker userTracker,
-                DeviceProvisionedController deviceProvisionedController,
-                SystemClock systemClock,
-                GlobalSettings globalSettings,
-                EventLog eventLog) {
-            super(
-                    powerManager,
-                    ambientDisplayConfiguration,
-                    batteryController,
-                    controller,
-                    keyguardStateController,
-                    headsUpManager,
-                    logger,
-                    mainHandler,
-                    flags,
-                    keyguardNotificationVisibilityProvider,
-                    uiEventLogger,
-                    userTracker,
-                    deviceProvisionedController,
-                    systemClock,
-                    globalSettings,
-                    eventLog
-            );
-            mUseHeadsUp = true;
-        }
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt
index 7ca3b1c..6300953 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.satellite.data
 
+import android.telephony.TelephonyManager
 import android.telephony.satellite.SatelliteManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -50,11 +51,13 @@
     private val demoModeController =
         mock<DemoModeController>().apply { whenever(this.isInDemoMode).thenReturn(false) }
     private val satelliteManager = mock<SatelliteManager>()
+    private val telephonyManager = mock<TelephonyManager>()
     private val systemClock = FakeSystemClock()
 
     private val realImpl =
         DeviceBasedSatelliteRepositoryImpl(
             Optional.of(satelliteManager),
+            telephonyManager,
             testDispatcher,
             testScope.backgroundScope,
             FakeLogBuffer.Factory.create(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 6b0ad4b..6651676 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -18,6 +18,8 @@
 
 import android.os.OutcomeReceiver
 import android.os.Process
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyManager
 import android.telephony.satellite.NtnSignalStrength
 import android.telephony.satellite.NtnSignalStrengthCallback
 import android.telephony.satellite.SatelliteManager
@@ -36,6 +38,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.log.core.FakeLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
 import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
@@ -59,6 +62,7 @@
 import org.mockito.Mockito
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.never
+import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
@@ -69,6 +73,7 @@
     private lateinit var underTest: DeviceBasedSatelliteRepositoryImpl
 
     @Mock private lateinit var satelliteManager: SatelliteManager
+    @Mock private lateinit var telephonyManager: TelephonyManager
 
     private val systemClock = FakeSystemClock()
     private val dispatcher = StandardTestDispatcher()
@@ -86,6 +91,7 @@
             underTest =
                 DeviceBasedSatelliteRepositoryImpl(
                     Optional.empty(),
+                    telephonyManager,
                     dispatcher,
                     testScope.backgroundScope,
                     FakeLogBuffer.Factory.create(),
@@ -362,6 +368,68 @@
             verify(satelliteManager).registerForModemStateChanged(any(), any())
         }
 
+    @Test
+    fun telephonyCrash_repoReregistersConnectionStateListener() =
+        testScope.runTest {
+            setupDefaultRepo()
+
+            // GIVEN connection state is requested
+            val connectionState by collectLastValue(underTest.connectionState)
+
+            runCurrent()
+
+            val telephonyCallback =
+                MobileTelephonyHelpers.getTelephonyCallbackForType<
+                    TelephonyCallback.RadioPowerStateListener
+                >(
+                    telephonyManager
+                )
+
+            // THEN listener is registered once
+            verify(satelliteManager, times(1)).registerForModemStateChanged(any(), any())
+
+            // WHEN a crash event happens (detected by radio state change)
+            telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_ON)
+            runCurrent()
+            telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_OFF)
+            runCurrent()
+
+            // THEN listeners are unregistered and re-registered
+            verify(satelliteManager, times(1)).unregisterForModemStateChanged(any())
+            verify(satelliteManager, times(2)).registerForModemStateChanged(any(), any())
+        }
+
+    @Test
+    fun telephonyCrash_repoReregistersSignalStrengthListener() =
+        testScope.runTest {
+            setupDefaultRepo()
+
+            // GIVEN signal strength is requested
+            val signalStrength by collectLastValue(underTest.signalStrength)
+
+            runCurrent()
+
+            val telephonyCallback =
+                MobileTelephonyHelpers.getTelephonyCallbackForType<
+                    TelephonyCallback.RadioPowerStateListener
+                >(
+                    telephonyManager
+                )
+
+            // THEN listeners are registered the first time
+            verify(satelliteManager, times(1)).registerForNtnSignalStrengthChanged(any(), any())
+
+            // WHEN a crash event happens (detected by radio state change)
+            telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_ON)
+            runCurrent()
+            telephonyCallback.onRadioPowerStateChanged(TelephonyManager.RADIO_POWER_OFF)
+            runCurrent()
+
+            // THEN listeners are unregistered and re-registered
+            verify(satelliteManager, times(1)).unregisterForNtnSignalStrengthChanged(any())
+            verify(satelliteManager, times(2)).registerForNtnSignalStrengthChanged(any(), any())
+        }
+
     private fun setUpRepo(
         uptime: Long = MIN_UPTIME,
         satMan: SatelliteManager? = satelliteManager,
@@ -380,6 +448,7 @@
         underTest =
             DeviceBasedSatelliteRepositoryImpl(
                 if (satMan != null) Optional.of(satMan) else Optional.empty(),
+                telephonyManager,
                 dispatcher,
                 testScope.backgroundScope,
                 FakeLogBuffer.Factory.create(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 7b0a556..0f89dcc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -152,6 +152,7 @@
 import com.android.systemui.util.settings.FakeGlobalSettings;
 import com.android.systemui.util.settings.SystemSettings;
 import com.android.systemui.util.time.SystemClock;
+import com.android.wm.shell.Flags;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.WindowManagerShellWrapper;
 import com.android.wm.shell.bubbles.Bubble;
@@ -468,7 +469,8 @@
                         mock(UserTracker.class),
                         mock(AvalancheProvider.class),
                         mock(SystemSettings.class),
-                        mock(PackageManager.class)
+                        mock(PackageManager.class),
+                        Optional.of(mock(Bubbles.class))
                         );
         interruptionDecisionProvider.start();
 
@@ -2299,6 +2301,66 @@
         assertThat(bubbleStateListener.mStateChangeCalls).isEqualTo(0);
     }
 
+    @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+    @Test
+    public void showBubbleOverflow_hasOverflowContents() {
+        mEntryListener.onEntryAdded(mRow);
+        mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true);
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+
+        BubbleStackView stackView = mBubbleController.getStackView();
+        spyOn(stackView);
+
+        // Dismiss the bubble so it's in the overflow
+        mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
+        assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+
+        verify(stackView).showOverflow(eq(true));
+    }
+
+    @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+    @Test
+    public void showBubbleOverflow_isEmpty() {
+        mEntryListener.onEntryAdded(mRow);
+        mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true);
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+
+        BubbleStackView stackView = mBubbleController.getStackView();
+        spyOn(stackView);
+
+        // Dismiss the bubble so it's in the overflow
+        mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
+        assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+        verify(stackView).showOverflow(eq(true));
+
+        // Cancel the bubble so it's removed from the overflow
+        mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+        verify(stackView).showOverflow(eq(false));
+    }
+
+    @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+    @Test
+    public void showBubbleOverflow_ignored() {
+        mEntryListener.onEntryAdded(mRow);
+        mEntryListener.onEntryUpdated(mRow, /* fromSystem= */ true);
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+
+        BubbleStackView stackView = mBubbleController.getStackView();
+        spyOn(stackView);
+
+        // Dismiss the bubble so it's in the overflow
+        mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_USER_GESTURE);
+        assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+
+        // Cancel the bubble so it's removed from the overflow
+        mBubbleController.removeBubble(mRow.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
+        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+
+        // Show overflow should never be called if the flag is off
+        verify(stackView, never()).showOverflow(anyBoolean());
+    }
+
     /** Creates a bubble using the userId and package. */
     private Bubble createBubble(int userId, String pkg) {
         final UserHandle userHandle = new UserHandle(userId);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
deleted file mode 100644
index c9964c2..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2020 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.systemui.wmshell;
-
-import android.hardware.display.AmbientDisplayConfiguration;
-import android.os.Handler;
-import android.os.PowerManager;
-
-import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
-import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
-import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
-import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl;
-import com.android.systemui.statusbar.policy.BatteryController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.EventLog;
-import com.android.systemui.util.settings.GlobalSettings;
-import com.android.systemui.util.time.SystemClock;
-
-public class TestableNotificationInterruptStateProviderImpl
-        extends NotificationInterruptStateProviderImpl {
-
-    TestableNotificationInterruptStateProviderImpl(
-            PowerManager powerManager,
-            AmbientDisplayConfiguration ambientDisplayConfiguration,
-            StatusBarStateController statusBarStateController,
-            KeyguardStateController keyguardStateController,
-            BatteryController batteryController,
-            HeadsUpManager headsUpManager,
-            NotificationInterruptLogger logger,
-            Handler mainHandler,
-            NotifPipelineFlags flags,
-            KeyguardNotificationVisibilityProvider keyguardNotificationVisibilityProvider,
-            UiEventLogger uiEventLogger,
-            UserTracker userTracker,
-            DeviceProvisionedController deviceProvisionedController,
-            SystemClock systemClock,
-            GlobalSettings globalSettings,
-            EventLog eventLog) {
-        super(
-                powerManager,
-                ambientDisplayConfiguration,
-                batteryController,
-                statusBarStateController,
-                keyguardStateController,
-                headsUpManager,
-                logger,
-                mainHandler,
-                flags,
-                keyguardNotificationVisibilityProvider,
-                uiEventLogger,
-                userTracker,
-                deviceProvisionedController,
-                systemClock,
-                globalSettings,
-                eventLog);
-        mUseHeadsUp = true;
-    }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
index 070a369..f75cdd4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
@@ -26,15 +26,15 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.plugins.statusbar.statusBarStateController
-import com.android.systemui.statusbar.policy.KeyguardStateControllerImpl
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.statusbar.policy.keyguardStateController
 import com.android.systemui.util.time.systemClock
 
-var Kosmos.alternateBouncerInteractor by
+val Kosmos.alternateBouncerInteractor: AlternateBouncerInteractor by
     Kosmos.Fixture {
         AlternateBouncerInteractor(
             statusBarStateController = statusBarStateController,
-            keyguardStateController = mock<KeyguardStateControllerImpl>(),
+            keyguardStateController = keyguardStateController,
             bouncerRepository = keyguardBouncerRepository,
             fingerprintPropertyRepository = fingerprintPropertyRepository,
             biometricSettingsRepository = biometricSettingsRepository,
@@ -44,5 +44,6 @@
             keyguardInteractor = { keyguardInteractor },
             keyguardTransitionInteractor = { keyguardTransitionInteractor },
             scope = testScope.backgroundScope,
+            sceneInteractor = { sceneInteractor },
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt
index 482d60c..c7955c3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt
@@ -21,7 +21,7 @@
 import com.android.systemui.kosmos.applicationCoroutineScope
 
 val Kosmos.fakeCommunalRepository by Fixture {
-    FakeCommunalRepository(applicationScope = applicationCoroutineScope)
+    FakeCommunalSceneRepository(applicationScope = applicationCoroutineScope)
 }
 
-val Kosmos.communalRepository by Fixture<CommunalRepository> { fakeCommunalRepository }
+val Kosmos.communalSceneRepository by Fixture<CommunalSceneRepository> { fakeCommunalRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
similarity index 88%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
index d958bae..a7bf87d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
@@ -14,14 +14,17 @@
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
 
-/** Fake implementation of [CommunalRepository]. */
+/** Fake implementation of [CommunalSceneRepository]. */
 @OptIn(ExperimentalCoroutinesApi::class)
-class FakeCommunalRepository(
+class FakeCommunalSceneRepository(
     applicationScope: CoroutineScope,
     override val currentScene: MutableStateFlow<SceneKey> =
         MutableStateFlow(CommunalScenes.Default),
-) : CommunalRepository {
-    override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
+) : CommunalSceneRepository {
+    override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) =
+        snapToScene(toScene)
+
+    override fun snapToScene(toScene: SceneKey) {
         this.currentScene.value = toScene
         this._transitionState.value = flowOf(ObservableTransitionState.Idle(toScene))
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index 3fe6973..1583d1c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -20,7 +20,6 @@
 import com.android.systemui.broadcast.broadcastDispatcher
 import com.android.systemui.communal.data.repository.communalMediaRepository
 import com.android.systemui.communal.data.repository.communalPrefsRepository
-import com.android.systemui.communal.data.repository.communalRepository
 import com.android.systemui.communal.data.repository.communalWidgetRepository
 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
 import com.android.systemui.flags.Flags
@@ -45,7 +44,7 @@
         applicationScope = applicationCoroutineScope,
         bgDispatcher = testDispatcher,
         broadcastDispatcher = broadcastDispatcher,
-        communalRepository = communalRepository,
+        communalSceneInteractor = communalSceneInteractor,
         widgetRepository = communalWidgetRepository,
         communalPrefsRepository = communalPrefsRepository,
         mediaRepository = communalMediaRepository,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt
new file mode 100644
index 0000000..ee48c10
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.domain.interactor
+
+import com.android.systemui.communal.data.repository.communalSceneRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.communalSceneInteractor: CommunalSceneInteractor by
+    Kosmos.Fixture {
+        CommunalSceneInteractor(
+            applicationScope = applicationCoroutineScope,
+            communalSceneRepository = communalSceneRepository,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
index 93e0b41..d558c96 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFingerprintAuthRepository.kt
@@ -40,6 +40,9 @@
     private val _isRunning = MutableStateFlow(false)
     override val isRunning: Flow<Boolean>
         get() = _isRunning
+
+    override val isEngaged: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
     fun setIsRunning(value: Boolean) {
         _isRunning.value = value
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 96a4049..1a45c42 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -125,6 +125,9 @@
     private val _isEncryptedOrLockdown = MutableStateFlow(true)
     override val isEncryptedOrLockdown: Flow<Boolean> = _isEncryptedOrLockdown
 
+    private val _isKeyguardEnabled = MutableStateFlow(true)
+    override val isKeyguardEnabled: StateFlow<Boolean> = _isKeyguardEnabled.asStateFlow()
+
     override val topClippingBounds = MutableStateFlow<Int?>(null)
 
     override fun setQuickSettingsVisible(isVisible: Boolean) {
@@ -184,6 +187,10 @@
         _clockShouldBeCentered.value = shouldBeCentered
     }
 
+    override fun setKeyguardEnabled(enabled: Boolean) {
+        _isKeyguardEnabled.value = enabled
+    }
+
     fun dozeTimeTick(millis: Long) {
         _dozeTimeTick.value = millis
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
index bbe37c1..42af25e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
@@ -34,5 +35,6 @@
             keyguardInteractor = keyguardInteractor,
             powerInteractor = powerInteractor,
             keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+            deviceEntryRepository = deviceEntryRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
index 23dcd96..edf77a0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
@@ -36,5 +37,6 @@
             communalInteractor = communalInteractor,
             powerInteractor = powerInteractor,
             keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+            deviceEntryRepository = deviceEntryRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
index 604d9e4..4039ee6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.communal.domain.interactor.communalInteractor
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
@@ -38,5 +39,7 @@
             communalInteractor = communalInteractor,
             keyguardOcclusionInteractor = keyguardOcclusionInteractor,
             biometricSettingsRepository = biometricSettingsRepository,
+            keyguardRepository = keyguardRepository,
+            keyguardEnabledInteractor = keyguardEnabledInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt
index 7eef704..be8048e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt
@@ -36,6 +36,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.power.data.repository.FakePowerRepository
 import com.android.systemui.power.domain.interactor.PowerInteractorFactory
+import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.user.data.repository.FakeUserRepository
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
@@ -89,6 +90,7 @@
                 { mock(DeviceEntryFingerprintAuthInteractor::class.java) },
                 { mock(KeyguardInteractor::class.java) },
                 { mock(KeyguardTransitionInteractor::class.java) },
+                { mock(SceneInteractor::class.java) },
                 testScope.backgroundScope,
             )
         val powerInteractorWithDeps =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractorKosmos.kt
new file mode 100644
index 0000000..0667a6b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardEnabledInteractorKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.domain.interactor
+
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+
+val Kosmos.keyguardEnabledInteractor by
+    Kosmos.Fixture {
+        KeyguardEnabledInteractor(
+            applicationCoroutineScope,
+            keyguardRepository,
+            biometricSettingsRepository,
+            keyguardTransitionInteractor,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt
index d344b75..5b1f95a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/taskswitcher/TaskSwitcherKosmos.kt
@@ -20,8 +20,8 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.mediaprojection.data.repository.MediaProjectionManagerRepository
 import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
-import com.android.systemui.mediaprojection.taskswitcher.data.repository.MediaProjectionManagerRepository
 import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
 import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
 import kotlinx.coroutines.ExperimentalCoroutinesApi
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconLabelVisibilityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconLabelVisibilityRepositoryKosmos.kt
new file mode 100644
index 0000000..277dbb7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconLabelVisibilityRepositoryKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.iconLabelVisibilityRepository by Kosmos.Fixture { IconLabelVisibilityRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconLabelVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconLabelVisibilityInteractorKosmos.kt
new file mode 100644
index 0000000..7b9e4a1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/IconLabelVisibilityInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.log.core.FakeLogBuffer
+import com.android.systemui.qs.panels.data.repository.iconLabelVisibilityRepository
+
+val Kosmos.iconLabelVisibilityInteractor by
+    Kosmos.Fixture {
+        IconLabelVisibilityInteractor(
+            iconLabelVisibilityRepository,
+            FakeLogBuffer.Factory.create(),
+            applicationCoroutineScope
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
index 34b266a..82cfaf5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
@@ -18,6 +18,8 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.infiniteGridSizeViewModel
 
 val Kosmos.infiniteGridLayout by
-    Kosmos.Fixture { InfiniteGridLayout(iconTilesInteractor, infiniteGridSizeInteractor) }
+    Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, infiniteGridSizeViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PartitionedGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PartitionedGridLayoutKosmos.kt
index 4febfe91..37c9552 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PartitionedGridLayoutKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PartitionedGridLayoutKosmos.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.panels.ui.compose.PartitionedGridLayout
+import com.android.systemui.qs.panels.ui.viewmodel.partitionedGridViewModel
 
 val Kosmos.partitionedGridLayout by
-    Kosmos.Fixture { PartitionedGridLayout(iconTilesInteractor, infiniteGridSizeInteractor) }
+    Kosmos.Fixture { PartitionedGridLayout(partitionedGridViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/IconLabelVisibilityViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/IconLabelVisibilityViewModelKosmos.kt
new file mode 100644
index 0000000..daf6087
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/IconLabelVisibilityViewModelKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.domain.interactor.iconLabelVisibilityInteractor
+
+val Kosmos.iconLabelVisibilityViewModel by
+    Kosmos.Fixture { IconLabelVisibilityViewModelImpl(iconLabelVisibilityInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModelKosmos.kt
new file mode 100644
index 0000000..89b42a6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/IconTilesViewModelKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.domain.interactor.iconTilesInteractor
+
+val Kosmos.iconTilesViewModel by Kosmos.Fixture { IconTilesViewModelImpl(iconTilesInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
new file mode 100644
index 0000000..f6dfb8b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.domain.interactor.infiniteGridSizeInteractor
+
+val Kosmos.infiniteGridSizeViewModel by
+    Kosmos.Fixture { InfiniteGridSizeViewModelImpl(infiniteGridSizeInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
new file mode 100644
index 0000000..b07cc7d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.partitionedGridViewModel by
+    Kosmos.Fixture {
+        PartitionedGridViewModel(
+            iconTilesViewModel,
+            infiniteGridSizeViewModel,
+            iconLabelVisibilityViewModel,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt
index 4f5c9b4..5b6fd8c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt
@@ -45,6 +45,7 @@
             other ?: return
         }
         check("icon").that(actual.icon()).isEqualTo(other.icon())
+        check("iconRes").that(actual.iconRes).isEqualTo(other.iconRes)
         check("label").that(actual.label).isEqualTo(other.label)
         check("activationState").that(actual.activationState).isEqualTo(other.activationState)
         check("secondaryLabel").that(actual.secondaryLabel).isEqualTo(other.secondaryLabel)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
index e0f60e9..b6194e3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/adapter/FakeQSSceneAdapter.kt
@@ -19,8 +19,8 @@
 import android.content.Context
 import android.view.View
 import com.android.systemui.settings.brightness.MirrorController
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.filterNotNull
 
@@ -41,7 +41,7 @@
     override val customizerAnimationDuration = _animationDuration.asStateFlow()
 
     private val _view = MutableStateFlow<View?>(null)
-    override val qsView: Flow<View> = _view.filterNotNull()
+    override val qsView: StateFlow<View?> = _view.asStateFlow()
 
     private val _state = MutableStateFlow<QSSceneAdapter.State?>(null)
     val state = _state.filterNotNull()
@@ -64,6 +64,8 @@
         }
     }
 
+    override fun applyLatestExpansionAndSquishiness() {}
+
     fun setCustomizing(value: Boolean) {
         updateCustomizerFlows(if (value) CustomizerState.Showing else CustomizerState.Hidden)
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/FakeOngoingActivityChipInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/FakeOngoingActivityChipInteractor.kt
index cd08274..90d459b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/FakeOngoingActivityChipInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/FakeOngoingActivityChipInteractor.kt
@@ -17,15 +17,9 @@
 package com.android.systemui.statusbar.chips.ui.viewmodel
 
 import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor
-import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
-import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
 import kotlinx.coroutines.flow.MutableStateFlow
 
-class FakeScreenRecordChipInteractor : ScreenRecordChipInteractor() {
-    override val chip: MutableStateFlow<OngoingActivityChipModel> =
-        MutableStateFlow(OngoingActivityChipModel.Hidden)
-}
-
 class FakeCallChipInteractor : CallChipInteractor() {
     override val chip: MutableStateFlow<OngoingActivityChipModel> =
         MutableStateFlow(OngoingActivityChipModel.Hidden)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
index ffbaa7f..9e02df9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
@@ -17,10 +17,20 @@
 package com.android.systemui.statusbar.chips.ui.viewmodel
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.screenrecord.data.repository.screenRecordRepository
+import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
+import com.android.systemui.util.time.fakeSystemClock
 
-val Kosmos.screenRecordChipInteractor: FakeScreenRecordChipInteractor by
-    Kosmos.Fixture { FakeScreenRecordChipInteractor() }
+val Kosmos.screenRecordChipInteractor: ScreenRecordChipInteractor by
+    Kosmos.Fixture {
+        ScreenRecordChipInteractor(
+            scope = applicationCoroutineScope,
+            screenRecordRepository = screenRecordRepository,
+            systemClock = fakeSystemClock,
+        )
+    }
 
 val Kosmos.callChipInteractor: FakeCallChipInteractor by Kosmos.Fixture { FakeCallChipInteractor() }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt
index 0e909c4..f19ac1e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.policy
 
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.mockito.mock
+import org.mockito.Mockito.mock
 
-var Kosmos.keyguardStateController by Kosmos.Fixture { mock<KeyguardStateController>() }
+var Kosmos.keyguardStateController: KeyguardStateController by
+    Kosmos.Fixture { mock(KeyguardStateController::class.java) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt
new file mode 100644
index 0000000..2ba1211
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.data.repository
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.volumePanelGlobalStateRepository by
+    Kosmos.Fixture { VolumePanelGlobalStateRepository(dumpManager) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/VolumePanelGlobalStateInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/VolumePanelGlobalStateInteractorKosmos.kt
new file mode 100644
index 0000000..6e52364
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/VolumePanelGlobalStateInteractorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.panel.data.repository.volumePanelGlobalStateRepository
+
+val Kosmos.volumePanelGlobalStateInteractor: VolumePanelGlobalStateInteractor by
+    Kosmos.Fixture { VolumePanelGlobalStateInteractor(volumePanelGlobalStateRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt
index a606588..34a008f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.statusbar.policy.configurationController
 import com.android.systemui.volume.panel.dagger.factory.volumePanelComponentFactory
 import com.android.systemui.volume.panel.domain.VolumePanelStartable
+import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor
 
 var Kosmos.volumePanelStartables: Set<VolumePanelStartable> by Kosmos.Fixture { emptySet() }
 
@@ -36,5 +37,6 @@
             volumePanelComponentFactory,
             configurationController,
             broadcastDispatcher,
+            volumePanelGlobalStateInteractor,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorKosmos.kt
index 63b3f23..c3300f5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/ui/navigation/VolumeNavigatorKosmos.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.statusbar.phone.systemUIDialogFactory
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.volume.VolumePanelFactory
+import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor
 import com.android.systemui.volume.panel.ui.viewmodel.volumePanelViewModelFactory
 
 val Kosmos.volumeNavigator by
@@ -36,5 +37,6 @@
             volumePanelViewModelFactory,
             systemUIDialogFactory,
             uiEventLoggerFake,
+            volumePanelGlobalStateInteractor,
         )
     }
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 95cbb6b..48bc803 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -56,11 +56,52 @@
     visibility: ["//visibility:public"],
 }
 
+// This and the next module contain the same classes with different implementations.
+// "ravenwood-runtime-common-device" will be statically linked in device side tests.
+// "ravenwood-runtime-common-ravenwood" will only exist in ravenwood-runtime, which will take
+// precedence even if the test jar (accidentally) contains "ravenwood-runtime-common-device".
+// "ravenwood-runtime-common" uses it to detect if the rutime is Ravenwood or not.
+java_library {
+    name: "ravenwood-runtime-common-ravenwood",
+    host_supported: true,
+    sdk_version: "core_current",
+    srcs: [
+        "runtime-common-ravenwood-src/**/*.java",
+    ],
+    visibility: ["//frameworks/base"],
+}
+
+java_library {
+    name: "ravenwood-runtime-common-device",
+    host_supported: true,
+    sdk_version: "core_current",
+    srcs: [
+        "runtime-common-device-src/**/*.java",
+    ],
+    visibility: ["//visibility:private"],
+}
+
+java_library {
+    name: "ravenwood-runtime-common",
+    host_supported: true,
+    sdk_version: "core_current",
+    srcs: [
+        "runtime-common-src/**/*.java",
+    ],
+    libs: [
+        "ravenwood-runtime-common-ravenwood",
+    ],
+    visibility: ["//visibility:private"],
+}
+
 java_library_host {
     name: "ravenwood-helper-libcore-runtime.host",
     srcs: [
         "runtime-helper-src/libcore-fake/**/*.java",
     ],
+    static_libs: [
+        "ravenwood-runtime-common",
+    ],
     visibility: ["//visibility:private"],
 }
 
@@ -77,6 +118,9 @@
     srcs: [
         "runtime-helper-src/framework/**/*.java",
     ],
+    static_libs: [
+        "ravenwood-runtime-common",
+    ],
     libs: [
         "framework-minus-apex.ravenwood",
         "ravenwood-junit",
@@ -105,6 +149,7 @@
     ],
     static_libs: [
         "androidx.test.monitor-for-device",
+        "ravenwood-runtime-common",
     ],
     libs: [
         "android.test.mock",
@@ -145,6 +190,10 @@
         "junit-flag-src/**/*.java",
     ],
     sdk_version: "test_current",
+    static_libs: [
+        "ravenwood-runtime-common",
+        "ravenwood-runtime-common-device",
+    ],
     libs: [
         "junit",
         "flag-junit",
@@ -199,7 +248,7 @@
     ],
 
     srcs: [
-        "runtime-helper-src/jni/*.cpp",
+        "runtime-jni/*.cpp",
     ],
 
     shared_libs: [
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index 5506a46..49e793f 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -86,10 +86,6 @@
                 sPendingUncaughtException.compareAndSet(null, throwable);
             };
 
-    public static boolean isOnRavenwood() {
-        return true;
-    }
-
     public static void init(RavenwoodRule rule) {
         if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
             maybeThrowPendingUncaughtException(false);
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 9d12f85..68b5aeb 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -29,6 +29,8 @@
 import android.platform.test.annotations.EnabledOnRavenwood;
 import android.platform.test.annotations.IgnoreUnderRavenwood;
 
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
 import org.junit.Assume;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
@@ -54,7 +56,7 @@
  * before a test class is fully initialized.
  */
 public class RavenwoodRule implements TestRule {
-    static final boolean IS_ON_RAVENWOOD = RavenwoodRuleImpl.isOnRavenwood();
+    static final boolean IS_ON_RAVENWOOD = RavenwoodCommonUtils.isOnRavenwood();
 
     /**
      * When probing is enabled, all tests will be unconditionally run on Ravenwood to detect
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
index c3786ee..5f1b0c2 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
@@ -16,6 +16,8 @@
 
 package android.platform.test.ravenwood;
 
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_SYSPROP;
+
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -101,6 +103,8 @@
         setValue("ro.soc.model", "Ravenwood");
 
         setValue("ro.debuggable", "1");
+
+        setValue(RAVENWOOD_SYSPROP, "1");
     }
 
     /** Copy constructor */
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
index 99ab327..19c1bff 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
@@ -15,9 +15,7 @@
  */
 package android.platform.test.ravenwood;
 
-import java.io.File;
-import java.io.PrintStream;
-import java.util.Arrays;
+import com.android.ravenwood.common.RavenwoodCommonUtils;
 
 /**
  * Utilities for writing (bivalent) ravenwood tests.
@@ -26,15 +24,6 @@
     private RavenwoodUtils() {
     }
 
-    private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
-
-    // LibcoreRavenwoodUtils calls it with reflections.
-    public static void loadRavenwoodNativeRuntime() {
-        if (RavenwoodRule.isOnRavenwood()) {
-            RavenwoodUtils.loadJniLibrary(RAVENWOOD_NATIVE_RUNTIME_NAME);
-        }
-    }
-
     /**
      * Load a JNI library respecting {@code java.library.path}
      * (which reflects {@code LD_LIBRARY_PATH}).
@@ -56,85 +45,6 @@
      * it uses {@code JNI_OnLoad()} as the entry point name on both.
      */
     public static void loadJniLibrary(String libname) {
-        if (RavenwoodRule.isOnRavenwood()) {
-            loadLibraryOnRavenwood(libname);
-        } else {
-            // Just delegate to the loadLibrary().
-            System.loadLibrary(libname);
-        }
-    }
-
-    private static void loadLibraryOnRavenwood(String libname) {
-        var path = System.getProperty("java.library.path");
-        var filename = "lib" + libname + ".so";
-
-        System.out.println("Looking for library " + libname + ".so in java.library.path:" + path);
-
-        try {
-            if (path == null) {
-                throw new UnsatisfiedLinkError("Cannot load library " + libname + "."
-                        + " Property java.library.path not set!");
-            }
-            for (var dir : path.split(":")) {
-                var file = new File(dir + "/" + filename);
-                if (file.exists()) {
-                    System.load(file.getAbsolutePath());
-                    return;
-                }
-            }
-            throw new UnsatisfiedLinkError("Library " + libname + " not found in "
-                    + "java.library.path: " + path);
-        } catch (Throwable e) {
-            dumpFiles(System.out);
-            throw e;
-        }
-    }
-
-    private static void dumpFiles(PrintStream out) {
-        try {
-            var path = System.getProperty("java.library.path");
-            out.println("# java.library.path=" + path);
-
-            for (var dir : path.split(":")) {
-                listFiles(out, new File(dir), "");
-
-                var gparent = new File((new File(dir)).getAbsolutePath() + "../../..")
-                        .getCanonicalFile();
-                if (gparent.getName().contains("testcases")) {
-                    // Special case: if we found this directory, dump its contents too.
-                    listFiles(out, gparent, "");
-                }
-            }
-
-            var gparent = new File("../..").getCanonicalFile();
-            out.println("# ../..=" + gparent);
-            listFiles(out, gparent, "");
-        } catch (Throwable th) {
-            out.println("Error: " + th.toString());
-            th.printStackTrace(out);
-        }
-    }
-
-    private static void listFiles(PrintStream out, File dir, String prefix) {
-        if (!dir.isDirectory()) {
-            out.println(prefix + dir.getAbsolutePath() + " is not a directory!");
-            return;
-        }
-        out.println(prefix + ":" + dir.getAbsolutePath() + "/");
-        // First, list the files.
-        for (var file : Arrays.stream(dir.listFiles()).sorted().toList()) {
-            out.println(prefix + "  " + file.getName() + "" + (file.isDirectory() ? "/" : ""));
-        }
-
-        // Then recurse.
-        if (dir.getAbsolutePath().startsWith("/usr") || dir.getAbsolutePath().startsWith("/lib")) {
-            // There would be too many files, so don't recurse.
-            return;
-        }
-        for (var file : Arrays.stream(dir.listFiles()).sorted().toList()) {
-            if (file.isDirectory()) {
-                listFiles(out, file, prefix + "  ");
-            }
-        }
+        RavenwoodCommonUtils.loadJniLibrary(libname);
     }
 }
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index 773a89a..483b98a 100644
--- a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -20,10 +20,6 @@
 import org.junit.runners.model.Statement;
 
 public class RavenwoodRuleImpl {
-    public static boolean isOnRavenwood() {
-        return false;
-    }
-
     public static void init(RavenwoodRule rule) {
         // No-op when running on a real device
     }
diff --git a/ravenwood/runtime-common-device-src/com/android/ravenwood/common/divergence/RavenwoodDivergence.java b/ravenwood/runtime-common-device-src/com/android/ravenwood/common/divergence/RavenwoodDivergence.java
new file mode 100644
index 0000000..1714716
--- /dev/null
+++ b/ravenwood/runtime-common-device-src/com/android/ravenwood/common/divergence/RavenwoodDivergence.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.common.divergence;
+
+/**
+ * A class that behaves differently on the device side and on Ravenwood, because we have
+ * two build modules with different implementation and we link the different ones at runtime.
+ */
+public final class RavenwoodDivergence {
+    private RavenwoodDivergence() {
+    }
+
+    public static boolean isOnRavenwood() {
+        return false;
+    }
+}
diff --git a/ravenwood/runtime-common-ravenwood-src/com/android/ravenwood/common/divergence/RavenwoodDivergence.java b/ravenwood/runtime-common-ravenwood-src/com/android/ravenwood/common/divergence/RavenwoodDivergence.java
new file mode 100644
index 0000000..59f474a
--- /dev/null
+++ b/ravenwood/runtime-common-ravenwood-src/com/android/ravenwood/common/divergence/RavenwoodDivergence.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.common.divergence;
+
+/**
+ * A class that behaves differently on the device side and on Ravenwood, because we have
+ * two build modules with different implementation and we link the different ones at runtime.
+ */
+public final class RavenwoodDivergence {
+    private RavenwoodDivergence() {
+    }
+
+    public static boolean isOnRavenwood() {
+        return true;
+    }
+}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
new file mode 100644
index 0000000..ee28099
--- /dev/null
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.common;
+
+import java.io.FileDescriptor;
+
+/**
+ * Collection of methods to workaround limitation in the hostside JVM.
+ */
+public abstract class JvmWorkaround {
+    JvmWorkaround() {
+    }
+
+    // We only support OpenJDK for now.
+    private static JvmWorkaround sInstance =
+            RavenwoodCommonUtils.isOnRavenwood() ? new OpenJdkWorkaround() : new NullWorkaround();
+
+    public static JvmWorkaround getInstance() {
+        return sInstance;
+    }
+
+    /**
+     * Equivalent to Android's FileDescriptor.setInt$().
+     */
+    public abstract void setFdInt(FileDescriptor fd, int fdInt);
+
+
+    /**
+     * Equivalent to Android's FileDescriptor.getInt$().
+     */
+    public abstract int getFdInt(FileDescriptor fd);
+
+    /**
+     * Placeholder implementation for the host side.
+     *
+     * Even on the host side, we don't want to throw just because the class is loaded,
+     * which could cause weird random issues, so we throw from individual methods rather
+     * than from the constructor.
+     */
+    private static class NullWorkaround extends JvmWorkaround {
+        private RuntimeException calledOnHostside() {
+            throw new RuntimeException("This method shouldn't be called on the host side");
+        }
+
+        @Override
+        public void setFdInt(FileDescriptor fd, int fdInt) {
+            throw calledOnHostside();
+        }
+
+        @Override
+        public int getFdInt(FileDescriptor fd) {
+            throw calledOnHostside();
+        }
+    }
+}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
new file mode 100644
index 0000000..9aedaab
--- /dev/null
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.common;
+
+import java.io.FileDescriptor;
+
+class OpenJdkWorkaround extends JvmWorkaround {
+    @Override
+    public void setFdInt(FileDescriptor fd, int fdInt) {
+        try {
+            final Object obj = Class.forName("jdk.internal.access.SharedSecrets").getMethod(
+                    "getJavaIOFileDescriptorAccess").invoke(null);
+            Class.forName("jdk.internal.access.JavaIOFileDescriptorAccess").getMethod(
+                    "set", FileDescriptor.class, int.class).invoke(obj, fd, fdInt);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException("Failed to interact with raw FileDescriptor internals;"
+                    + " perhaps JRE has changed?", e);
+        }
+    }
+
+    @Override
+    public int getFdInt(FileDescriptor fd) {
+        try {
+            final Object obj = Class.forName("jdk.internal.access.SharedSecrets").getMethod(
+                    "getJavaIOFileDescriptorAccess").invoke(null);
+            return (int) Class.forName("jdk.internal.access.JavaIOFileDescriptorAccess").getMethod(
+                    "get", FileDescriptor.class).invoke(obj, fd);
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException("Failed to interact with raw FileDescriptor internals;"
+                    + " perhaps JRE has changed?", e);
+        }
+    }
+}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodBadIntegrityException.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodBadIntegrityException.java
new file mode 100644
index 0000000..61d54cb
--- /dev/null
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodBadIntegrityException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.common;
+
+public class RavenwoodBadIntegrityException extends RavenwoodRuntimeException {
+    public RavenwoodBadIntegrityException(String message) {
+        super(message);
+    }
+
+    public RavenwoodBadIntegrityException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
new file mode 100644
index 0000000..c8cc8d9
--- /dev/null
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.common;
+
+import com.android.ravenwood.common.divergence.RavenwoodDivergence;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.PrintStream;
+import java.util.Arrays;
+
+public class RavenwoodCommonUtils {
+    private static final String TAG = "RavenwoodCommonUtils";
+
+    private RavenwoodCommonUtils() {
+    }
+
+    private static final Object sLock = new Object();
+
+    /** Name of `libravenwood_runtime` */
+    private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
+
+    /** Directory name of `out/host/linux-x86/testcases/ravenwood-runtime` */
+    private static final String RAVENWOOD_RUNTIME_DIR_NAME = "ravenwood-runtime";
+
+    private static boolean sEnableExtraRuntimeCheck =
+            "1".equals(System.getenv("RAVENWOOD_ENABLE_EXTRA_RUNTIME_CHECK"));
+
+    private static final boolean IS_ON_RAVENWOOD = RavenwoodDivergence.isOnRavenwood();
+
+    private static final String RAVEWOOD_RUNTIME_PATH = getRavenwoodRuntimePathInternal();
+
+    public static final String RAVENWOOD_SYSPROP = "ro.is_on_ravenwood";
+
+    // @GuardedBy("sLock")
+    private static boolean sIntegrityChecked = false;
+
+    /**
+     * @return if we're running on Ravenwood.
+     */
+    public static boolean isOnRavenwood() {
+        return IS_ON_RAVENWOOD;
+    }
+
+    /**
+     * Throws if the runtime is not Ravenwood.
+     */
+    public static void ensureOnRavenwood() {
+        if (!isOnRavenwood()) {
+            throw new RavenwoodRuntimeException("This is only supposed to be used on Ravenwood");
+        }
+    }
+
+    /**
+     * @return if the various extra runtime check should be enabled.
+     */
+    public static boolean shouldEnableExtraRuntimeCheck() {
+        return sEnableExtraRuntimeCheck;
+    }
+
+    /**
+     * Load the main runtime JNI library.
+     */
+    public static void loadRavenwoodNativeRuntime() {
+        ensureOnRavenwood();
+        loadJniLibrary(RAVENWOOD_NATIVE_RUNTIME_NAME);
+    }
+
+    /**
+     * Internal implementation of
+     * {@link android.platform.test.ravenwood.RavenwoodUtils#loadJniLibrary(String)}
+     */
+    public static void loadJniLibrary(String libname) {
+        if (RavenwoodCommonUtils.isOnRavenwood()) {
+            loadJniLibraryInternal(libname);
+        } else {
+            System.loadLibrary(libname);
+        }
+    }
+
+    /**
+     * Function equivalent to ART's System.loadLibrary. See RavenwoodUtils for why we need it.
+     */
+    private static void loadJniLibraryInternal(String libname) {
+        var path = System.getProperty("java.library.path");
+        var filename = "lib" + libname + ".so";
+
+        System.out.println("Looking for library " + libname + ".so in java.library.path:" + path);
+
+        try {
+            if (path == null) {
+                throw new UnsatisfiedLinkError("Cannot load library " + libname + "."
+                        + " Property java.library.path not set!");
+            }
+            for (var dir : path.split(":")) {
+                var file = new File(dir + "/" + filename);
+                if (file.exists()) {
+                    System.load(file.getAbsolutePath());
+                    return;
+                }
+            }
+            throw new UnsatisfiedLinkError("Library " + libname + " not found in "
+                    + "java.library.path: " + path);
+        } catch (Throwable e) {
+            dumpFiles(System.out);
+            throw e;
+        }
+    }
+
+    private static void dumpFiles(PrintStream out) {
+        try {
+            var path = System.getProperty("java.library.path");
+            out.println("# java.library.path=" + path);
+
+            for (var dir : path.split(":")) {
+                listFiles(out, new File(dir), "");
+
+                var gparent = new File((new File(dir)).getAbsolutePath() + "../../..")
+                        .getCanonicalFile();
+                if (gparent.getName().contains("testcases")) {
+                    // Special case: if we found this directory, dump its contents too.
+                    listFiles(out, gparent, "");
+                }
+            }
+
+            var gparent = new File("../..").getCanonicalFile();
+            out.println("# ../..=" + gparent);
+            listFiles(out, gparent, "");
+        } catch (Throwable th) {
+            out.println("Error: " + th.toString());
+            th.printStackTrace(out);
+        }
+    }
+
+    private static void listFiles(PrintStream out, File dir, String prefix) {
+        if (!dir.isDirectory()) {
+            out.println(prefix + dir.getAbsolutePath() + " is not a directory!");
+            return;
+        }
+        out.println(prefix + ":" + dir.getAbsolutePath() + "/");
+        // First, list the files.
+        for (var file : Arrays.stream(dir.listFiles()).sorted().toList()) {
+            out.println(prefix + "  " + file.getName() + "" + (file.isDirectory() ? "/" : ""));
+        }
+
+        // Then recurse.
+        if (dir.getAbsolutePath().startsWith("/usr") || dir.getAbsolutePath().startsWith("/lib")) {
+            // There would be too many files, so don't recurse.
+            return;
+        }
+        for (var file : Arrays.stream(dir.listFiles()).sorted().toList()) {
+            if (file.isDirectory()) {
+                listFiles(out, file, prefix + "  ");
+            }
+        }
+    }
+
+    /**
+     * @return the full directory path that contains the "ravenwood-runtime" files.
+     *
+     * This method throws if called on the device side.
+     */
+    public static String getRavenwoodRuntimePath() {
+        ensureOnRavenwood();
+        return RAVEWOOD_RUNTIME_PATH;
+    }
+
+    private static String getRavenwoodRuntimePathInternal() {
+        if (!isOnRavenwood()) {
+            return null;
+        }
+        var path = System.getProperty("java.library.path");
+
+        System.out.println("Looking for " + RAVENWOOD_RUNTIME_DIR_NAME + " directory"
+                + " in java.library.path:" + path);
+
+        try {
+            if (path == null) {
+                throw new IllegalStateException("java.library.path shouldn't be null");
+            }
+            for (var dir : path.split(":")) {
+
+                // For each path, see if the path contains RAVENWOOD_RUNTIME_DIR_NAME.
+                var d = new File(dir);
+                for (;;) {
+                    if (d.getParent() == null) {
+                        break; // Root dir, stop.
+                    }
+                    if (RAVENWOOD_RUNTIME_DIR_NAME.equals(d.getName())) {
+                        var ret = d.getAbsolutePath() + "/";
+                        System.out.println("Found: " + ret);
+                        return ret;
+                    }
+                    d = d.getParentFile();
+                }
+            }
+            throw new IllegalStateException(RAVENWOOD_RUNTIME_DIR_NAME + " not found");
+        } catch (Throwable e) {
+            dumpFiles(System.out);
+            throw e;
+        }
+    }
+
+    /** Close an {@link AutoCloseable}. */
+    public static void closeQuietly(AutoCloseable c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (Exception e) {
+                // Ignore
+            }
+        }
+    }
+
+    /** Close a {@link FileDescriptor}. */
+    public static void closeQuietly(FileDescriptor fd) {
+        var is = new FileInputStream(fd);
+        RavenwoodCommonUtils.closeQuietly(is);
+    }
+}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeException.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeException.java
new file mode 100644
index 0000000..7b0cebc
--- /dev/null
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.common;
+
+public class RavenwoodRuntimeException extends RuntimeException {
+    public RavenwoodRuntimeException(String message) {
+        super(message);
+    }
+
+    public RavenwoodRuntimeException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java
new file mode 100644
index 0000000..6540221
--- /dev/null
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodRuntimeNative.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.common;
+
+import java.io.FileDescriptor;
+
+/**
+ * Class to host all the JNI methods used in ravenwood runtime.
+ */
+public class RavenwoodRuntimeNative {
+    private RavenwoodRuntimeNative() {
+    }
+
+    static {
+        RavenwoodCommonUtils.ensureOnRavenwood();
+        RavenwoodCommonUtils.loadRavenwoodNativeRuntime();
+    }
+
+    public static native void applyFreeFunction(long freeFunction, long nativePtr);
+
+    public static native long nLseek(int fd, long offset, int whence);
+
+    public static native int[] nPipe2(int flags);
+
+    public static native int nDup(int oldfd);
+
+    public static native int nFcntlInt(int fd, int cmd, int arg);
+
+    public static long lseek(FileDescriptor fd, long offset, int whence) {
+        return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence);
+    }
+
+    public static FileDescriptor[] pipe2(int flags) {
+        var fds = nPipe2(flags);
+        var ret = new FileDescriptor[] {
+                new FileDescriptor(),
+                new FileDescriptor(),
+        };
+        JvmWorkaround.getInstance().setFdInt(ret[0], fds[0]);
+        JvmWorkaround.getInstance().setFdInt(ret[1], fds[1]);
+
+        return ret;
+    }
+
+    public static FileDescriptor dup(FileDescriptor fd) {
+        var fdInt = nDup(JvmWorkaround.getInstance().getFdInt(fd));
+
+        var retFd = new java.io.FileDescriptor();
+        JvmWorkaround.getInstance().setFdInt(retFd, fdInt);
+        return retFd;
+    }
+
+    public static int fcntlInt(FileDescriptor fd, int cmd, int arg) {
+        var fdInt = JvmWorkaround.getInstance().getFdInt(fd);
+
+        return nFcntlInt(fdInt, cmd, arg);
+    }
+}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
index 2d79914..8fe6853 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
@@ -26,6 +26,7 @@
 import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.ravenwood.common.JvmWorkaround;
 
 import java.io.File;
 import java.io.FileDescriptor;
@@ -46,27 +47,11 @@
     private static final Map<FileDescriptor, RandomAccessFile> sActive = new HashMap<>();
 
     public static void native_setFdInt$ravenwood(FileDescriptor fd, int fdInt) {
-        try {
-            final Object obj = Class.forName("jdk.internal.access.SharedSecrets").getMethod(
-                    "getJavaIOFileDescriptorAccess").invoke(null);
-            Class.forName("jdk.internal.access.JavaIOFileDescriptorAccess").getMethod(
-                    "set", FileDescriptor.class, int.class).invoke(obj, fd, fdInt);
-        } catch (ReflectiveOperationException e) {
-            throw new RuntimeException("Failed to interact with raw FileDescriptor internals;"
-                    + " perhaps JRE has changed?", e);
-        }
+        JvmWorkaround.getInstance().setFdInt(fd, fdInt);
     }
 
     public static int native_getFdInt$ravenwood(FileDescriptor fd) {
-        try {
-            final Object obj = Class.forName("jdk.internal.access.SharedSecrets").getMethod(
-                    "getJavaIOFileDescriptorAccess").invoke(null);
-            return (int) Class.forName("jdk.internal.access.JavaIOFileDescriptorAccess").getMethod(
-                    "get", FileDescriptor.class).invoke(obj, fd);
-        } catch (ReflectiveOperationException e) {
-            throw new RuntimeException("Failed to interact with raw FileDescriptor internals;"
-                    + " perhaps JRE has changed?", e);
-        }
+        return JvmWorkaround.getInstance().getFdInt(fd);
     }
 
     public static FileDescriptor native_open$ravenwood(File file, int pfdMode) throws IOException {
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java
index 68bf922..b00cee0 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java
@@ -19,6 +19,7 @@
 import android.util.Log;
 
 import com.android.internal.ravenwood.RavenwoodEnvironment;
+import com.android.ravenwood.common.RavenwoodCommonUtils;
 
 public class RavenwoodEnvironment_host {
     private static final String TAG = RavenwoodEnvironment.TAG;
@@ -39,7 +40,7 @@
             if (sInitialized) {
                 return;
             }
-            Log.w(TAG, "Initializing Ravenwood environment");
+            Log.i(TAG, "Initializing Ravenwood environment");
 
             // Set the default values.
             var sysProps = RavenwoodSystemProperties.DEFAULT_VALUES;
@@ -54,4 +55,4 @@
             sInitialized = true;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
index 69ff262..e198646 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
@@ -15,7 +15,7 @@
  */
 package com.android.platform.test.ravenwood.runtimehelper;
 
-import android.platform.test.ravenwood.RavenwoodUtils;
+import com.android.ravenwood.common.RavenwoodCommonUtils;
 
 import java.io.File;
 import java.lang.reflect.Modifier;
@@ -141,7 +141,7 @@
 
         log("Loading " + LIBANDROID_RUNTIME_NAME + " for '" + libanrdoidClasses + "' and '"
                 + libhwuiClasses + "'");
-        RavenwoodUtils.loadJniLibrary(LIBANDROID_RUNTIME_NAME);
+        RavenwoodCommonUtils.loadJniLibrary(LIBANDROID_RUNTIME_NAME);
     }
 
     /**
diff --git a/ravenwood/runtime-helper-src/jni/ravenwood_runtime.cpp b/ravenwood/runtime-helper-src/jni/ravenwood_runtime.cpp
deleted file mode 100644
index 8e3a21d..0000000
--- a/ravenwood/runtime-helper-src/jni/ravenwood_runtime.cpp
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <nativehelper/JNIHelp.h>
-#include "jni.h"
-#include "utils/Log.h"
-#include "utils/misc.h"
-
-
-typedef void (*FreeFunction)(void*);
-
-static void NativeAllocationRegistry_applyFreeFunction(JNIEnv*,
-                                                       jclass,
-                                                       jlong freeFunction,
-                                                       jlong ptr) {
-    void* nativePtr = reinterpret_cast<void*>(static_cast<uintptr_t>(ptr));
-    FreeFunction nativeFreeFunction
-        = reinterpret_cast<FreeFunction>(static_cast<uintptr_t>(freeFunction));
-    nativeFreeFunction(nativePtr);
-}
-
-static const JNINativeMethod sMethods_NAR[] =
-{
-    { "applyFreeFunction", "(JJ)V", (void*)NativeAllocationRegistry_applyFreeFunction },
-};
-
-extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
-{
-    JNIEnv* env = NULL;
-    jint result = -1;
-
-    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
-        ALOGE("GetEnv failed!");
-        return result;
-    }
-    ALOG_ASSERT(env, "Could not retrieve the env!");
-
-    ALOGI("%s: JNI_OnLoad", __FILE__);
-
-    // Initialize the Ravenwood version of NativeAllocationRegistry.
-    // We don't use this JNI on the device side, but if we ever have to do, skip this part.
-#ifndef __ANDROID__
-    int res = jniRegisterNativeMethods(env, "libcore/util/NativeAllocationRegistry",
-            sMethods_NAR, NELEM(sMethods_NAR));
-    if (res < 0) {
-        return res;
-    }
-#endif
-
-    return JNI_VERSION_1_4;
-}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/ErrnoException.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/ErrnoException.java
index 388156a..843455d 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/ErrnoException.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/ErrnoException.java
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+// [ravenwood] Copied from libcore.
+
 package android.system;
 
 import java.io.IOException;
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
new file mode 100644
index 0000000..e031eb2
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.system;
+
+import com.android.ravenwood.common.RavenwoodRuntimeNative;
+
+import java.io.FileDescriptor;
+
+/**
+ * OS class replacement used on Ravenwood. For now, we just implement APIs as we need them...
+ * TODO(b/340887115): Need a better integration with libcore.
+ */
+public final class Os {
+    private Os() {}
+
+    public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
+        return RavenwoodRuntimeNative.lseek(fd, offset, whence);
+    }
+
+
+    public static FileDescriptor[] pipe2(int flags) throws ErrnoException {
+        return RavenwoodRuntimeNative.pipe2(flags);
+    }
+
+    public static FileDescriptor dup(FileDescriptor fd) throws ErrnoException {
+        return RavenwoodRuntimeNative.dup(fd);
+    }
+
+    public static int fcntlInt(FileDescriptor fd, int cmd, int arg) throws ErrnoException {
+        return RavenwoodRuntimeNative.fcntlInt(fd, cmd, arg);
+    }
+}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/OsConstants.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/OsConstants.java
new file mode 100644
index 0000000..c56ec8a
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/OsConstants.java
@@ -0,0 +1,1259 @@
+/*
+ * Copyright (C) 2011 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 android.system;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
+/**
+ * Copied from libcore's version, with the local changes:
+ * - All the imports are removed. (they're only used in javadoc)
+ * - All the annotations are removed.
+ * - The initConstants() method is moved to a nested class.
+ *
+ * TODO(b/340887115): Need a better integration with libcore.
+ */
+
+public class OsConstants {
+//    @UnsupportedAppUsage
+    private OsConstants() {
+    }
+
+    /**
+     * Returns the index of the element in the {@link StructCapUserData} (cap_user_data)
+     * array that this capability is stored in.
+     *
+     * @param x capability
+     * @return index of the element in the {@link StructCapUserData} array storing this capability
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static int CAP_TO_INDEX(int x) { return x >>> 5; }
+
+    /**
+     * Returns the mask for the given capability. This is relative to the capability's
+     * {@link StructCapUserData} (cap_user_data) element, the index of which can be
+     * retrieved with {@link CAP_TO_INDEX}.
+     *
+     * @param x capability
+     * @return mask for given capability
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static int CAP_TO_MASK(int x) { return 1 << (x & 31); }
+
+    /**
+     * Tests whether the given mode is a block device.
+     */
+    public static boolean S_ISBLK(int mode) { return (mode & S_IFMT) == S_IFBLK; }
+
+    /**
+     * Tests whether the given mode is a character device.
+     */
+    public static boolean S_ISCHR(int mode) { return (mode & S_IFMT) == S_IFCHR; }
+
+    /**
+     * Tests whether the given mode is a directory.
+     */
+    public static boolean S_ISDIR(int mode) { return (mode & S_IFMT) == S_IFDIR; }
+
+    /**
+     * Tests whether the given mode is a FIFO.
+     */
+    public static boolean S_ISFIFO(int mode) { return (mode & S_IFMT) == S_IFIFO; }
+
+    /**
+     * Tests whether the given mode is a regular file.
+     */
+    public static boolean S_ISREG(int mode) { return (mode & S_IFMT) == S_IFREG; }
+
+    /**
+     * Tests whether the given mode is a symbolic link.
+     */
+    public static boolean S_ISLNK(int mode) { return (mode & S_IFMT) == S_IFLNK; }
+
+    /**
+     * Tests whether the given mode is a socket.
+     */
+    public static boolean S_ISSOCK(int mode) { return (mode & S_IFMT) == S_IFSOCK; }
+
+    /**
+     * Extracts the exit status of a child. Only valid if WIFEXITED returns true.
+     */
+    public static int WEXITSTATUS(int status) { return (status & 0xff00) >> 8; }
+
+    /**
+     * Tests whether the child dumped core. Only valid if WIFSIGNALED returns true.
+     */
+    public static boolean WCOREDUMP(int status) { return (status & 0x80) != 0; }
+
+    /**
+     * Returns the signal that caused the child to exit. Only valid if WIFSIGNALED returns true.
+     */
+    public static int WTERMSIG(int status) { return status & 0x7f; }
+
+    /**
+     * Returns the signal that cause the child to stop. Only valid if WIFSTOPPED returns true.
+     */
+    public static int WSTOPSIG(int status) { return WEXITSTATUS(status); }
+
+    /**
+     * Tests whether the child exited normally.
+     */
+    public static boolean WIFEXITED(int status) { return (WTERMSIG(status) == 0); }
+
+    /**
+     * Tests whether the child was stopped (not terminated) by a signal.
+     */
+    public static boolean WIFSTOPPED(int status) { return (WTERMSIG(status) == 0x7f); }
+
+    /**
+     * Tests whether the child was terminated by a signal.
+     */
+    public static boolean WIFSIGNALED(int status) { return (WTERMSIG(status + 1) >= 2); }
+
+    public static final int AF_INET = placeholder();
+    public static final int AF_INET6 = placeholder();
+    public static final int AF_NETLINK = placeholder();
+    public static final int AF_PACKET = placeholder();
+    public static final int AF_UNIX = placeholder();
+
+    /**
+     * The virt-vsock address family, linux specific.
+     * It is used with {@code struct sockaddr_vm} from uapi/linux/vm_sockets.h.
+     *
+     * @see <a href="https://man7.org/linux/man-pages/man7/vsock.7.html">vsock(7)</a>
+     * @see VmSocketAddress
+     */
+    public static final int AF_VSOCK = placeholder();
+    public static final int AF_UNSPEC = placeholder();
+    public static final int AI_ADDRCONFIG = placeholder();
+    public static final int AI_ALL = placeholder();
+    public static final int AI_CANONNAME = placeholder();
+    public static final int AI_NUMERICHOST = placeholder();
+    public static final int AI_NUMERICSERV = placeholder();
+    public static final int AI_PASSIVE = placeholder();
+    public static final int AI_V4MAPPED = placeholder();
+    public static final int ARPHRD_ETHER = placeholder();
+
+    /**
+     * The virtio-vsock {@code svmPort} value to bind for any available port.
+     *
+     * @see <a href="https://man7.org/linux/man-pages/man7/vsock.7.html">vsock(7)</a>
+     * @see VmSocketAddress
+     */
+    public static final int VMADDR_PORT_ANY = placeholder();
+
+    /**
+     * The virtio-vsock {@code svmCid} value to listens for all CIDs.
+     *
+     * @see <a href="https://man7.org/linux/man-pages/man7/vsock.7.html">vsock(7)</a>
+     * @see VmSocketAddress
+     */
+    public static final int VMADDR_CID_ANY = placeholder();
+
+    /**
+     * The virtio-vsock {@code svmCid} value for host communication.
+     *
+     * @see <a href="https://man7.org/linux/man-pages/man7/vsock.7.html">vsock(7)</a>
+     * @see VmSocketAddress
+     */
+    public static final int VMADDR_CID_LOCAL = placeholder();
+
+    /**
+     * The virtio-vsock {@code svmCid} value for loopback communication.
+     *
+     * @see <a href="https://man7.org/linux/man-pages/man7/vsock.7.html">vsock(7)</a>
+     * @see VmSocketAddress
+     */
+    public static final int VMADDR_CID_HOST = placeholder();
+
+    /**
+     * ARP protocol loopback device identifier.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int ARPHRD_LOOPBACK = placeholder();
+    public static final int CAP_AUDIT_CONTROL = placeholder();
+    public static final int CAP_AUDIT_WRITE = placeholder();
+    public static final int CAP_BLOCK_SUSPEND = placeholder();
+    public static final int CAP_CHOWN = placeholder();
+    public static final int CAP_DAC_OVERRIDE = placeholder();
+    public static final int CAP_DAC_READ_SEARCH = placeholder();
+    public static final int CAP_FOWNER = placeholder();
+    public static final int CAP_FSETID = placeholder();
+    public static final int CAP_IPC_LOCK = placeholder();
+    public static final int CAP_IPC_OWNER = placeholder();
+    public static final int CAP_KILL = placeholder();
+    public static final int CAP_LAST_CAP = placeholder();
+    public static final int CAP_LEASE = placeholder();
+    public static final int CAP_LINUX_IMMUTABLE = placeholder();
+    public static final int CAP_MAC_ADMIN = placeholder();
+    public static final int CAP_MAC_OVERRIDE = placeholder();
+    public static final int CAP_MKNOD = placeholder();
+    public static final int CAP_NET_ADMIN = placeholder();
+    public static final int CAP_NET_BIND_SERVICE = placeholder();
+    public static final int CAP_NET_BROADCAST = placeholder();
+    public static final int CAP_NET_RAW = placeholder();
+    public static final int CAP_SETFCAP = placeholder();
+    public static final int CAP_SETGID = placeholder();
+    public static final int CAP_SETPCAP = placeholder();
+    public static final int CAP_SETUID = placeholder();
+    public static final int CAP_SYS_ADMIN = placeholder();
+    public static final int CAP_SYS_BOOT = placeholder();
+    public static final int CAP_SYS_CHROOT = placeholder();
+    public static final int CAP_SYSLOG = placeholder();
+    public static final int CAP_SYS_MODULE = placeholder();
+    public static final int CAP_SYS_NICE = placeholder();
+    public static final int CAP_SYS_PACCT = placeholder();
+    public static final int CAP_SYS_PTRACE = placeholder();
+    public static final int CAP_SYS_RAWIO = placeholder();
+    public static final int CAP_SYS_RESOURCE = placeholder();
+    public static final int CAP_SYS_TIME = placeholder();
+    public static final int CAP_SYS_TTY_CONFIG = placeholder();
+    public static final int CAP_WAKE_ALARM = placeholder();
+    public static final int E2BIG = placeholder();
+    public static final int EACCES = placeholder();
+    public static final int EADDRINUSE = placeholder();
+    public static final int EADDRNOTAVAIL = placeholder();
+    public static final int EAFNOSUPPORT = placeholder();
+    public static final int EAGAIN = placeholder();
+    public static final int EAI_AGAIN = placeholder();
+    public static final int EAI_BADFLAGS = placeholder();
+    public static final int EAI_FAIL = placeholder();
+    public static final int EAI_FAMILY = placeholder();
+    public static final int EAI_MEMORY = placeholder();
+    public static final int EAI_NODATA = placeholder();
+    public static final int EAI_NONAME = placeholder();
+    public static final int EAI_OVERFLOW = placeholder();
+    public static final int EAI_SERVICE = placeholder();
+    public static final int EAI_SOCKTYPE = placeholder();
+    public static final int EAI_SYSTEM = placeholder();
+    public static final int EALREADY = placeholder();
+    public static final int EBADF = placeholder();
+    public static final int EBADMSG = placeholder();
+    public static final int EBUSY = placeholder();
+    public static final int ECANCELED = placeholder();
+    public static final int ECHILD = placeholder();
+    public static final int ECONNABORTED = placeholder();
+    public static final int ECONNREFUSED = placeholder();
+    public static final int ECONNRESET = placeholder();
+    public static final int EDEADLK = placeholder();
+    public static final int EDESTADDRREQ = placeholder();
+    public static final int EDOM = placeholder();
+    public static final int EDQUOT = placeholder();
+    public static final int EEXIST = placeholder();
+    public static final int EFAULT = placeholder();
+    public static final int EFBIG = placeholder();
+    public static final int EHOSTUNREACH = placeholder();
+    public static final int EIDRM = placeholder();
+    public static final int EILSEQ = placeholder();
+    public static final int EINPROGRESS = placeholder();
+    public static final int EINTR = placeholder();
+    public static final int EINVAL = placeholder();
+    public static final int EIO = placeholder();
+    public static final int EISCONN = placeholder();
+    public static final int EISDIR = placeholder();
+    public static final int ELOOP = placeholder();
+    public static final int EMFILE = placeholder();
+    public static final int EMLINK = placeholder();
+    public static final int EMSGSIZE = placeholder();
+    public static final int EMULTIHOP = placeholder();
+    public static final int ENAMETOOLONG = placeholder();
+    public static final int ENETDOWN = placeholder();
+    public static final int ENETRESET = placeholder();
+    public static final int ENETUNREACH = placeholder();
+    public static final int ENFILE = placeholder();
+    public static final int ENOBUFS = placeholder();
+    public static final int ENODATA = placeholder();
+    public static final int ENODEV = placeholder();
+    public static final int ENOENT = placeholder();
+    public static final int ENOEXEC = placeholder();
+    public static final int ENOLCK = placeholder();
+    public static final int ENOLINK = placeholder();
+    public static final int ENOMEM = placeholder();
+    public static final int ENOMSG = placeholder();
+    public static final int ENONET = placeholder();
+    public static final int ENOPROTOOPT = placeholder();
+    public static final int ENOSPC = placeholder();
+    public static final int ENOSR = placeholder();
+    public static final int ENOSTR = placeholder();
+    public static final int ENOSYS = placeholder();
+    public static final int ENOTCONN = placeholder();
+    public static final int ENOTDIR = placeholder();
+    public static final int ENOTEMPTY = placeholder();
+    public static final int ENOTSOCK = placeholder();
+    public static final int ENOTSUP = placeholder();
+    public static final int ENOTTY = placeholder();
+    public static final int ENXIO = placeholder();
+    public static final int EOPNOTSUPP = placeholder();
+    public static final int EOVERFLOW = placeholder();
+    public static final int EPERM = placeholder();
+    public static final int EPIPE = placeholder();
+    public static final int EPROTO = placeholder();
+    public static final int EPROTONOSUPPORT = placeholder();
+    public static final int EPROTOTYPE = placeholder();
+    public static final int ERANGE = placeholder();
+    public static final int EROFS = placeholder();
+    public static final int ESPIPE = placeholder();
+    public static final int ESRCH = placeholder();
+    public static final int ESTALE = placeholder();
+    public static final int ETH_P_ALL = placeholder();
+    public static final int ETH_P_ARP = placeholder();
+    public static final int ETH_P_IP = placeholder();
+    public static final int ETH_P_IPV6 = placeholder();
+    public static final int ETIME = placeholder();
+    public static final int ETIMEDOUT = placeholder();
+    public static final int ETXTBSY = placeholder();
+    /**
+     * "Too many users" error.
+     * See <a href="https://man7.org/linux/man-pages/man3/errno.3.html">errno(3)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int EUSERS = placeholder();
+    // On Linux, EWOULDBLOCK == EAGAIN. Use EAGAIN instead, to reduce confusion.
+    public static final int EXDEV = placeholder();
+    public static final int EXIT_FAILURE = placeholder();
+    public static final int EXIT_SUCCESS = placeholder();
+    public static final int FD_CLOEXEC = placeholder();
+    public static final int FIONREAD = placeholder();
+    public static final int F_DUPFD = placeholder();
+    public static final int F_DUPFD_CLOEXEC = placeholder();
+    public static final int F_GETFD = placeholder();
+    public static final int F_GETFL = placeholder();
+    public static final int F_GETLK = placeholder();
+    public static final int F_GETLK64 = placeholder();
+    public static final int F_GETOWN = placeholder();
+    public static final int F_OK = placeholder();
+    public static final int F_RDLCK = placeholder();
+    public static final int F_SETFD = placeholder();
+    public static final int F_SETFL = placeholder();
+    public static final int F_SETLK = placeholder();
+    public static final int F_SETLK64 = placeholder();
+    public static final int F_SETLKW = placeholder();
+    public static final int F_SETLKW64 = placeholder();
+    public static final int F_SETOWN = placeholder();
+    public static final int F_UNLCK = placeholder();
+    public static final int F_WRLCK = placeholder();
+    public static final int ICMP_ECHO = placeholder();
+    public static final int ICMP_ECHOREPLY = placeholder();
+    public static final int ICMP6_ECHO_REQUEST = placeholder();
+    public static final int ICMP6_ECHO_REPLY = placeholder();
+    public static final int IFA_F_DADFAILED = placeholder();
+    public static final int IFA_F_DEPRECATED = placeholder();
+    public static final int IFA_F_HOMEADDRESS = placeholder();
+    public static final int IFA_F_MANAGETEMPADDR = placeholder();
+    public static final int IFA_F_NODAD = placeholder();
+    public static final int IFA_F_NOPREFIXROUTE = placeholder();
+    public static final int IFA_F_OPTIMISTIC = placeholder();
+    public static final int IFA_F_PERMANENT = placeholder();
+    public static final int IFA_F_SECONDARY = placeholder();
+    public static final int IFA_F_TEMPORARY = placeholder();
+    public static final int IFA_F_TENTATIVE = placeholder();
+    public static final int IFF_ALLMULTI = placeholder();
+    public static final int IFF_AUTOMEDIA = placeholder();
+    public static final int IFF_BROADCAST = placeholder();
+    public static final int IFF_DEBUG = placeholder();
+    public static final int IFF_DYNAMIC = placeholder();
+    public static final int IFF_LOOPBACK = placeholder();
+    public static final int IFF_MASTER = placeholder();
+    public static final int IFF_MULTICAST = placeholder();
+    public static final int IFF_NOARP = placeholder();
+    public static final int IFF_NOTRAILERS = placeholder();
+    public static final int IFF_POINTOPOINT = placeholder();
+    public static final int IFF_PORTSEL = placeholder();
+    public static final int IFF_PROMISC = placeholder();
+    public static final int IFF_RUNNING = placeholder();
+    public static final int IFF_SLAVE = placeholder();
+    public static final int IFF_UP = placeholder();
+    public static final int IPPROTO_ICMP = placeholder();
+    public static final int IPPROTO_ICMPV6 = placeholder();
+    public static final int IPPROTO_IP = placeholder();
+    public static final int IPPROTO_IPV6 = placeholder();
+    public static final int IPPROTO_RAW = placeholder();
+    public static final int IPPROTO_TCP = placeholder();
+    public static final int IPPROTO_UDP = placeholder();
+
+    /**
+     * Encapsulation Security Payload protocol
+     *
+     * <p>Defined in /uapi/linux/in.h
+     */
+    public static final int IPPROTO_ESP = placeholder();
+
+    public static final int IPV6_CHECKSUM = placeholder();
+    public static final int IPV6_MULTICAST_HOPS = placeholder();
+    public static final int IPV6_MULTICAST_IF = placeholder();
+    public static final int IPV6_MULTICAST_LOOP = placeholder();
+    public static final int IPV6_PKTINFO = placeholder();
+    public static final int IPV6_RECVDSTOPTS = placeholder();
+    public static final int IPV6_RECVHOPLIMIT = placeholder();
+    public static final int IPV6_RECVHOPOPTS = placeholder();
+    public static final int IPV6_RECVPKTINFO = placeholder();
+    public static final int IPV6_RECVRTHDR = placeholder();
+    public static final int IPV6_RECVTCLASS = placeholder();
+    public static final int IPV6_TCLASS = placeholder();
+    public static final int IPV6_UNICAST_HOPS = placeholder();
+    public static final int IPV6_V6ONLY = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int IP_MULTICAST_ALL = placeholder();
+    public static final int IP_MULTICAST_IF = placeholder();
+    public static final int IP_MULTICAST_LOOP = placeholder();
+    public static final int IP_MULTICAST_TTL = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int IP_RECVTOS = placeholder();
+    public static final int IP_TOS = placeholder();
+    public static final int IP_TTL = placeholder();
+    /**
+     * Version constant to be used in {@link StructCapUserHeader} with
+     * {@link Os#capset(StructCapUserHeader, StructCapUserData[])} and
+     * {@link Os#capget(StructCapUserHeader)}.
+     *
+     * See <a href="https://man7.org/linux/man-pages/man2/capget.2.html">capget(2)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int _LINUX_CAPABILITY_VERSION_3 = placeholder();
+    public static final int MAP_FIXED = placeholder();
+    public static final int MAP_ANONYMOUS = placeholder();
+    /**
+     * Flag argument for {@code mmap(long, long, int, int, FileDescriptor, long)}.
+     *
+     * See <a href="http://man7.org/linux/man-pages/man2/mmap.2.html">mmap(2)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int MAP_POPULATE = placeholder();
+    public static final int MAP_PRIVATE = placeholder();
+    public static final int MAP_SHARED = placeholder();
+    public static final int MCAST_JOIN_GROUP = placeholder();
+    public static final int MCAST_LEAVE_GROUP = placeholder();
+    public static final int MCAST_JOIN_SOURCE_GROUP = placeholder();
+    public static final int MCAST_LEAVE_SOURCE_GROUP = placeholder();
+    public static final int MCAST_BLOCK_SOURCE = placeholder();
+    public static final int MCAST_UNBLOCK_SOURCE = placeholder();
+    public static final int MCL_CURRENT = placeholder();
+    public static final int MCL_FUTURE = placeholder();
+    public static final int MFD_CLOEXEC = placeholder();
+    public static final int MSG_CTRUNC = placeholder();
+    public static final int MSG_DONTROUTE = placeholder();
+    public static final int MSG_EOR = placeholder();
+    public static final int MSG_OOB = placeholder();
+    public static final int MSG_PEEK = placeholder();
+    public static final int MSG_TRUNC = placeholder();
+    public static final int MSG_WAITALL = placeholder();
+    public static final int MS_ASYNC = placeholder();
+    public static final int MS_INVALIDATE = placeholder();
+    public static final int MS_SYNC = placeholder();
+    public static final int NETLINK_NETFILTER = placeholder();
+    public static final int NETLINK_ROUTE = placeholder();
+    /**
+     * SELinux enforces that only system_server and netd may use this netlink socket type.
+     */
+    public static final int NETLINK_INET_DIAG = placeholder();
+
+    /**
+     * SELinux enforces that only system_server and netd may use this netlink socket type.
+     *
+     * @see <a href="https://man7.org/linux/man-pages/man7/netlink.7.html">netlink(7)</a>
+     */
+    public static final int NETLINK_XFRM = placeholder();
+
+    public static final int NI_DGRAM = placeholder();
+    public static final int NI_NAMEREQD = placeholder();
+    public static final int NI_NOFQDN = placeholder();
+    public static final int NI_NUMERICHOST = placeholder();
+    public static final int NI_NUMERICSERV = placeholder();
+    public static final int O_ACCMODE = placeholder();
+    public static final int O_APPEND = placeholder();
+    public static final int O_CLOEXEC = placeholder();
+    public static final int O_CREAT = placeholder();
+    /**
+     * Flag for {@code Os#open(String, int, int)}.
+     *
+     * When enabled, tries to minimize cache effects of the I/O to and from this
+     * file. In general this will degrade performance, but it is
+     * useful in special situations, such as when applications do
+     * their own caching. File I/O is done directly to/from
+     * user-space buffers. The {@link O_DIRECT} flag on its own makes an
+     * effort to transfer data synchronously, but does not give
+     * the guarantees of the {@link O_SYNC} flag that data and necessary
+     * metadata are transferred. To guarantee synchronous I/O,
+     * {@link O_SYNC} must be used in addition to {@link O_DIRECT}.
+     *
+     * See <a href="https://man7.org/linux/man-pages/man2/open.2.html">open(2)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int O_DIRECT = placeholder();
+    public static final int O_EXCL = placeholder();
+    public static final int O_NOCTTY = placeholder();
+    public static final int O_NOFOLLOW = placeholder();
+    public static final int O_NONBLOCK = placeholder();
+    public static final int O_RDONLY = placeholder();
+    public static final int O_RDWR = placeholder();
+    public static final int O_SYNC = placeholder();
+    public static final int O_DSYNC = placeholder();
+    public static final int O_TRUNC = placeholder();
+    public static final int O_WRONLY = placeholder();
+    public static final int POLLERR = placeholder();
+    public static final int POLLHUP = placeholder();
+    public static final int POLLIN = placeholder();
+    public static final int POLLNVAL = placeholder();
+    public static final int POLLOUT = placeholder();
+    public static final int POLLPRI = placeholder();
+    public static final int POLLRDBAND = placeholder();
+    public static final int POLLRDNORM = placeholder();
+    public static final int POLLWRBAND = placeholder();
+    public static final int POLLWRNORM = placeholder();
+    /**
+     * Reads or changes the ambient capability set of the calling thread.
+     * Has to be used as a first argument for {@link Os#prctl(int, long, long, long, long)}.
+     *
+     * See <a href="https://man7.org/linux/man-pages/man2/prctl.2.html">prctl(2)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int PR_CAP_AMBIENT = placeholder();
+    /**
+     * The capability specified in {@code arg3} of {@link Os#prctl(int, long, long, long, long)}
+     * is added to the ambient set. The specified capability must already
+     * be present in both the permitted and the inheritable sets of the process.
+     * Has to be used as a second argument for {@link Os#prctl(int, long, long, long, long)}.
+     *
+     * See <a href="https://man7.org/linux/man-pages/man2/prctl.2.html">prctl(2)</a>.
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int PR_CAP_AMBIENT_RAISE = placeholder();
+    public static final int PR_GET_DUMPABLE = placeholder();
+    public static final int PR_SET_DUMPABLE = placeholder();
+    public static final int PR_SET_NO_NEW_PRIVS = placeholder();
+    public static final int PROT_EXEC = placeholder();
+    public static final int PROT_NONE = placeholder();
+    public static final int PROT_READ = placeholder();
+    public static final int PROT_WRITE = placeholder();
+    public static final int R_OK = placeholder();
+    /**
+     * Specifies a value one greater than the maximum file
+     * descriptor number that can be opened by this process.
+     *
+     * <p>Attempts ({@link Os#open(String, int, int)}, {@link Os#pipe()},
+     * {@link Os#dup(java.io.FileDescriptor)}, etc.) to exceed this
+     * limit yield the error {@link EMFILE}.
+     *
+     * See <a href="https://man7.org/linux/man-pages/man3/vlimit.3.html">getrlimit(2)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int RLIMIT_NOFILE = placeholder();
+    public static final int RT_SCOPE_HOST = placeholder();
+    public static final int RT_SCOPE_LINK = placeholder();
+    public static final int RT_SCOPE_NOWHERE = placeholder();
+    public static final int RT_SCOPE_SITE = placeholder();
+    public static final int RT_SCOPE_UNIVERSE = placeholder();
+    /**
+     * Bitmask for IPv4 addresses add/delete events multicast groups mask.
+     * Used in {@link NetlinkSocketAddress}.
+     *
+     * See <a href="https://man7.org/linux/man-pages/man7/netlink.7.html">netlink(7)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int RTMGRP_IPV4_IFADDR = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int RTMGRP_IPV4_MROUTE = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int RTMGRP_IPV4_ROUTE = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int RTMGRP_IPV4_RULE = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int RTMGRP_IPV6_IFADDR = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int RTMGRP_IPV6_IFINFO = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int RTMGRP_IPV6_MROUTE = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int RTMGRP_IPV6_PREFIX = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int RTMGRP_IPV6_ROUTE = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int RTMGRP_LINK = placeholder();
+    public static final int RTMGRP_NEIGH = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int RTMGRP_NOTIFY = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int RTMGRP_TC = placeholder();
+    public static final int SEEK_CUR = placeholder();
+    public static final int SEEK_END = placeholder();
+    public static final int SEEK_SET = placeholder();
+    public static final int SHUT_RD = placeholder();
+    public static final int SHUT_RDWR = placeholder();
+    public static final int SHUT_WR = placeholder();
+    public static final int SIGABRT = placeholder();
+    public static final int SIGALRM = placeholder();
+    public static final int SIGBUS = placeholder();
+    public static final int SIGCHLD = placeholder();
+    public static final int SIGCONT = placeholder();
+    public static final int SIGFPE = placeholder();
+    public static final int SIGHUP = placeholder();
+    public static final int SIGILL = placeholder();
+    public static final int SIGINT = placeholder();
+    public static final int SIGIO = placeholder();
+    public static final int SIGKILL = placeholder();
+    public static final int SIGPIPE = placeholder();
+    public static final int SIGPROF = placeholder();
+    public static final int SIGPWR = placeholder();
+    public static final int SIGQUIT = placeholder();
+    public static final int SIGRTMAX = placeholder();
+    public static final int SIGRTMIN = placeholder();
+    public static final int SIGSEGV = placeholder();
+    public static final int SIGSTKFLT = placeholder();
+    public static final int SIGSTOP = placeholder();
+    public static final int SIGSYS = placeholder();
+    public static final int SIGTERM = placeholder();
+    public static final int SIGTRAP = placeholder();
+    public static final int SIGTSTP = placeholder();
+    public static final int SIGTTIN = placeholder();
+    public static final int SIGTTOU = placeholder();
+    public static final int SIGURG = placeholder();
+    public static final int SIGUSR1 = placeholder();
+    public static final int SIGUSR2 = placeholder();
+    public static final int SIGVTALRM = placeholder();
+    public static final int SIGWINCH = placeholder();
+    public static final int SIGXCPU = placeholder();
+    public static final int SIGXFSZ = placeholder();
+    public static final int SIOCGIFADDR = placeholder();
+    public static final int SIOCGIFBRDADDR = placeholder();
+    public static final int SIOCGIFDSTADDR = placeholder();
+    public static final int SIOCGIFNETMASK = placeholder();
+
+    /**
+     * Set the close-on-exec ({@code FD_CLOEXEC}) flag on the new file
+     * descriptor created by {@link Os#socket(int,int,int)} or
+     * {@link Os#socketpair(int,int,int,java.io.FileDescriptor,java.io.FileDescriptor)}.
+     * See the description of the O_CLOEXEC flag in
+     * <a href="http://man7.org/linux/man-pages/man2/open.2.html">open(2)</a>
+     * for reasons why this may be useful.
+     *
+     * <p>Applications wishing to make use of this flag on older API versions
+     * may use {@link #O_CLOEXEC} instead. On Android, {@code O_CLOEXEC} and
+     * {@code SOCK_CLOEXEC} are the same value.
+     */
+    public static final int SOCK_CLOEXEC = placeholder();
+    public static final int SOCK_DGRAM = placeholder();
+
+    /**
+     * Set the O_NONBLOCK file status flag on the file descriptor
+     * created by {@link Os#socket(int,int,int)} or
+     * {@link Os#socketpair(int,int,int,java.io.FileDescriptor,java.io.FileDescriptor)}.
+     *
+     * <p>Applications wishing to make use of this flag on older API versions
+     * may use {@link #O_NONBLOCK} instead. On Android, {@code O_NONBLOCK}
+     * and {@code SOCK_NONBLOCK} are the same value.
+     */
+    public static final int SOCK_NONBLOCK = placeholder();
+    public static final int SOCK_RAW = placeholder();
+    public static final int SOCK_SEQPACKET = placeholder();
+    public static final int SOCK_STREAM = placeholder();
+    public static final int SOL_SOCKET = placeholder();
+    public static final int SOL_UDP = placeholder();
+    public static final int SOL_PACKET = placeholder();
+    public static final int SO_BINDTODEVICE = placeholder();
+    public static final int SO_BROADCAST = placeholder();
+    public static final int SO_DEBUG = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int SO_DOMAIN = placeholder();
+    public static final int SO_DONTROUTE = placeholder();
+    public static final int SO_ERROR = placeholder();
+    public static final int SO_KEEPALIVE = placeholder();
+    public static final int SO_LINGER = placeholder();
+    public static final int SO_OOBINLINE = placeholder();
+    public static final int SO_PASSCRED = placeholder();
+    public static final int SO_PEERCRED = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int SO_PROTOCOL = placeholder();
+    public static final int SO_RCVBUF = placeholder();
+    public static final int SO_RCVLOWAT = placeholder();
+    public static final int SO_RCVTIMEO = placeholder();
+    public static final int SO_REUSEADDR = placeholder();
+    public static final int SO_SNDBUF = placeholder();
+    public static final int SO_SNDLOWAT = placeholder();
+    public static final int SO_SNDTIMEO = placeholder();
+    public static final int SO_TYPE = placeholder();
+    public static final int PACKET_IGNORE_OUTGOING = placeholder();
+    /**
+     * Bitmask for flags argument of
+     * {@link splice(java.io.FileDescriptor, Int64Ref , FileDescriptor, Int64Ref, long, int)}.
+     *
+     * Attempt to move pages instead of copying.  This is only a
+     * hint to the kernel: pages may still be copied if the
+     * kernel cannot move the pages from the pipe, or if the pipe
+     * buffers don't refer to full pages.
+     *
+     * See <a href="https://man7.org/linux/man-pages/man2/splice.2.html">splice(2)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int SPLICE_F_MOVE = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int SPLICE_F_NONBLOCK = placeholder();
+    /**
+     * Bitmask for flags argument of
+     * {@link splice(java.io.FileDescriptor, Int64Ref, FileDescriptor, Int64Ref, long, int)}.
+     *
+     * <p>Indicates that more data will be coming in a subsequent splice. This is
+     * a helpful hint when the {@code fdOut} refers to a socket.
+     *
+     * See <a href="https://man7.org/linux/man-pages/man2/splice.2.html">splice(2)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int SPLICE_F_MORE = placeholder();
+    public static final int STDERR_FILENO = placeholder();
+    public static final int STDIN_FILENO = placeholder();
+    public static final int STDOUT_FILENO = placeholder();
+    public static final int ST_MANDLOCK = placeholder();
+    public static final int ST_NOATIME = placeholder();
+    public static final int ST_NODEV = placeholder();
+    public static final int ST_NODIRATIME = placeholder();
+    public static final int ST_NOEXEC = placeholder();
+    public static final int ST_NOSUID = placeholder();
+    public static final int ST_RDONLY = placeholder();
+    public static final int ST_RELATIME = placeholder();
+    public static final int ST_SYNCHRONOUS = placeholder();
+    public static final int S_IFBLK = placeholder();
+    public static final int S_IFCHR = placeholder();
+    public static final int S_IFDIR = placeholder();
+    public static final int S_IFIFO = placeholder();
+    public static final int S_IFLNK = placeholder();
+    public static final int S_IFMT = placeholder();
+    public static final int S_IFREG = placeholder();
+    public static final int S_IFSOCK = placeholder();
+    public static final int S_IRGRP = placeholder();
+    public static final int S_IROTH = placeholder();
+    public static final int S_IRUSR = placeholder();
+    public static final int S_IRWXG = placeholder();
+    public static final int S_IRWXO = placeholder();
+    public static final int S_IRWXU = placeholder();
+    public static final int S_ISGID = placeholder();
+    public static final int S_ISUID = placeholder();
+    public static final int S_ISVTX = placeholder();
+    public static final int S_IWGRP = placeholder();
+    public static final int S_IWOTH = placeholder();
+    public static final int S_IWUSR = placeholder();
+    public static final int S_IXGRP = placeholder();
+    public static final int S_IXOTH = placeholder();
+    public static final int S_IXUSR = placeholder();
+    public static final int TCP_NODELAY = placeholder();
+    public static final int TCP_USER_TIMEOUT = placeholder();
+    public static final int UDP_GRO = placeholder();
+    public static final int UDP_SEGMENT = placeholder();
+    /**
+     * Get the number of bytes in the output buffer.
+     *
+     * See <a href="https://man7.org/linux/man-pages/man2/ioctl.2.html">ioctl(2)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int TIOCOUTQ = placeholder();
+    /**
+     * Sockopt option to encapsulate ESP packets in UDP.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int UDP_ENCAP = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int UDP_ENCAP_ESPINUDP_NON_IKE = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int UDP_ENCAP_ESPINUDP = placeholder();
+    /** @hide */
+//    @UnsupportedAppUsage
+    public static final int UNIX_PATH_MAX = placeholder();
+    public static final int WCONTINUED = placeholder();
+    public static final int WEXITED = placeholder();
+    public static final int WNOHANG = placeholder();
+    public static final int WNOWAIT = placeholder();
+    public static final int WSTOPPED = placeholder();
+    public static final int WUNTRACED = placeholder();
+    public static final int W_OK = placeholder();
+    /**
+     * {@code flags} option for {@link Os#setxattr(String, String, byte[], int)}.
+     *
+     * <p>Performs a pure create, which fails if the named attribute exists already.
+     *
+     * See <a href="http://man7.org/linux/man-pages/man2/setxattr.2.html">setxattr(2)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int XATTR_CREATE = placeholder();
+    /**
+     * {@code flags} option for {@link Os#setxattr(String, String, byte[], int)}.
+     *
+     * <p>Perform a pure replace operation, which fails if the named attribute
+     * does not already exist.
+     *
+     * See <a href="http://man7.org/linux/man-pages/man2/setxattr.2.html">setxattr(2)</a>.
+     *
+     * @hide
+     */
+//    @UnsupportedAppUsage
+//    @SystemApi(client = MODULE_LIBRARIES)
+    public static final int XATTR_REPLACE = placeholder();
+    public static final int X_OK = placeholder();
+    public static final int _SC_2_CHAR_TERM = placeholder();
+    public static final int _SC_2_C_BIND = placeholder();
+    public static final int _SC_2_C_DEV = placeholder();
+    public static final int _SC_2_C_VERSION = placeholder();
+    public static final int _SC_2_FORT_DEV = placeholder();
+    public static final int _SC_2_FORT_RUN = placeholder();
+    public static final int _SC_2_LOCALEDEF = placeholder();
+    public static final int _SC_2_SW_DEV = placeholder();
+    public static final int _SC_2_UPE = placeholder();
+    public static final int _SC_2_VERSION = placeholder();
+    public static final int _SC_AIO_LISTIO_MAX = placeholder();
+    public static final int _SC_AIO_MAX = placeholder();
+    public static final int _SC_AIO_PRIO_DELTA_MAX = placeholder();
+    public static final int _SC_ARG_MAX = placeholder();
+    public static final int _SC_ASYNCHRONOUS_IO = placeholder();
+    public static final int _SC_ATEXIT_MAX = placeholder();
+    public static final int _SC_AVPHYS_PAGES = placeholder();
+    public static final int _SC_BC_BASE_MAX = placeholder();
+    public static final int _SC_BC_DIM_MAX = placeholder();
+    public static final int _SC_BC_SCALE_MAX = placeholder();
+    public static final int _SC_BC_STRING_MAX = placeholder();
+    public static final int _SC_CHILD_MAX = placeholder();
+    public static final int _SC_CLK_TCK = placeholder();
+    public static final int _SC_COLL_WEIGHTS_MAX = placeholder();
+    public static final int _SC_DELAYTIMER_MAX = placeholder();
+    public static final int _SC_EXPR_NEST_MAX = placeholder();
+    public static final int _SC_FSYNC = placeholder();
+    public static final int _SC_GETGR_R_SIZE_MAX = placeholder();
+    public static final int _SC_GETPW_R_SIZE_MAX = placeholder();
+    public static final int _SC_IOV_MAX = placeholder();
+    public static final int _SC_JOB_CONTROL = placeholder();
+    public static final int _SC_LINE_MAX = placeholder();
+    public static final int _SC_LOGIN_NAME_MAX = placeholder();
+    public static final int _SC_MAPPED_FILES = placeholder();
+    public static final int _SC_MEMLOCK = placeholder();
+    public static final int _SC_MEMLOCK_RANGE = placeholder();
+    public static final int _SC_MEMORY_PROTECTION = placeholder();
+    public static final int _SC_MESSAGE_PASSING = placeholder();
+    public static final int _SC_MQ_OPEN_MAX = placeholder();
+    public static final int _SC_MQ_PRIO_MAX = placeholder();
+    public static final int _SC_NGROUPS_MAX = placeholder();
+    public static final int _SC_NPROCESSORS_CONF = placeholder();
+    public static final int _SC_NPROCESSORS_ONLN = placeholder();
+    public static final int _SC_OPEN_MAX = placeholder();
+    public static final int _SC_PAGESIZE = placeholder();
+    public static final int _SC_PAGE_SIZE = placeholder();
+    public static final int _SC_PASS_MAX = placeholder();
+    public static final int _SC_PHYS_PAGES = placeholder();
+    public static final int _SC_PRIORITIZED_IO = placeholder();
+    public static final int _SC_PRIORITY_SCHEDULING = placeholder();
+    public static final int _SC_REALTIME_SIGNALS = placeholder();
+    public static final int _SC_RE_DUP_MAX = placeholder();
+    public static final int _SC_RTSIG_MAX = placeholder();
+    public static final int _SC_SAVED_IDS = placeholder();
+    public static final int _SC_SEMAPHORES = placeholder();
+    public static final int _SC_SEM_NSEMS_MAX = placeholder();
+    public static final int _SC_SEM_VALUE_MAX = placeholder();
+    public static final int _SC_SHARED_MEMORY_OBJECTS = placeholder();
+    public static final int _SC_SIGQUEUE_MAX = placeholder();
+    public static final int _SC_STREAM_MAX = placeholder();
+    public static final int _SC_SYNCHRONIZED_IO = placeholder();
+    public static final int _SC_THREADS = placeholder();
+    public static final int _SC_THREAD_ATTR_STACKADDR = placeholder();
+    public static final int _SC_THREAD_ATTR_STACKSIZE = placeholder();
+    public static final int _SC_THREAD_DESTRUCTOR_ITERATIONS = placeholder();
+    public static final int _SC_THREAD_KEYS_MAX = placeholder();
+    public static final int _SC_THREAD_PRIORITY_SCHEDULING = placeholder();
+    public static final int _SC_THREAD_PRIO_INHERIT = placeholder();
+    public static final int _SC_THREAD_PRIO_PROTECT = placeholder();
+    public static final int _SC_THREAD_SAFE_FUNCTIONS = placeholder();
+    public static final int _SC_THREAD_STACK_MIN = placeholder();
+    public static final int _SC_THREAD_THREADS_MAX = placeholder();
+    public static final int _SC_TIMERS = placeholder();
+    public static final int _SC_TIMER_MAX = placeholder();
+    public static final int _SC_TTY_NAME_MAX = placeholder();
+    public static final int _SC_TZNAME_MAX = placeholder();
+    public static final int _SC_VERSION = placeholder();
+    public static final int _SC_XBS5_ILP32_OFF32 = placeholder();
+    public static final int _SC_XBS5_ILP32_OFFBIG = placeholder();
+    public static final int _SC_XBS5_LP64_OFF64 = placeholder();
+    public static final int _SC_XBS5_LPBIG_OFFBIG = placeholder();
+    public static final int _SC_XOPEN_CRYPT = placeholder();
+    public static final int _SC_XOPEN_ENH_I18N = placeholder();
+    public static final int _SC_XOPEN_LEGACY = placeholder();
+    public static final int _SC_XOPEN_REALTIME = placeholder();
+    public static final int _SC_XOPEN_REALTIME_THREADS = placeholder();
+    public static final int _SC_XOPEN_SHM = placeholder();
+    public static final int _SC_XOPEN_UNIX = placeholder();
+    public static final int _SC_XOPEN_VERSION = placeholder();
+    public static final int _SC_XOPEN_XCU_VERSION = placeholder();
+
+    /**
+     * Returns the string name of a getaddrinfo(3) error value.
+     * For example, "EAI_AGAIN".
+     */
+    public static String gaiName(int error) {
+        if (error == EAI_AGAIN) {
+            return "EAI_AGAIN";
+        }
+        if (error == EAI_BADFLAGS) {
+            return "EAI_BADFLAGS";
+        }
+        if (error == EAI_FAIL) {
+            return "EAI_FAIL";
+        }
+        if (error == EAI_FAMILY) {
+            return "EAI_FAMILY";
+        }
+        if (error == EAI_MEMORY) {
+            return "EAI_MEMORY";
+        }
+        if (error == EAI_NODATA) {
+            return "EAI_NODATA";
+        }
+        if (error == EAI_NONAME) {
+            return "EAI_NONAME";
+        }
+        if (error == EAI_OVERFLOW) {
+            return "EAI_OVERFLOW";
+        }
+        if (error == EAI_SERVICE) {
+            return "EAI_SERVICE";
+        }
+        if (error == EAI_SOCKTYPE) {
+            return "EAI_SOCKTYPE";
+        }
+        if (error == EAI_SYSTEM) {
+            return "EAI_SYSTEM";
+        }
+        return null;
+    }
+
+    /**
+     * Returns the string name of an errno value.
+     * For example, "EACCES". See {@link Os#strerror} for human-readable errno descriptions.
+     */
+    public static String errnoName(int errno) {
+        if (errno == E2BIG) {
+            return "E2BIG";
+        }
+        if (errno == EACCES) {
+            return "EACCES";
+        }
+        if (errno == EADDRINUSE) {
+            return "EADDRINUSE";
+        }
+        if (errno == EADDRNOTAVAIL) {
+            return "EADDRNOTAVAIL";
+        }
+        if (errno == EAFNOSUPPORT) {
+            return "EAFNOSUPPORT";
+        }
+        if (errno == EAGAIN) {
+            return "EAGAIN";
+        }
+        if (errno == EALREADY) {
+            return "EALREADY";
+        }
+        if (errno == EBADF) {
+            return "EBADF";
+        }
+        if (errno == EBADMSG) {
+            return "EBADMSG";
+        }
+        if (errno == EBUSY) {
+            return "EBUSY";
+        }
+        if (errno == ECANCELED) {
+            return "ECANCELED";
+        }
+        if (errno == ECHILD) {
+            return "ECHILD";
+        }
+        if (errno == ECONNABORTED) {
+            return "ECONNABORTED";
+        }
+        if (errno == ECONNREFUSED) {
+            return "ECONNREFUSED";
+        }
+        if (errno == ECONNRESET) {
+            return "ECONNRESET";
+        }
+        if (errno == EDEADLK) {
+            return "EDEADLK";
+        }
+        if (errno == EDESTADDRREQ) {
+            return "EDESTADDRREQ";
+        }
+        if (errno == EDOM) {
+            return "EDOM";
+        }
+        if (errno == EDQUOT) {
+            return "EDQUOT";
+        }
+        if (errno == EEXIST) {
+            return "EEXIST";
+        }
+        if (errno == EFAULT) {
+            return "EFAULT";
+        }
+        if (errno == EFBIG) {
+            return "EFBIG";
+        }
+        if (errno == EHOSTUNREACH) {
+            return "EHOSTUNREACH";
+        }
+        if (errno == EIDRM) {
+            return "EIDRM";
+        }
+        if (errno == EILSEQ) {
+            return "EILSEQ";
+        }
+        if (errno == EINPROGRESS) {
+            return "EINPROGRESS";
+        }
+        if (errno == EINTR) {
+            return "EINTR";
+        }
+        if (errno == EINVAL) {
+            return "EINVAL";
+        }
+        if (errno == EIO) {
+            return "EIO";
+        }
+        if (errno == EISCONN) {
+            return "EISCONN";
+        }
+        if (errno == EISDIR) {
+            return "EISDIR";
+        }
+        if (errno == ELOOP) {
+            return "ELOOP";
+        }
+        if (errno == EMFILE) {
+            return "EMFILE";
+        }
+        if (errno == EMLINK) {
+            return "EMLINK";
+        }
+        if (errno == EMSGSIZE) {
+            return "EMSGSIZE";
+        }
+        if (errno == EMULTIHOP) {
+            return "EMULTIHOP";
+        }
+        if (errno == ENAMETOOLONG) {
+            return "ENAMETOOLONG";
+        }
+        if (errno == ENETDOWN) {
+            return "ENETDOWN";
+        }
+        if (errno == ENETRESET) {
+            return "ENETRESET";
+        }
+        if (errno == ENETUNREACH) {
+            return "ENETUNREACH";
+        }
+        if (errno == ENFILE) {
+            return "ENFILE";
+        }
+        if (errno == ENOBUFS) {
+            return "ENOBUFS";
+        }
+        if (errno == ENODATA) {
+            return "ENODATA";
+        }
+        if (errno == ENODEV) {
+            return "ENODEV";
+        }
+        if (errno == ENOENT) {
+            return "ENOENT";
+        }
+        if (errno == ENOEXEC) {
+            return "ENOEXEC";
+        }
+        if (errno == ENOLCK) {
+            return "ENOLCK";
+        }
+        if (errno == ENOLINK) {
+            return "ENOLINK";
+        }
+        if (errno == ENOMEM) {
+            return "ENOMEM";
+        }
+        if (errno == ENOMSG) {
+            return "ENOMSG";
+        }
+        if (errno == ENONET) {
+            return "ENONET";
+        }
+        if (errno == ENOPROTOOPT) {
+            return "ENOPROTOOPT";
+        }
+        if (errno == ENOSPC) {
+            return "ENOSPC";
+        }
+        if (errno == ENOSR) {
+            return "ENOSR";
+        }
+        if (errno == ENOSTR) {
+            return "ENOSTR";
+        }
+        if (errno == ENOSYS) {
+            return "ENOSYS";
+        }
+        if (errno == ENOTCONN) {
+            return "ENOTCONN";
+        }
+        if (errno == ENOTDIR) {
+            return "ENOTDIR";
+        }
+        if (errno == ENOTEMPTY) {
+            return "ENOTEMPTY";
+        }
+        if (errno == ENOTSOCK) {
+            return "ENOTSOCK";
+        }
+        if (errno == ENOTSUP) {
+            return "ENOTSUP";
+        }
+        if (errno == ENOTTY) {
+            return "ENOTTY";
+        }
+        if (errno == ENXIO) {
+            return "ENXIO";
+        }
+        if (errno == EOPNOTSUPP) {
+            return "EOPNOTSUPP";
+        }
+        if (errno == EOVERFLOW) {
+            return "EOVERFLOW";
+        }
+        if (errno == EPERM) {
+            return "EPERM";
+        }
+        if (errno == EPIPE) {
+            return "EPIPE";
+        }
+        if (errno == EPROTO) {
+            return "EPROTO";
+        }
+        if (errno == EPROTONOSUPPORT) {
+            return "EPROTONOSUPPORT";
+        }
+        if (errno == EPROTOTYPE) {
+            return "EPROTOTYPE";
+        }
+        if (errno == ERANGE) {
+            return "ERANGE";
+        }
+        if (errno == EROFS) {
+            return "EROFS";
+        }
+        if (errno == ESPIPE) {
+            return "ESPIPE";
+        }
+        if (errno == ESRCH) {
+            return "ESRCH";
+        }
+        if (errno == ESTALE) {
+            return "ESTALE";
+        }
+        if (errno == ETIME) {
+            return "ETIME";
+        }
+        if (errno == ETIMEDOUT) {
+            return "ETIMEDOUT";
+        }
+        if (errno == ETXTBSY) {
+            return "ETXTBSY";
+        }
+        if (errno == EXDEV) {
+            return "EXDEV";
+        }
+        return null;
+    }
+
+    // [ravenwood-change] Moved to a nested class.
+    //    @UnsupportedAppUsage
+    static class Native {
+        private static native void initConstants();
+    }
+
+    // A hack to avoid these constants being inlined by javac...
+//    @UnsupportedAppUsage
+    private static int placeholder() { return 0; }
+    // ...because we want to initialize them at runtime.
+    static {
+        // [ravenwood-change] Load the JNI lib.
+        RavenwoodCommonUtils.loadRavenwoodNativeRuntime();
+        Native.initConstants();
+    }
+}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/ravenwood/LibcoreRavenwoodUtils.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/ravenwood/LibcoreRavenwoodUtils.java
deleted file mode 100644
index 839b62a..0000000
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/ravenwood/LibcoreRavenwoodUtils.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package libcore.ravenwood;
-
-public class LibcoreRavenwoodUtils {
-    private LibcoreRavenwoodUtils() {
-    }
-
-    public static void loadRavenwoodNativeRuntime() {
-        // TODO Stop using reflections.
-        // We need to call RavenwoodUtils.loadRavenwoodNativeRuntime(), but due to the build
-        // structure complexity, we can't refer to to this method directly from here,
-        // so let's use reflections for now...
-        try {
-            final var clazz = Class.forName("android.platform.test.ravenwood.RavenwoodUtils");
-            final var method = clazz.getMethod("loadRavenwoodNativeRuntime");
-            method.invoke(null);
-        } catch (Throwable th) {
-            throw new IllegalStateException("Failed to load Ravenwood native runtime", th);
-        }
-    }
-}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
index 93861e8..14b5a4f 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
@@ -15,7 +15,7 @@
  */
 package libcore.util;
 
-import libcore.ravenwood.LibcoreRavenwoodUtils;
+import com.android.ravenwood.common.RavenwoodRuntimeNative;
 
 import java.lang.ref.Cleaner;
 import java.lang.ref.Reference;
@@ -27,11 +27,6 @@
  *   (Should ART switch to java.lang.ref.Cleaner?)
  */
 public class NativeAllocationRegistry {
-    static {
-        // Initialize the JNI method.
-        LibcoreRavenwoodUtils.loadRavenwoodNativeRuntime();
-    }
-
     private final long mFreeFunction;
     private static final Cleaner sCleaner = Cleaner.create();
 
@@ -71,7 +66,7 @@
         }
 
         final Runnable releaser = () -> {
-            applyFreeFunction(mFreeFunction, nativePtr);
+            RavenwoodRuntimeNative.applyFreeFunction(mFreeFunction, nativePtr);
         };
         sCleaner.register(referent, releaser);
 
@@ -79,10 +74,4 @@
         Reference.reachabilityFence(referent);
         return releaser;
     }
-
-    /**
-     * Calls {@code freeFunction}({@code nativePtr}).
-     */
-    public static native void applyFreeFunction(long freeFunction, long nativePtr);
 }
-
diff --git a/ravenwood/runtime-jni/ravenwood_os_constants.cpp b/ravenwood/runtime-jni/ravenwood_os_constants.cpp
new file mode 100644
index 0000000..ea6c9d4
--- /dev/null
+++ b/ravenwood/runtime-jni/ravenwood_os_constants.cpp
@@ -0,0 +1,766 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Copied from libcore/luni/src/main/native/android_system_OsConstants.cpp,
+// changes annotated with [ravenwood-change].
+
+#define LOG_TAG "OsConstants"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/tcp.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <sys/xattr.h>
+#include <unistd.h>
+
+#include <net/if_arp.h>
+#include <linux/if_ether.h>
+
+// After the others because these are not necessarily self-contained in glibc.
+#include <linux/if_addr.h>
+#include <linux/rtnetlink.h>
+
+// Include linux socket constants for setting sockopts
+#include <linux/udp.h>
+
+#include <net/if.h> // After <sys/socket.h> to work around a Mac header file bug.
+
+// [ravenwood-change] always include it
+// #if defined(__BIONIC__)
+#include <linux/capability.h>
+// #endif
+
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/jni_macros.h>
+
+// [ravenwood-change] -- can't access "Portability.h", so just inline it here.
+// #include "Portability.h"
+#include <byteswap.h>
+#include <sys/sendfile.h>
+#include <sys/statvfs.h>
+#include <netdb.h>
+#include <linux/vm_sockets.h>
+
+// For LOG_ALWAYS_FATAL_IF
+#include "utils/Log.h"
+
+
+static void initConstant(JNIEnv* env, jclass c, const char* fieldName, int value) {
+    jfieldID field = env->GetStaticFieldID(c, fieldName, "I");
+    env->SetStaticIntField(c, field, value);
+}
+
+static void OsConstants_initConstants(JNIEnv* env, jclass) {
+    // [ravenwood-change] -- the constants are in the outer class, but this JNI method is in the
+    // nested class, so we need to get the outer class here.
+    jclass c = env->FindClass("android/system/OsConstants");
+    LOG_ALWAYS_FATAL_IF(c == NULL, "Unable to find class android/system/OsConstants");
+
+    initConstant(env, c, "AF_INET", AF_INET);
+    initConstant(env, c, "AF_INET6", AF_INET6);
+    initConstant(env, c, "AF_PACKET", AF_PACKET);
+    initConstant(env, c, "AF_NETLINK", AF_NETLINK);
+    initConstant(env, c, "AF_UNIX", AF_UNIX);
+    initConstant(env, c, "AF_VSOCK", AF_VSOCK);
+    initConstant(env, c, "AF_UNSPEC", AF_UNSPEC);
+    initConstant(env, c, "AI_ADDRCONFIG", AI_ADDRCONFIG);
+    initConstant(env, c, "AI_ALL", AI_ALL);
+    initConstant(env, c, "AI_CANONNAME", AI_CANONNAME);
+    initConstant(env, c, "AI_NUMERICHOST", AI_NUMERICHOST);
+#if defined(AI_NUMERICSERV)
+    initConstant(env, c, "AI_NUMERICSERV", AI_NUMERICSERV);
+#endif
+    initConstant(env, c, "AI_PASSIVE", AI_PASSIVE);
+    initConstant(env, c, "AI_V4MAPPED", AI_V4MAPPED);
+    initConstant(env, c, "ARPHRD_ETHER", ARPHRD_ETHER);
+    initConstant(env, c, "VMADDR_PORT_ANY", VMADDR_PORT_ANY);
+    initConstant(env, c, "VMADDR_CID_ANY", VMADDR_CID_ANY);
+    initConstant(env, c, "VMADDR_CID_LOCAL", VMADDR_CID_LOCAL);
+    initConstant(env, c, "VMADDR_CID_HOST", VMADDR_CID_HOST);
+    initConstant(env, c, "ARPHRD_LOOPBACK", ARPHRD_LOOPBACK);
+#if defined(CAP_LAST_CAP)
+    initConstant(env, c, "CAP_AUDIT_CONTROL", CAP_AUDIT_CONTROL);
+    initConstant(env, c, "CAP_AUDIT_WRITE", CAP_AUDIT_WRITE);
+    initConstant(env, c, "CAP_BLOCK_SUSPEND", CAP_BLOCK_SUSPEND);
+    initConstant(env, c, "CAP_CHOWN", CAP_CHOWN);
+    initConstant(env, c, "CAP_DAC_OVERRIDE", CAP_DAC_OVERRIDE);
+    initConstant(env, c, "CAP_DAC_READ_SEARCH", CAP_DAC_READ_SEARCH);
+    initConstant(env, c, "CAP_FOWNER", CAP_FOWNER);
+    initConstant(env, c, "CAP_FSETID", CAP_FSETID);
+    initConstant(env, c, "CAP_IPC_LOCK", CAP_IPC_LOCK);
+    initConstant(env, c, "CAP_IPC_OWNER", CAP_IPC_OWNER);
+    initConstant(env, c, "CAP_KILL", CAP_KILL);
+    initConstant(env, c, "CAP_LAST_CAP", CAP_LAST_CAP);
+    initConstant(env, c, "CAP_LEASE", CAP_LEASE);
+    initConstant(env, c, "CAP_LINUX_IMMUTABLE", CAP_LINUX_IMMUTABLE);
+    initConstant(env, c, "CAP_MAC_ADMIN", CAP_MAC_ADMIN);
+    initConstant(env, c, "CAP_MAC_OVERRIDE", CAP_MAC_OVERRIDE);
+    initConstant(env, c, "CAP_MKNOD", CAP_MKNOD);
+    initConstant(env, c, "CAP_NET_ADMIN", CAP_NET_ADMIN);
+    initConstant(env, c, "CAP_NET_BIND_SERVICE", CAP_NET_BIND_SERVICE);
+    initConstant(env, c, "CAP_NET_BROADCAST", CAP_NET_BROADCAST);
+    initConstant(env, c, "CAP_NET_RAW", CAP_NET_RAW);
+    initConstant(env, c, "CAP_SETFCAP", CAP_SETFCAP);
+    initConstant(env, c, "CAP_SETGID", CAP_SETGID);
+    initConstant(env, c, "CAP_SETPCAP", CAP_SETPCAP);
+    initConstant(env, c, "CAP_SETUID", CAP_SETUID);
+    initConstant(env, c, "CAP_SYS_ADMIN", CAP_SYS_ADMIN);
+    initConstant(env, c, "CAP_SYS_BOOT", CAP_SYS_BOOT);
+    initConstant(env, c, "CAP_SYS_CHROOT", CAP_SYS_CHROOT);
+    initConstant(env, c, "CAP_SYSLOG", CAP_SYSLOG);
+    initConstant(env, c, "CAP_SYS_MODULE", CAP_SYS_MODULE);
+    initConstant(env, c, "CAP_SYS_NICE", CAP_SYS_NICE);
+    initConstant(env, c, "CAP_SYS_PACCT", CAP_SYS_PACCT);
+    initConstant(env, c, "CAP_SYS_PTRACE", CAP_SYS_PTRACE);
+    initConstant(env, c, "CAP_SYS_RAWIO", CAP_SYS_RAWIO);
+    initConstant(env, c, "CAP_SYS_RESOURCE", CAP_SYS_RESOURCE);
+    initConstant(env, c, "CAP_SYS_TIME", CAP_SYS_TIME);
+    initConstant(env, c, "CAP_SYS_TTY_CONFIG", CAP_SYS_TTY_CONFIG);
+    initConstant(env, c, "CAP_WAKE_ALARM", CAP_WAKE_ALARM);
+#endif
+    initConstant(env, c, "E2BIG", E2BIG);
+    initConstant(env, c, "EACCES", EACCES);
+    initConstant(env, c, "EADDRINUSE", EADDRINUSE);
+    initConstant(env, c, "EADDRNOTAVAIL", EADDRNOTAVAIL);
+    initConstant(env, c, "EAFNOSUPPORT", EAFNOSUPPORT);
+    initConstant(env, c, "EAGAIN", EAGAIN);
+    initConstant(env, c, "EAI_AGAIN", EAI_AGAIN);
+    initConstant(env, c, "EAI_BADFLAGS", EAI_BADFLAGS);
+    initConstant(env, c, "EAI_FAIL", EAI_FAIL);
+    initConstant(env, c, "EAI_FAMILY", EAI_FAMILY);
+    initConstant(env, c, "EAI_MEMORY", EAI_MEMORY);
+    initConstant(env, c, "EAI_NODATA", EAI_NODATA);
+    initConstant(env, c, "EAI_NONAME", EAI_NONAME);
+#if defined(EAI_OVERFLOW)
+    initConstant(env, c, "EAI_OVERFLOW", EAI_OVERFLOW);
+#endif
+    initConstant(env, c, "EAI_SERVICE", EAI_SERVICE);
+    initConstant(env, c, "EAI_SOCKTYPE", EAI_SOCKTYPE);
+    initConstant(env, c, "EAI_SYSTEM", EAI_SYSTEM);
+    initConstant(env, c, "EALREADY", EALREADY);
+    initConstant(env, c, "EBADF", EBADF);
+    initConstant(env, c, "EBADMSG", EBADMSG);
+    initConstant(env, c, "EBUSY", EBUSY);
+    initConstant(env, c, "ECANCELED", ECANCELED);
+    initConstant(env, c, "ECHILD", ECHILD);
+    initConstant(env, c, "ECONNABORTED", ECONNABORTED);
+    initConstant(env, c, "ECONNREFUSED", ECONNREFUSED);
+    initConstant(env, c, "ECONNRESET", ECONNRESET);
+    initConstant(env, c, "EDEADLK", EDEADLK);
+    initConstant(env, c, "EDESTADDRREQ", EDESTADDRREQ);
+    initConstant(env, c, "EDOM", EDOM);
+    initConstant(env, c, "EDQUOT", EDQUOT);
+    initConstant(env, c, "EEXIST", EEXIST);
+    initConstant(env, c, "EFAULT", EFAULT);
+    initConstant(env, c, "EFBIG", EFBIG);
+    initConstant(env, c, "EHOSTUNREACH", EHOSTUNREACH);
+    initConstant(env, c, "EIDRM", EIDRM);
+    initConstant(env, c, "EILSEQ", EILSEQ);
+    initConstant(env, c, "EINPROGRESS", EINPROGRESS);
+    initConstant(env, c, "EINTR", EINTR);
+    initConstant(env, c, "EINVAL", EINVAL);
+    initConstant(env, c, "EIO", EIO);
+    initConstant(env, c, "EISCONN", EISCONN);
+    initConstant(env, c, "EISDIR", EISDIR);
+    initConstant(env, c, "ELOOP", ELOOP);
+    initConstant(env, c, "EMFILE", EMFILE);
+    initConstant(env, c, "EMLINK", EMLINK);
+    initConstant(env, c, "EMSGSIZE", EMSGSIZE);
+    initConstant(env, c, "EMULTIHOP", EMULTIHOP);
+    initConstant(env, c, "ENAMETOOLONG", ENAMETOOLONG);
+    initConstant(env, c, "ENETDOWN", ENETDOWN);
+    initConstant(env, c, "ENETRESET", ENETRESET);
+    initConstant(env, c, "ENETUNREACH", ENETUNREACH);
+    initConstant(env, c, "ENFILE", ENFILE);
+    initConstant(env, c, "ENOBUFS", ENOBUFS);
+    initConstant(env, c, "ENODATA", ENODATA);
+    initConstant(env, c, "ENODEV", ENODEV);
+    initConstant(env, c, "ENOENT", ENOENT);
+    initConstant(env, c, "ENOEXEC", ENOEXEC);
+    initConstant(env, c, "ENOLCK", ENOLCK);
+    initConstant(env, c, "ENOLINK", ENOLINK);
+    initConstant(env, c, "ENOMEM", ENOMEM);
+    initConstant(env, c, "ENOMSG", ENOMSG);
+    initConstant(env, c, "ENONET", ENONET);
+    initConstant(env, c, "ENOPROTOOPT", ENOPROTOOPT);
+    initConstant(env, c, "ENOSPC", ENOSPC);
+    initConstant(env, c, "ENOSR", ENOSR);
+    initConstant(env, c, "ENOSTR", ENOSTR);
+    initConstant(env, c, "ENOSYS", ENOSYS);
+    initConstant(env, c, "ENOTCONN", ENOTCONN);
+    initConstant(env, c, "ENOTDIR", ENOTDIR);
+    initConstant(env, c, "ENOTEMPTY", ENOTEMPTY);
+    initConstant(env, c, "ENOTSOCK", ENOTSOCK);
+    initConstant(env, c, "ENOTSUP", ENOTSUP);
+    initConstant(env, c, "ENOTTY", ENOTTY);
+    initConstant(env, c, "ENXIO", ENXIO);
+    initConstant(env, c, "EOPNOTSUPP", EOPNOTSUPP);
+    initConstant(env, c, "EOVERFLOW", EOVERFLOW);
+    initConstant(env, c, "EPERM", EPERM);
+    initConstant(env, c, "EPIPE", EPIPE);
+    initConstant(env, c, "EPROTO", EPROTO);
+    initConstant(env, c, "EPROTONOSUPPORT", EPROTONOSUPPORT);
+    initConstant(env, c, "EPROTOTYPE", EPROTOTYPE);
+    initConstant(env, c, "ERANGE", ERANGE);
+    initConstant(env, c, "EROFS", EROFS);
+    initConstant(env, c, "ESPIPE", ESPIPE);
+    initConstant(env, c, "ESRCH", ESRCH);
+    initConstant(env, c, "ESTALE", ESTALE);
+    initConstant(env, c, "ETH_P_ALL", ETH_P_ALL);
+    initConstant(env, c, "ETH_P_ARP", ETH_P_ARP);
+    initConstant(env, c, "ETH_P_IP", ETH_P_IP);
+    initConstant(env, c, "ETH_P_IPV6", ETH_P_IPV6);
+    initConstant(env, c, "ETIME", ETIME);
+    initConstant(env, c, "ETIMEDOUT", ETIMEDOUT);
+    initConstant(env, c, "ETXTBSY", ETXTBSY);
+    initConstant(env, c, "EUSERS", EUSERS);
+#if EWOULDBLOCK != EAGAIN
+#error EWOULDBLOCK != EAGAIN
+#endif
+    initConstant(env, c, "EXDEV", EXDEV);
+    initConstant(env, c, "EXIT_FAILURE", EXIT_FAILURE);
+    initConstant(env, c, "EXIT_SUCCESS", EXIT_SUCCESS);
+    initConstant(env, c, "FD_CLOEXEC", FD_CLOEXEC);
+    initConstant(env, c, "FIONREAD", FIONREAD);
+    initConstant(env, c, "F_DUPFD", F_DUPFD);
+    initConstant(env, c, "F_DUPFD_CLOEXEC", F_DUPFD_CLOEXEC);
+    initConstant(env, c, "F_GETFD", F_GETFD);
+    initConstant(env, c, "F_GETFL", F_GETFL);
+    initConstant(env, c, "F_GETLK", F_GETLK);
+#if defined(F_GETLK64)
+    initConstant(env, c, "F_GETLK64", F_GETLK64);
+#endif
+    initConstant(env, c, "F_GETOWN", F_GETOWN);
+    initConstant(env, c, "F_OK", F_OK);
+    initConstant(env, c, "F_RDLCK", F_RDLCK);
+    initConstant(env, c, "F_SETFD", F_SETFD);
+    initConstant(env, c, "F_SETFL", F_SETFL);
+    initConstant(env, c, "F_SETLK", F_SETLK);
+#if defined(F_SETLK64)
+    initConstant(env, c, "F_SETLK64", F_SETLK64);
+#endif
+    initConstant(env, c, "F_SETLKW", F_SETLKW);
+#if defined(F_SETLKW64)
+    initConstant(env, c, "F_SETLKW64", F_SETLKW64);
+#endif
+    initConstant(env, c, "F_SETOWN", F_SETOWN);
+    initConstant(env, c, "F_UNLCK", F_UNLCK);
+    initConstant(env, c, "F_WRLCK", F_WRLCK);
+    initConstant(env, c, "ICMP_ECHO", ICMP_ECHO);
+    initConstant(env, c, "ICMP_ECHOREPLY", ICMP_ECHOREPLY);
+    initConstant(env, c, "ICMP6_ECHO_REQUEST", ICMP6_ECHO_REQUEST);
+    initConstant(env, c, "ICMP6_ECHO_REPLY", ICMP6_ECHO_REPLY);
+#if defined(IFA_F_DADFAILED)
+    initConstant(env, c, "IFA_F_DADFAILED", IFA_F_DADFAILED);
+#endif
+#if defined(IFA_F_DEPRECATED)
+    initConstant(env, c, "IFA_F_DEPRECATED", IFA_F_DEPRECATED);
+#endif
+#if defined(IFA_F_HOMEADDRESS)
+    initConstant(env, c, "IFA_F_HOMEADDRESS", IFA_F_HOMEADDRESS);
+#endif
+#if defined(IFA_F_MANAGETEMPADDR)
+    initConstant(env, c, "IFA_F_MANAGETEMPADDR", IFA_F_MANAGETEMPADDR);
+#endif
+#if defined(IFA_F_NODAD)
+    initConstant(env, c, "IFA_F_NODAD", IFA_F_NODAD);
+#endif
+#if defined(IFA_F_NOPREFIXROUTE)
+    initConstant(env, c, "IFA_F_NOPREFIXROUTE", IFA_F_NOPREFIXROUTE);
+#endif
+#if defined(IFA_F_OPTIMISTIC)
+    initConstant(env, c, "IFA_F_OPTIMISTIC", IFA_F_OPTIMISTIC);
+#endif
+#if defined(IFA_F_PERMANENT)
+    initConstant(env, c, "IFA_F_PERMANENT", IFA_F_PERMANENT);
+#endif
+#if defined(IFA_F_SECONDARY)
+    initConstant(env, c, "IFA_F_SECONDARY", IFA_F_SECONDARY);
+#endif
+#if defined(IFA_F_TEMPORARY)
+    initConstant(env, c, "IFA_F_TEMPORARY", IFA_F_TEMPORARY);
+#endif
+#if defined(IFA_F_TENTATIVE)
+    initConstant(env, c, "IFA_F_TENTATIVE", IFA_F_TENTATIVE);
+#endif
+    initConstant(env, c, "IFF_ALLMULTI", IFF_ALLMULTI);
+#if defined(IFF_AUTOMEDIA)
+    initConstant(env, c, "IFF_AUTOMEDIA", IFF_AUTOMEDIA);
+#endif
+    initConstant(env, c, "IFF_BROADCAST", IFF_BROADCAST);
+    initConstant(env, c, "IFF_DEBUG", IFF_DEBUG);
+#if defined(IFF_DYNAMIC)
+    initConstant(env, c, "IFF_DYNAMIC", IFF_DYNAMIC);
+#endif
+    initConstant(env, c, "IFF_LOOPBACK", IFF_LOOPBACK);
+#if defined(IFF_MASTER)
+    initConstant(env, c, "IFF_MASTER", IFF_MASTER);
+#endif
+    initConstant(env, c, "IFF_MULTICAST", IFF_MULTICAST);
+    initConstant(env, c, "IFF_NOARP", IFF_NOARP);
+    initConstant(env, c, "IFF_NOTRAILERS", IFF_NOTRAILERS);
+    initConstant(env, c, "IFF_POINTOPOINT", IFF_POINTOPOINT);
+#if defined(IFF_PORTSEL)
+    initConstant(env, c, "IFF_PORTSEL", IFF_PORTSEL);
+#endif
+    initConstant(env, c, "IFF_PROMISC", IFF_PROMISC);
+    initConstant(env, c, "IFF_RUNNING", IFF_RUNNING);
+#if defined(IFF_SLAVE)
+    initConstant(env, c, "IFF_SLAVE", IFF_SLAVE);
+#endif
+    initConstant(env, c, "IFF_UP", IFF_UP);
+    initConstant(env, c, "IPPROTO_ICMP", IPPROTO_ICMP);
+    initConstant(env, c, "IPPROTO_ICMPV6", IPPROTO_ICMPV6);
+    initConstant(env, c, "IPPROTO_IP", IPPROTO_IP);
+    initConstant(env, c, "IPPROTO_IPV6", IPPROTO_IPV6);
+    initConstant(env, c, "IPPROTO_RAW", IPPROTO_RAW);
+    initConstant(env, c, "IPPROTO_TCP", IPPROTO_TCP);
+    initConstant(env, c, "IPPROTO_UDP", IPPROTO_UDP);
+    initConstant(env, c, "IPPROTO_ESP", IPPROTO_ESP);
+    initConstant(env, c, "IPV6_CHECKSUM", IPV6_CHECKSUM);
+    initConstant(env, c, "IPV6_MULTICAST_HOPS", IPV6_MULTICAST_HOPS);
+    initConstant(env, c, "IPV6_MULTICAST_IF", IPV6_MULTICAST_IF);
+    initConstant(env, c, "IPV6_MULTICAST_LOOP", IPV6_MULTICAST_LOOP);
+#if defined(IPV6_PKTINFO)
+    initConstant(env, c, "IPV6_PKTINFO", IPV6_PKTINFO);
+#endif
+#if defined(IPV6_RECVDSTOPTS)
+    initConstant(env, c, "IPV6_RECVDSTOPTS", IPV6_RECVDSTOPTS);
+#endif
+#if defined(IPV6_RECVHOPLIMIT)
+    initConstant(env, c, "IPV6_RECVHOPLIMIT", IPV6_RECVHOPLIMIT);
+#endif
+#if defined(IPV6_RECVHOPOPTS)
+    initConstant(env, c, "IPV6_RECVHOPOPTS", IPV6_RECVHOPOPTS);
+#endif
+#if defined(IPV6_RECVPKTINFO)
+    initConstant(env, c, "IPV6_RECVPKTINFO", IPV6_RECVPKTINFO);
+#endif
+#if defined(IPV6_RECVRTHDR)
+    initConstant(env, c, "IPV6_RECVRTHDR", IPV6_RECVRTHDR);
+#endif
+#if defined(IPV6_RECVTCLASS)
+    initConstant(env, c, "IPV6_RECVTCLASS", IPV6_RECVTCLASS);
+#endif
+#if defined(IPV6_TCLASS)
+    initConstant(env, c, "IPV6_TCLASS", IPV6_TCLASS);
+#endif
+    initConstant(env, c, "IPV6_UNICAST_HOPS", IPV6_UNICAST_HOPS);
+    initConstant(env, c, "IPV6_V6ONLY", IPV6_V6ONLY);
+    initConstant(env, c, "IP_MULTICAST_ALL", IP_MULTICAST_ALL);
+    initConstant(env, c, "IP_MULTICAST_IF", IP_MULTICAST_IF);
+    initConstant(env, c, "IP_MULTICAST_LOOP", IP_MULTICAST_LOOP);
+    initConstant(env, c, "IP_MULTICAST_TTL", IP_MULTICAST_TTL);
+    initConstant(env, c, "IP_RECVTOS", IP_RECVTOS);
+    initConstant(env, c, "IP_TOS", IP_TOS);
+    initConstant(env, c, "IP_TTL", IP_TTL);
+#if defined(_LINUX_CAPABILITY_VERSION_3)
+    initConstant(env, c, "_LINUX_CAPABILITY_VERSION_3", _LINUX_CAPABILITY_VERSION_3);
+#endif
+    initConstant(env, c, "MAP_FIXED", MAP_FIXED);
+    initConstant(env, c, "MAP_ANONYMOUS", MAP_ANONYMOUS);
+    initConstant(env, c, "MAP_POPULATE", MAP_POPULATE);
+    initConstant(env, c, "MAP_PRIVATE", MAP_PRIVATE);
+    initConstant(env, c, "MAP_SHARED", MAP_SHARED);
+#if defined(MCAST_JOIN_GROUP)
+    initConstant(env, c, "MCAST_JOIN_GROUP", MCAST_JOIN_GROUP);
+#endif
+#if defined(MCAST_LEAVE_GROUP)
+    initConstant(env, c, "MCAST_LEAVE_GROUP", MCAST_LEAVE_GROUP);
+#endif
+#if defined(MCAST_JOIN_SOURCE_GROUP)
+    initConstant(env, c, "MCAST_JOIN_SOURCE_GROUP", MCAST_JOIN_SOURCE_GROUP);
+#endif
+#if defined(MCAST_LEAVE_SOURCE_GROUP)
+    initConstant(env, c, "MCAST_LEAVE_SOURCE_GROUP", MCAST_LEAVE_SOURCE_GROUP);
+#endif
+#if defined(MCAST_BLOCK_SOURCE)
+    initConstant(env, c, "MCAST_BLOCK_SOURCE", MCAST_BLOCK_SOURCE);
+#endif
+#if defined(MCAST_UNBLOCK_SOURCE)
+    initConstant(env, c, "MCAST_UNBLOCK_SOURCE", MCAST_UNBLOCK_SOURCE);
+#endif
+    initConstant(env, c, "MCL_CURRENT", MCL_CURRENT);
+    initConstant(env, c, "MCL_FUTURE", MCL_FUTURE);
+#if defined(MFD_CLOEXEC)
+    initConstant(env, c, "MFD_CLOEXEC", MFD_CLOEXEC);
+#endif
+    initConstant(env, c, "MSG_CTRUNC", MSG_CTRUNC);
+    initConstant(env, c, "MSG_DONTROUTE", MSG_DONTROUTE);
+    initConstant(env, c, "MSG_EOR", MSG_EOR);
+    initConstant(env, c, "MSG_OOB", MSG_OOB);
+    initConstant(env, c, "MSG_PEEK", MSG_PEEK);
+    initConstant(env, c, "MSG_TRUNC", MSG_TRUNC);
+    initConstant(env, c, "MSG_WAITALL", MSG_WAITALL);
+    initConstant(env, c, "MS_ASYNC", MS_ASYNC);
+    initConstant(env, c, "MS_INVALIDATE", MS_INVALIDATE);
+    initConstant(env, c, "MS_SYNC", MS_SYNC);
+    initConstant(env, c, "NETLINK_NETFILTER", NETLINK_NETFILTER);
+    initConstant(env, c, "NETLINK_ROUTE", NETLINK_ROUTE);
+    initConstant(env, c, "NETLINK_INET_DIAG", NETLINK_INET_DIAG);
+    initConstant(env, c, "NETLINK_XFRM", NETLINK_XFRM);
+    initConstant(env, c, "NI_DGRAM", NI_DGRAM);
+    initConstant(env, c, "NI_NAMEREQD", NI_NAMEREQD);
+    initConstant(env, c, "NI_NOFQDN", NI_NOFQDN);
+    initConstant(env, c, "NI_NUMERICHOST", NI_NUMERICHOST);
+    initConstant(env, c, "NI_NUMERICSERV", NI_NUMERICSERV);
+    initConstant(env, c, "O_ACCMODE", O_ACCMODE);
+    initConstant(env, c, "O_APPEND", O_APPEND);
+    initConstant(env, c, "O_CLOEXEC", O_CLOEXEC);
+    initConstant(env, c, "O_CREAT", O_CREAT);
+    initConstant(env, c, "O_DIRECT", O_DIRECT);
+    initConstant(env, c, "O_EXCL", O_EXCL);
+    initConstant(env, c, "O_NOCTTY", O_NOCTTY);
+    initConstant(env, c, "O_NOFOLLOW", O_NOFOLLOW);
+    initConstant(env, c, "O_NONBLOCK", O_NONBLOCK);
+    initConstant(env, c, "O_RDONLY", O_RDONLY);
+    initConstant(env, c, "O_RDWR", O_RDWR);
+    initConstant(env, c, "O_SYNC", O_SYNC);
+    initConstant(env, c, "O_DSYNC", O_DSYNC);
+    initConstant(env, c, "O_TRUNC", O_TRUNC);
+    initConstant(env, c, "O_WRONLY", O_WRONLY);
+    initConstant(env, c, "POLLERR", POLLERR);
+    initConstant(env, c, "POLLHUP", POLLHUP);
+    initConstant(env, c, "POLLIN", POLLIN);
+    initConstant(env, c, "POLLNVAL", POLLNVAL);
+    initConstant(env, c, "POLLOUT", POLLOUT);
+    initConstant(env, c, "POLLPRI", POLLPRI);
+    initConstant(env, c, "POLLRDBAND", POLLRDBAND);
+    initConstant(env, c, "POLLRDNORM", POLLRDNORM);
+    initConstant(env, c, "POLLWRBAND", POLLWRBAND);
+    initConstant(env, c, "POLLWRNORM", POLLWRNORM);
+#if defined(PR_CAP_AMBIENT)
+    initConstant(env, c, "PR_CAP_AMBIENT", PR_CAP_AMBIENT);
+#endif
+#if defined(PR_CAP_AMBIENT_RAISE)
+    initConstant(env, c, "PR_CAP_AMBIENT_RAISE", PR_CAP_AMBIENT_RAISE);
+#endif
+#if defined(PR_GET_DUMPABLE)
+    initConstant(env, c, "PR_GET_DUMPABLE", PR_GET_DUMPABLE);
+#endif
+#if defined(PR_SET_DUMPABLE)
+    initConstant(env, c, "PR_SET_DUMPABLE", PR_SET_DUMPABLE);
+#endif
+#if defined(PR_SET_NO_NEW_PRIVS)
+    initConstant(env, c, "PR_SET_NO_NEW_PRIVS", PR_SET_NO_NEW_PRIVS);
+#endif
+    initConstant(env, c, "PROT_EXEC", PROT_EXEC);
+    initConstant(env, c, "PROT_NONE", PROT_NONE);
+    initConstant(env, c, "PROT_READ", PROT_READ);
+    initConstant(env, c, "PROT_WRITE", PROT_WRITE);
+    initConstant(env, c, "R_OK", R_OK);
+    initConstant(env, c, "RLIMIT_NOFILE", RLIMIT_NOFILE);
+// NOTE: The RT_* constants are not preprocessor defines, they're enum
+// members. The best we can do (barring UAPI / kernel version checks) is
+// to hope they exist on all host linuxes we're building on. These
+// constants have been around since 2.6.35 at least, so we should be ok.
+    initConstant(env, c, "RT_SCOPE_HOST", RT_SCOPE_HOST);
+    initConstant(env, c, "RT_SCOPE_LINK", RT_SCOPE_LINK);
+    initConstant(env, c, "RT_SCOPE_NOWHERE", RT_SCOPE_NOWHERE);
+    initConstant(env, c, "RT_SCOPE_SITE", RT_SCOPE_SITE);
+    initConstant(env, c, "RT_SCOPE_UNIVERSE", RT_SCOPE_UNIVERSE);
+    initConstant(env, c, "RTMGRP_IPV4_IFADDR", RTMGRP_IPV4_IFADDR);
+    initConstant(env, c, "RTMGRP_IPV4_MROUTE", RTMGRP_IPV4_MROUTE);
+    initConstant(env, c, "RTMGRP_IPV4_ROUTE", RTMGRP_IPV4_ROUTE);
+    initConstant(env, c, "RTMGRP_IPV4_RULE", RTMGRP_IPV4_RULE);
+    initConstant(env, c, "RTMGRP_IPV6_IFADDR", RTMGRP_IPV6_IFADDR);
+    initConstant(env, c, "RTMGRP_IPV6_IFINFO", RTMGRP_IPV6_IFINFO);
+    initConstant(env, c, "RTMGRP_IPV6_MROUTE", RTMGRP_IPV6_MROUTE);
+    initConstant(env, c, "RTMGRP_IPV6_PREFIX", RTMGRP_IPV6_PREFIX);
+    initConstant(env, c, "RTMGRP_IPV6_ROUTE", RTMGRP_IPV6_ROUTE);
+    initConstant(env, c, "RTMGRP_LINK", RTMGRP_LINK);
+    initConstant(env, c, "RTMGRP_NEIGH", RTMGRP_NEIGH);
+    initConstant(env, c, "RTMGRP_NOTIFY", RTMGRP_NOTIFY);
+    initConstant(env, c, "RTMGRP_TC", RTMGRP_TC);
+    initConstant(env, c, "SEEK_CUR", SEEK_CUR);
+    initConstant(env, c, "SEEK_END", SEEK_END);
+    initConstant(env, c, "SEEK_SET", SEEK_SET);
+    initConstant(env, c, "SHUT_RD", SHUT_RD);
+    initConstant(env, c, "SHUT_RDWR", SHUT_RDWR);
+    initConstant(env, c, "SHUT_WR", SHUT_WR);
+    initConstant(env, c, "SIGABRT", SIGABRT);
+    initConstant(env, c, "SIGALRM", SIGALRM);
+    initConstant(env, c, "SIGBUS", SIGBUS);
+    initConstant(env, c, "SIGCHLD", SIGCHLD);
+    initConstant(env, c, "SIGCONT", SIGCONT);
+    initConstant(env, c, "SIGFPE", SIGFPE);
+    initConstant(env, c, "SIGHUP", SIGHUP);
+    initConstant(env, c, "SIGILL", SIGILL);
+    initConstant(env, c, "SIGINT", SIGINT);
+    initConstant(env, c, "SIGIO", SIGIO);
+    initConstant(env, c, "SIGKILL", SIGKILL);
+    initConstant(env, c, "SIGPIPE", SIGPIPE);
+    initConstant(env, c, "SIGPROF", SIGPROF);
+#if defined(SIGPWR)
+    initConstant(env, c, "SIGPWR", SIGPWR);
+#endif
+    initConstant(env, c, "SIGQUIT", SIGQUIT);
+#if defined(SIGRTMAX)
+    initConstant(env, c, "SIGRTMAX", SIGRTMAX);
+#endif
+#if defined(SIGRTMIN)
+    initConstant(env, c, "SIGRTMIN", SIGRTMIN);
+#endif
+    initConstant(env, c, "SIGSEGV", SIGSEGV);
+#if defined(SIGSTKFLT)
+    initConstant(env, c, "SIGSTKFLT", SIGSTKFLT);
+#endif
+    initConstant(env, c, "SIGSTOP", SIGSTOP);
+    initConstant(env, c, "SIGSYS", SIGSYS);
+    initConstant(env, c, "SIGTERM", SIGTERM);
+    initConstant(env, c, "SIGTRAP", SIGTRAP);
+    initConstant(env, c, "SIGTSTP", SIGTSTP);
+    initConstant(env, c, "SIGTTIN", SIGTTIN);
+    initConstant(env, c, "SIGTTOU", SIGTTOU);
+    initConstant(env, c, "SIGURG", SIGURG);
+    initConstant(env, c, "SIGUSR1", SIGUSR1);
+    initConstant(env, c, "SIGUSR2", SIGUSR2);
+    initConstant(env, c, "SIGVTALRM", SIGVTALRM);
+    initConstant(env, c, "SIGWINCH", SIGWINCH);
+    initConstant(env, c, "SIGXCPU", SIGXCPU);
+    initConstant(env, c, "SIGXFSZ", SIGXFSZ);
+    initConstant(env, c, "SIOCGIFADDR", SIOCGIFADDR);
+    initConstant(env, c, "SIOCGIFBRDADDR", SIOCGIFBRDADDR);
+    initConstant(env, c, "SIOCGIFDSTADDR", SIOCGIFDSTADDR);
+    initConstant(env, c, "SIOCGIFNETMASK", SIOCGIFNETMASK);
+    initConstant(env, c, "SOCK_CLOEXEC", SOCK_CLOEXEC);
+    initConstant(env, c, "SOCK_DGRAM", SOCK_DGRAM);
+    initConstant(env, c, "SOCK_NONBLOCK", SOCK_NONBLOCK);
+    initConstant(env, c, "SOCK_RAW", SOCK_RAW);
+    initConstant(env, c, "SOCK_SEQPACKET", SOCK_SEQPACKET);
+    initConstant(env, c, "SOCK_STREAM", SOCK_STREAM);
+    initConstant(env, c, "SOL_SOCKET", SOL_SOCKET);
+#if defined(SOL_UDP)
+    initConstant(env, c, "SOL_UDP", SOL_UDP);
+#endif
+    initConstant(env, c, "SOL_PACKET", SOL_PACKET);
+#if defined(SO_BINDTODEVICE)
+    initConstant(env, c, "SO_BINDTODEVICE", SO_BINDTODEVICE);
+#endif
+    initConstant(env, c, "SO_BROADCAST", SO_BROADCAST);
+    initConstant(env, c, "SO_DEBUG", SO_DEBUG);
+#if defined(SO_DOMAIN)
+    initConstant(env, c, "SO_DOMAIN", SO_DOMAIN);
+#endif
+    initConstant(env, c, "SO_DONTROUTE", SO_DONTROUTE);
+    initConstant(env, c, "SO_ERROR", SO_ERROR);
+    initConstant(env, c, "SO_KEEPALIVE", SO_KEEPALIVE);
+    initConstant(env, c, "SO_LINGER", SO_LINGER);
+    initConstant(env, c, "SO_OOBINLINE", SO_OOBINLINE);
+#if defined(SO_PASSCRED)
+    initConstant(env, c, "SO_PASSCRED", SO_PASSCRED);
+#endif
+#if defined(SO_PEERCRED)
+    initConstant(env, c, "SO_PEERCRED", SO_PEERCRED);
+#endif
+#if defined(SO_PROTOCOL)
+    initConstant(env, c, "SO_PROTOCOL", SO_PROTOCOL);
+#endif
+    initConstant(env, c, "SO_RCVBUF", SO_RCVBUF);
+    initConstant(env, c, "SO_RCVLOWAT", SO_RCVLOWAT);
+    initConstant(env, c, "SO_RCVTIMEO", SO_RCVTIMEO);
+    initConstant(env, c, "SO_REUSEADDR", SO_REUSEADDR);
+    initConstant(env, c, "SO_SNDBUF", SO_SNDBUF);
+    initConstant(env, c, "SO_SNDLOWAT", SO_SNDLOWAT);
+    initConstant(env, c, "SO_SNDTIMEO", SO_SNDTIMEO);
+    initConstant(env, c, "SO_TYPE", SO_TYPE);
+#if defined(PACKET_IGNORE_OUTGOING)
+    initConstant(env, c, "PACKET_IGNORE_OUTGOING", PACKET_IGNORE_OUTGOING);
+#endif
+    initConstant(env, c, "SPLICE_F_MOVE", SPLICE_F_MOVE);
+    initConstant(env, c, "SPLICE_F_NONBLOCK", SPLICE_F_NONBLOCK);
+    initConstant(env, c, "SPLICE_F_MORE", SPLICE_F_MORE);
+    initConstant(env, c, "STDERR_FILENO", STDERR_FILENO);
+    initConstant(env, c, "STDIN_FILENO", STDIN_FILENO);
+    initConstant(env, c, "STDOUT_FILENO", STDOUT_FILENO);
+    initConstant(env, c, "ST_MANDLOCK", ST_MANDLOCK);
+    initConstant(env, c, "ST_NOATIME", ST_NOATIME);
+    initConstant(env, c, "ST_NODEV", ST_NODEV);
+    initConstant(env, c, "ST_NODIRATIME", ST_NODIRATIME);
+    initConstant(env, c, "ST_NOEXEC", ST_NOEXEC);
+    initConstant(env, c, "ST_NOSUID", ST_NOSUID);
+    initConstant(env, c, "ST_RDONLY", ST_RDONLY);
+    initConstant(env, c, "ST_RELATIME", ST_RELATIME);
+    initConstant(env, c, "ST_SYNCHRONOUS", ST_SYNCHRONOUS);
+    initConstant(env, c, "S_IFBLK", S_IFBLK);
+    initConstant(env, c, "S_IFCHR", S_IFCHR);
+    initConstant(env, c, "S_IFDIR", S_IFDIR);
+    initConstant(env, c, "S_IFIFO", S_IFIFO);
+    initConstant(env, c, "S_IFLNK", S_IFLNK);
+    initConstant(env, c, "S_IFMT", S_IFMT);
+    initConstant(env, c, "S_IFREG", S_IFREG);
+    initConstant(env, c, "S_IFSOCK", S_IFSOCK);
+    initConstant(env, c, "S_IRGRP", S_IRGRP);
+    initConstant(env, c, "S_IROTH", S_IROTH);
+    initConstant(env, c, "S_IRUSR", S_IRUSR);
+    initConstant(env, c, "S_IRWXG", S_IRWXG);
+    initConstant(env, c, "S_IRWXO", S_IRWXO);
+    initConstant(env, c, "S_IRWXU", S_IRWXU);
+    initConstant(env, c, "S_ISGID", S_ISGID);
+    initConstant(env, c, "S_ISUID", S_ISUID);
+    initConstant(env, c, "S_ISVTX", S_ISVTX);
+    initConstant(env, c, "S_IWGRP", S_IWGRP);
+    initConstant(env, c, "S_IWOTH", S_IWOTH);
+    initConstant(env, c, "S_IWUSR", S_IWUSR);
+    initConstant(env, c, "S_IXGRP", S_IXGRP);
+    initConstant(env, c, "S_IXOTH", S_IXOTH);
+    initConstant(env, c, "S_IXUSR", S_IXUSR);
+    initConstant(env, c, "TCP_NODELAY", TCP_NODELAY);
+#if defined(TCP_USER_TIMEOUT)
+    initConstant(env, c, "TCP_USER_TIMEOUT", TCP_USER_TIMEOUT);
+#endif
+    initConstant(env, c, "TIOCOUTQ", TIOCOUTQ);
+    initConstant(env, c, "UDP_ENCAP", UDP_ENCAP);
+    initConstant(env, c, "UDP_ENCAP_ESPINUDP_NON_IKE", UDP_ENCAP_ESPINUDP_NON_IKE);
+    initConstant(env, c, "UDP_ENCAP_ESPINUDP", UDP_ENCAP_ESPINUDP);
+#if defined(UDP_GRO)
+    initConstant(env, c, "UDP_GRO", UDP_GRO);
+#endif
+#if defined(UDP_SEGMENT)
+    initConstant(env, c, "UDP_SEGMENT", UDP_SEGMENT);
+#endif
+    // UNIX_PATH_MAX is mentioned in some versions of unix(7), but not actually declared.
+    initConstant(env, c, "UNIX_PATH_MAX", sizeof(sockaddr_un::sun_path));
+    initConstant(env, c, "WCONTINUED", WCONTINUED);
+    initConstant(env, c, "WEXITED", WEXITED);
+    initConstant(env, c, "WNOHANG", WNOHANG);
+    initConstant(env, c, "WNOWAIT", WNOWAIT);
+    initConstant(env, c, "WSTOPPED", WSTOPPED);
+    initConstant(env, c, "WUNTRACED", WUNTRACED);
+    initConstant(env, c, "W_OK", W_OK);
+    initConstant(env, c, "XATTR_CREATE", XATTR_CREATE);
+    initConstant(env, c, "XATTR_REPLACE", XATTR_REPLACE);
+    initConstant(env, c, "X_OK", X_OK);
+    initConstant(env, c, "_SC_2_CHAR_TERM", _SC_2_CHAR_TERM);
+    initConstant(env, c, "_SC_2_C_BIND", _SC_2_C_BIND);
+    initConstant(env, c, "_SC_2_C_DEV", _SC_2_C_DEV);
+#if defined(_SC_2_C_VERSION)
+    initConstant(env, c, "_SC_2_C_VERSION", _SC_2_C_VERSION);
+#endif
+    initConstant(env, c, "_SC_2_FORT_DEV", _SC_2_FORT_DEV);
+    initConstant(env, c, "_SC_2_FORT_RUN", _SC_2_FORT_RUN);
+    initConstant(env, c, "_SC_2_LOCALEDEF", _SC_2_LOCALEDEF);
+    initConstant(env, c, "_SC_2_SW_DEV", _SC_2_SW_DEV);
+    initConstant(env, c, "_SC_2_UPE", _SC_2_UPE);
+    initConstant(env, c, "_SC_2_VERSION", _SC_2_VERSION);
+    initConstant(env, c, "_SC_AIO_LISTIO_MAX", _SC_AIO_LISTIO_MAX);
+    initConstant(env, c, "_SC_AIO_MAX", _SC_AIO_MAX);
+    initConstant(env, c, "_SC_AIO_PRIO_DELTA_MAX", _SC_AIO_PRIO_DELTA_MAX);
+    initConstant(env, c, "_SC_ARG_MAX", _SC_ARG_MAX);
+    initConstant(env, c, "_SC_ASYNCHRONOUS_IO", _SC_ASYNCHRONOUS_IO);
+    initConstant(env, c, "_SC_ATEXIT_MAX", _SC_ATEXIT_MAX);
+#if defined(_SC_AVPHYS_PAGES)
+    initConstant(env, c, "_SC_AVPHYS_PAGES", _SC_AVPHYS_PAGES);
+#endif
+    initConstant(env, c, "_SC_BC_BASE_MAX", _SC_BC_BASE_MAX);
+    initConstant(env, c, "_SC_BC_DIM_MAX", _SC_BC_DIM_MAX);
+    initConstant(env, c, "_SC_BC_SCALE_MAX", _SC_BC_SCALE_MAX);
+    initConstant(env, c, "_SC_BC_STRING_MAX", _SC_BC_STRING_MAX);
+    initConstant(env, c, "_SC_CHILD_MAX", _SC_CHILD_MAX);
+    initConstant(env, c, "_SC_CLK_TCK", _SC_CLK_TCK);
+    initConstant(env, c, "_SC_COLL_WEIGHTS_MAX", _SC_COLL_WEIGHTS_MAX);
+    initConstant(env, c, "_SC_DELAYTIMER_MAX", _SC_DELAYTIMER_MAX);
+    initConstant(env, c, "_SC_EXPR_NEST_MAX", _SC_EXPR_NEST_MAX);
+    initConstant(env, c, "_SC_FSYNC", _SC_FSYNC);
+    initConstant(env, c, "_SC_GETGR_R_SIZE_MAX", _SC_GETGR_R_SIZE_MAX);
+    initConstant(env, c, "_SC_GETPW_R_SIZE_MAX", _SC_GETPW_R_SIZE_MAX);
+    initConstant(env, c, "_SC_IOV_MAX", _SC_IOV_MAX);
+    initConstant(env, c, "_SC_JOB_CONTROL", _SC_JOB_CONTROL);
+    initConstant(env, c, "_SC_LINE_MAX", _SC_LINE_MAX);
+    initConstant(env, c, "_SC_LOGIN_NAME_MAX", _SC_LOGIN_NAME_MAX);
+    initConstant(env, c, "_SC_MAPPED_FILES", _SC_MAPPED_FILES);
+    initConstant(env, c, "_SC_MEMLOCK", _SC_MEMLOCK);
+    initConstant(env, c, "_SC_MEMLOCK_RANGE", _SC_MEMLOCK_RANGE);
+    initConstant(env, c, "_SC_MEMORY_PROTECTION", _SC_MEMORY_PROTECTION);
+    initConstant(env, c, "_SC_MESSAGE_PASSING", _SC_MESSAGE_PASSING);
+    initConstant(env, c, "_SC_MQ_OPEN_MAX", _SC_MQ_OPEN_MAX);
+    initConstant(env, c, "_SC_MQ_PRIO_MAX", _SC_MQ_PRIO_MAX);
+    initConstant(env, c, "_SC_NGROUPS_MAX", _SC_NGROUPS_MAX);
+    initConstant(env, c, "_SC_NPROCESSORS_CONF", _SC_NPROCESSORS_CONF);
+    initConstant(env, c, "_SC_NPROCESSORS_ONLN", _SC_NPROCESSORS_ONLN);
+    initConstant(env, c, "_SC_OPEN_MAX", _SC_OPEN_MAX);
+    initConstant(env, c, "_SC_PAGESIZE", _SC_PAGESIZE);
+    initConstant(env, c, "_SC_PAGE_SIZE", _SC_PAGE_SIZE);
+    initConstant(env, c, "_SC_PASS_MAX", _SC_PASS_MAX);
+#if defined(_SC_PHYS_PAGES)
+    initConstant(env, c, "_SC_PHYS_PAGES", _SC_PHYS_PAGES);
+#endif
+    initConstant(env, c, "_SC_PRIORITIZED_IO", _SC_PRIORITIZED_IO);
+    initConstant(env, c, "_SC_PRIORITY_SCHEDULING", _SC_PRIORITY_SCHEDULING);
+    initConstant(env, c, "_SC_REALTIME_SIGNALS", _SC_REALTIME_SIGNALS);
+    initConstant(env, c, "_SC_RE_DUP_MAX", _SC_RE_DUP_MAX);
+    initConstant(env, c, "_SC_RTSIG_MAX", _SC_RTSIG_MAX);
+    initConstant(env, c, "_SC_SAVED_IDS", _SC_SAVED_IDS);
+    initConstant(env, c, "_SC_SEMAPHORES", _SC_SEMAPHORES);
+    initConstant(env, c, "_SC_SEM_NSEMS_MAX", _SC_SEM_NSEMS_MAX);
+    initConstant(env, c, "_SC_SEM_VALUE_MAX", _SC_SEM_VALUE_MAX);
+    initConstant(env, c, "_SC_SHARED_MEMORY_OBJECTS", _SC_SHARED_MEMORY_OBJECTS);
+    initConstant(env, c, "_SC_SIGQUEUE_MAX", _SC_SIGQUEUE_MAX);
+    initConstant(env, c, "_SC_STREAM_MAX", _SC_STREAM_MAX);
+    initConstant(env, c, "_SC_SYNCHRONIZED_IO", _SC_SYNCHRONIZED_IO);
+    initConstant(env, c, "_SC_THREADS", _SC_THREADS);
+    initConstant(env, c, "_SC_THREAD_ATTR_STACKADDR", _SC_THREAD_ATTR_STACKADDR);
+    initConstant(env, c, "_SC_THREAD_ATTR_STACKSIZE", _SC_THREAD_ATTR_STACKSIZE);
+    initConstant(env, c, "_SC_THREAD_DESTRUCTOR_ITERATIONS", _SC_THREAD_DESTRUCTOR_ITERATIONS);
+    initConstant(env, c, "_SC_THREAD_KEYS_MAX", _SC_THREAD_KEYS_MAX);
+    initConstant(env, c, "_SC_THREAD_PRIORITY_SCHEDULING", _SC_THREAD_PRIORITY_SCHEDULING);
+    initConstant(env, c, "_SC_THREAD_PRIO_INHERIT", _SC_THREAD_PRIO_INHERIT);
+    initConstant(env, c, "_SC_THREAD_PRIO_PROTECT", _SC_THREAD_PRIO_PROTECT);
+    initConstant(env, c, "_SC_THREAD_SAFE_FUNCTIONS", _SC_THREAD_SAFE_FUNCTIONS);
+    initConstant(env, c, "_SC_THREAD_STACK_MIN", _SC_THREAD_STACK_MIN);
+    initConstant(env, c, "_SC_THREAD_THREADS_MAX", _SC_THREAD_THREADS_MAX);
+    initConstant(env, c, "_SC_TIMERS", _SC_TIMERS);
+    initConstant(env, c, "_SC_TIMER_MAX", _SC_TIMER_MAX);
+    initConstant(env, c, "_SC_TTY_NAME_MAX", _SC_TTY_NAME_MAX);
+    initConstant(env, c, "_SC_TZNAME_MAX", _SC_TZNAME_MAX);
+    initConstant(env, c, "_SC_VERSION", _SC_VERSION);
+    initConstant(env, c, "_SC_XBS5_ILP32_OFF32", _SC_XBS5_ILP32_OFF32);
+    initConstant(env, c, "_SC_XBS5_ILP32_OFFBIG", _SC_XBS5_ILP32_OFFBIG);
+    initConstant(env, c, "_SC_XBS5_LP64_OFF64", _SC_XBS5_LP64_OFF64);
+    initConstant(env, c, "_SC_XBS5_LPBIG_OFFBIG", _SC_XBS5_LPBIG_OFFBIG);
+    initConstant(env, c, "_SC_XOPEN_CRYPT", _SC_XOPEN_CRYPT);
+    initConstant(env, c, "_SC_XOPEN_ENH_I18N", _SC_XOPEN_ENH_I18N);
+    initConstant(env, c, "_SC_XOPEN_LEGACY", _SC_XOPEN_LEGACY);
+    initConstant(env, c, "_SC_XOPEN_REALTIME", _SC_XOPEN_REALTIME);
+    initConstant(env, c, "_SC_XOPEN_REALTIME_THREADS", _SC_XOPEN_REALTIME_THREADS);
+    initConstant(env, c, "_SC_XOPEN_SHM", _SC_XOPEN_SHM);
+    initConstant(env, c, "_SC_XOPEN_UNIX", _SC_XOPEN_UNIX);
+    initConstant(env, c, "_SC_XOPEN_VERSION", _SC_XOPEN_VERSION);
+    initConstant(env, c, "_SC_XOPEN_XCU_VERSION", _SC_XOPEN_XCU_VERSION);
+}
+
+static JNINativeMethod gMethods[] = {
+    NATIVE_METHOD(OsConstants, initConstants, "()V"),
+};
+
+void register_android_system_OsConstants(JNIEnv* env) {
+    // [ravenwood-change] -- method moved to the nested class
+    jniRegisterNativeMethods(env, "android/system/OsConstants$Native", gMethods, NELEM(gMethods));
+}
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
new file mode 100644
index 0000000..34cf9f9
--- /dev/null
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <string.h>
+#include <unistd.h>
+#include <nativehelper/JNIHelp.h>
+#include "jni.h"
+#include "utils/Log.h"
+#include "utils/misc.h"
+
+// Defined in ravenwood_os_constants.cpp
+void register_android_system_OsConstants(JNIEnv* env);
+
+// ---- Exception related ----
+
+static void throwErrnoException(JNIEnv* env, const char* functionName) {
+    int error = errno;
+    jniThrowErrnoException(env, functionName, error);
+}
+
+template <typename rc_t>
+static rc_t throwIfMinusOne(JNIEnv* env, const char* name, rc_t rc) {
+    if (rc == rc_t(-1)) {
+        throwErrnoException(env, name);
+    }
+    return rc;
+}
+
+// ---- JNI methods ----
+
+typedef void (*FreeFunction)(void*);
+
+static void nApplyFreeFunction(JNIEnv*, jclass, jlong freeFunction, jlong ptr) {
+    void* nativePtr = reinterpret_cast<void*>(static_cast<uintptr_t>(ptr));
+    FreeFunction nativeFreeFunction
+        = reinterpret_cast<FreeFunction>(static_cast<uintptr_t>(freeFunction));
+    nativeFreeFunction(nativePtr);
+}
+
+static jint nFcntlInt(JNIEnv* env, jclass, jint fd, jint cmd, jint arg) {
+    return throwIfMinusOne(env, "fcntl", TEMP_FAILURE_RETRY(fcntl(fd, cmd, arg)));
+}
+
+static jlong nLseek(JNIEnv* env, jclass, jint fd, jlong offset, jint whence) {
+    return throwIfMinusOne(env, "lseek", TEMP_FAILURE_RETRY(lseek(fd, offset, whence)));
+}
+
+static jintArray nPipe2(JNIEnv* env, jclass, jint flags) {
+    int fds[2];
+    throwIfMinusOne(env, "pipe2", TEMP_FAILURE_RETRY(pipe2(fds, flags)));
+
+    jintArray result;
+    result = env->NewIntArray(2);
+    if (result == NULL) {
+        return NULL; /* out of memory error thrown */
+    }
+    env->SetIntArrayRegion(result, 0, 2, fds);
+    return result;
+}
+
+static jlong nDup(JNIEnv* env, jclass, jint fd) {
+    return throwIfMinusOne(env, "fcntl", TEMP_FAILURE_RETRY(fcntl(fd, F_DUPFD_CLOEXEC, 0)));
+}
+
+// ---- Registration ----
+
+static const JNINativeMethod sMethods[] =
+{
+    { "applyFreeFunction", "(JJ)V", (void*)nApplyFreeFunction },
+    { "nFcntlInt", "(III)I", (void*)nFcntlInt },
+    { "nLseek", "(IJI)J", (void*)nLseek },
+    { "nPipe2", "(I)[I", (void*)nPipe2 },
+    { "nDup", "(I)I", (void*)nDup },
+};
+
+extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
+{
+    JNIEnv* env = NULL;
+    jint result = -1;
+
+    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
+        ALOGE("GetEnv failed!");
+        return result;
+    }
+    ALOG_ASSERT(env, "Could not retrieve the env!");
+
+    ALOGI("%s: JNI_OnLoad", __FILE__);
+
+    jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/common/RavenwoodRuntimeNative",
+            sMethods, NELEM(sMethods));
+    if (res < 0) {
+        return res;
+    }
+
+    register_android_system_OsConstants(env);
+
+    return JNI_VERSION_1_4;
+}
diff --git a/ravenwood/runtime-test/Android.bp b/ravenwood/runtime-test/Android.bp
new file mode 100644
index 0000000..4102920
--- /dev/null
+++ b/ravenwood/runtime-test/Android.bp
@@ -0,0 +1,23 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_ravenwood_test {
+    name: "RavenwoodRuntimeTest",
+
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+    ],
+    srcs: [
+        "test/**/*.java",
+    ],
+    // sdk_version: "module_current",
+    auto_gen_config: true,
+}
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java b/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java
new file mode 100644
index 0000000..3332e24
--- /dev/null
+++ b/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.runtimetest;
+
+// Copied from libcore/luni/src/test/java/libcore/android/system/OsConstantsTest.java
+
+import static android.system.OsConstants.CAP_TO_INDEX;
+import static android.system.OsConstants.CAP_TO_MASK;
+import static android.system.OsConstants.S_ISBLK;
+import static android.system.OsConstants.S_ISCHR;
+import static android.system.OsConstants.S_ISDIR;
+import static android.system.OsConstants.S_ISFIFO;
+import static android.system.OsConstants.S_ISLNK;
+import static android.system.OsConstants.S_ISREG;
+import static android.system.OsConstants.S_ISSOCK;
+import static android.system.OsConstants.WCOREDUMP;
+import static android.system.OsConstants.WEXITSTATUS;
+import static android.system.OsConstants.WIFEXITED;
+import static android.system.OsConstants.WIFSIGNALED;
+import static android.system.OsConstants.WIFSTOPPED;
+import static android.system.OsConstants.WSTOPSIG;
+import static android.system.OsConstants.WTERMSIG;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.system.OsConstants;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class OsConstantsTest {
+
+    // http://b/15602893
+    @Test
+    public void testBug15602893() {
+        assertTrue(OsConstants.RT_SCOPE_HOST > 0);
+        assertTrue(OsConstants.RT_SCOPE_LINK > 0);
+        assertTrue(OsConstants.RT_SCOPE_SITE > 0);
+
+        assertTrue(OsConstants.IFA_F_TENTATIVE > 0);
+    }
+
+    // introduced for http://b/30402085
+    @Test
+    public void testTcpUserTimeoutIsDefined() {
+        assertTrue(OsConstants.TCP_USER_TIMEOUT > 0);
+    }
+
+    /**
+     * Verifies equality assertions given in the documentation for
+     * {@link OsConstants#SOCK_CLOEXEC} and {@link OsConstants#SOCK_NONBLOCK}.
+     */
+    @Test
+    public void testConstantsEqual() {
+        assertEquals(OsConstants.O_CLOEXEC,  OsConstants.SOCK_CLOEXEC);
+        assertEquals(OsConstants.O_NONBLOCK, OsConstants.SOCK_NONBLOCK);
+    }
+
+    @Test
+    public void test_CAP_constants() {
+        assertEquals(0,  OsConstants.CAP_CHOWN);
+        assertEquals(1,  OsConstants.CAP_DAC_OVERRIDE);
+        assertEquals(2,  OsConstants.CAP_DAC_READ_SEARCH);
+        assertEquals(3,  OsConstants.CAP_FOWNER);
+        assertEquals(4,  OsConstants.CAP_FSETID);
+        assertEquals(5,  OsConstants.CAP_KILL);
+        assertEquals(6,  OsConstants.CAP_SETGID);
+        assertEquals(7,  OsConstants.CAP_SETUID);
+        assertEquals(8,  OsConstants.CAP_SETPCAP);
+        assertEquals(9,  OsConstants.CAP_LINUX_IMMUTABLE);
+        assertEquals(10, OsConstants.CAP_NET_BIND_SERVICE);
+        assertEquals(11, OsConstants.CAP_NET_BROADCAST);
+        assertEquals(12, OsConstants.CAP_NET_ADMIN);
+        assertEquals(13, OsConstants.CAP_NET_RAW);
+        assertEquals(14, OsConstants.CAP_IPC_LOCK);
+        assertEquals(15, OsConstants.CAP_IPC_OWNER);
+        assertEquals(16, OsConstants.CAP_SYS_MODULE);
+        assertEquals(17, OsConstants.CAP_SYS_RAWIO);
+        assertEquals(18, OsConstants.CAP_SYS_CHROOT);
+        assertEquals(19, OsConstants.CAP_SYS_PTRACE);
+        assertEquals(20, OsConstants.CAP_SYS_PACCT);
+        assertEquals(21, OsConstants.CAP_SYS_ADMIN);
+        assertEquals(22, OsConstants.CAP_SYS_BOOT);
+        assertEquals(23, OsConstants.CAP_SYS_NICE);
+        assertEquals(24, OsConstants.CAP_SYS_RESOURCE);
+        assertEquals(25, OsConstants.CAP_SYS_TIME);
+        assertEquals(26, OsConstants.CAP_SYS_TTY_CONFIG);
+        assertEquals(27, OsConstants.CAP_MKNOD);
+        assertEquals(28, OsConstants.CAP_LEASE);
+        assertEquals(29, OsConstants.CAP_AUDIT_WRITE);
+        assertEquals(30, OsConstants.CAP_AUDIT_CONTROL);
+        assertEquals(31, OsConstants.CAP_SETFCAP);
+        assertEquals(32, OsConstants.CAP_MAC_OVERRIDE);
+        assertEquals(33, OsConstants.CAP_MAC_ADMIN);
+        assertEquals(34, OsConstants.CAP_SYSLOG);
+        assertEquals(35, OsConstants.CAP_WAKE_ALARM);
+        assertEquals(36, OsConstants.CAP_BLOCK_SUSPEND);
+        // last constant
+        assertEquals(40, OsConstants.CAP_LAST_CAP);
+    }
+
+    @Test
+    public void test_CAP_TO_INDEX() {
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_CHOWN));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_DAC_OVERRIDE));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_DAC_READ_SEARCH));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_FOWNER));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_FSETID));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_KILL));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SETGID));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SETUID));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SETPCAP));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_LINUX_IMMUTABLE));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_NET_BIND_SERVICE));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_NET_BROADCAST));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_NET_ADMIN));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_NET_RAW));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_IPC_LOCK));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_IPC_OWNER));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SYS_MODULE));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SYS_RAWIO));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SYS_CHROOT));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SYS_PTRACE));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SYS_PACCT));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SYS_ADMIN));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SYS_BOOT));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SYS_NICE));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SYS_RESOURCE));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SYS_TIME));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SYS_TTY_CONFIG));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_MKNOD));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_LEASE));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_AUDIT_WRITE));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_AUDIT_CONTROL));
+        assertEquals(0, CAP_TO_INDEX(OsConstants.CAP_SETFCAP));
+        assertEquals(1, CAP_TO_INDEX(OsConstants.CAP_MAC_OVERRIDE));
+        assertEquals(1, CAP_TO_INDEX(OsConstants.CAP_MAC_ADMIN));
+        assertEquals(1, CAP_TO_INDEX(OsConstants.CAP_SYSLOG));
+        assertEquals(1, CAP_TO_INDEX(OsConstants.CAP_WAKE_ALARM));
+        assertEquals(1, CAP_TO_INDEX(OsConstants.CAP_BLOCK_SUSPEND));
+    }
+
+    @Test
+    public void test_CAP_TO_MASK() {
+        assertEquals(1 << 0,  CAP_TO_MASK(OsConstants.CAP_CHOWN));
+        assertEquals(1 << 1,  CAP_TO_MASK(OsConstants.CAP_DAC_OVERRIDE));
+        assertEquals(1 << 2,  CAP_TO_MASK(OsConstants.CAP_DAC_READ_SEARCH));
+        assertEquals(1 << 3,  CAP_TO_MASK(OsConstants.CAP_FOWNER));
+        assertEquals(1 << 4,  CAP_TO_MASK(OsConstants.CAP_FSETID));
+        assertEquals(1 << 5,  CAP_TO_MASK(OsConstants.CAP_KILL));
+        assertEquals(1 << 6,  CAP_TO_MASK(OsConstants.CAP_SETGID));
+        assertEquals(1 << 7,  CAP_TO_MASK(OsConstants.CAP_SETUID));
+        assertEquals(1 << 8,  CAP_TO_MASK(OsConstants.CAP_SETPCAP));
+        assertEquals(1 << 9,  CAP_TO_MASK(OsConstants.CAP_LINUX_IMMUTABLE));
+        assertEquals(1 << 10, CAP_TO_MASK(OsConstants.CAP_NET_BIND_SERVICE));
+        assertEquals(1 << 11, CAP_TO_MASK(OsConstants.CAP_NET_BROADCAST));
+        assertEquals(1 << 12, CAP_TO_MASK(OsConstants.CAP_NET_ADMIN));
+        assertEquals(1 << 13, CAP_TO_MASK(OsConstants.CAP_NET_RAW));
+        assertEquals(1 << 14, CAP_TO_MASK(OsConstants.CAP_IPC_LOCK));
+        assertEquals(1 << 15, CAP_TO_MASK(OsConstants.CAP_IPC_OWNER));
+        assertEquals(1 << 16, CAP_TO_MASK(OsConstants.CAP_SYS_MODULE));
+        assertEquals(1 << 17, CAP_TO_MASK(OsConstants.CAP_SYS_RAWIO));
+        assertEquals(1 << 18, CAP_TO_MASK(OsConstants.CAP_SYS_CHROOT));
+        assertEquals(1 << 19, CAP_TO_MASK(OsConstants.CAP_SYS_PTRACE));
+        assertEquals(1 << 20, CAP_TO_MASK(OsConstants.CAP_SYS_PACCT));
+        assertEquals(1 << 21, CAP_TO_MASK(OsConstants.CAP_SYS_ADMIN));
+        assertEquals(1 << 22, CAP_TO_MASK(OsConstants.CAP_SYS_BOOT));
+        assertEquals(1 << 23, CAP_TO_MASK(OsConstants.CAP_SYS_NICE));
+        assertEquals(1 << 24, CAP_TO_MASK(OsConstants.CAP_SYS_RESOURCE));
+        assertEquals(1 << 25, CAP_TO_MASK(OsConstants.CAP_SYS_TIME));
+        assertEquals(1 << 26, CAP_TO_MASK(OsConstants.CAP_SYS_TTY_CONFIG));
+        assertEquals(1 << 27, CAP_TO_MASK(OsConstants.CAP_MKNOD));
+        assertEquals(1 << 28, CAP_TO_MASK(OsConstants.CAP_LEASE));
+        assertEquals(1 << 29, CAP_TO_MASK(OsConstants.CAP_AUDIT_WRITE));
+        assertEquals(1 << 30, CAP_TO_MASK(OsConstants.CAP_AUDIT_CONTROL));
+        assertEquals(1 << 31, CAP_TO_MASK(OsConstants.CAP_SETFCAP));
+        assertEquals(1 << 0,  CAP_TO_MASK(OsConstants.CAP_MAC_OVERRIDE));
+        assertEquals(1 << 1,  CAP_TO_MASK(OsConstants.CAP_MAC_ADMIN));
+        assertEquals(1 << 2,  CAP_TO_MASK(OsConstants.CAP_SYSLOG));
+        assertEquals(1 << 3,  CAP_TO_MASK(OsConstants.CAP_WAKE_ALARM));
+        assertEquals(1 << 4,  CAP_TO_MASK(OsConstants.CAP_BLOCK_SUSPEND));
+    }
+
+    @Test
+    public void test_S_ISLNK() {
+        assertTrue(S_ISLNK(OsConstants.S_IFLNK));
+
+        assertFalse(S_ISLNK(OsConstants.S_IFBLK));
+        assertFalse(S_ISLNK(OsConstants.S_IFCHR));
+        assertFalse(S_ISLNK(OsConstants.S_IFDIR));
+        assertFalse(S_ISLNK(OsConstants.S_IFIFO));
+        assertFalse(S_ISLNK(OsConstants.S_IFMT));
+        assertFalse(S_ISLNK(OsConstants.S_IFREG));
+        assertFalse(S_ISLNK(OsConstants.S_IFSOCK));
+        assertFalse(S_ISLNK(OsConstants.S_IRGRP));
+        assertFalse(S_ISLNK(OsConstants.S_IROTH));
+        assertFalse(S_ISLNK(OsConstants.S_IRUSR));
+        assertFalse(S_ISLNK(OsConstants.S_IRWXG));
+        assertFalse(S_ISLNK(OsConstants.S_IRWXO));
+        assertFalse(S_ISLNK(OsConstants.S_IRWXU));
+        assertFalse(S_ISLNK(OsConstants.S_ISGID));
+        assertFalse(S_ISLNK(OsConstants.S_ISUID));
+        assertFalse(S_ISLNK(OsConstants.S_ISVTX));
+        assertFalse(S_ISLNK(OsConstants.S_IWGRP));
+        assertFalse(S_ISLNK(OsConstants.S_IWOTH));
+        assertFalse(S_ISLNK(OsConstants.S_IWUSR));
+        assertFalse(S_ISLNK(OsConstants.S_IXGRP));
+        assertFalse(S_ISLNK(OsConstants.S_IXOTH));
+        assertFalse(S_ISLNK(OsConstants.S_IXUSR));
+    }
+
+    @Test
+    public void test_S_ISREG() {
+        assertTrue(S_ISREG(OsConstants.S_IFREG));
+
+        assertFalse(S_ISREG(OsConstants.S_IFBLK));
+        assertFalse(S_ISREG(OsConstants.S_IFCHR));
+        assertFalse(S_ISREG(OsConstants.S_IFDIR));
+        assertFalse(S_ISREG(OsConstants.S_IFIFO));
+        assertFalse(S_ISREG(OsConstants.S_IFLNK));
+        assertFalse(S_ISREG(OsConstants.S_IFMT));
+        assertFalse(S_ISREG(OsConstants.S_IFSOCK));
+        assertFalse(S_ISREG(OsConstants.S_IRGRP));
+        assertFalse(S_ISREG(OsConstants.S_IROTH));
+        assertFalse(S_ISREG(OsConstants.S_IRUSR));
+        assertFalse(S_ISREG(OsConstants.S_IRWXG));
+        assertFalse(S_ISREG(OsConstants.S_IRWXO));
+        assertFalse(S_ISREG(OsConstants.S_IRWXU));
+        assertFalse(S_ISREG(OsConstants.S_ISGID));
+        assertFalse(S_ISREG(OsConstants.S_ISUID));
+        assertFalse(S_ISREG(OsConstants.S_ISVTX));
+        assertFalse(S_ISREG(OsConstants.S_IWGRP));
+        assertFalse(S_ISREG(OsConstants.S_IWOTH));
+        assertFalse(S_ISREG(OsConstants.S_IWUSR));
+        assertFalse(S_ISREG(OsConstants.S_IXGRP));
+        assertFalse(S_ISREG(OsConstants.S_IXOTH));
+        assertFalse(S_ISREG(OsConstants.S_IXUSR));
+    }
+
+    @Test
+    public void test_S_ISDIR() {
+        assertTrue(S_ISDIR(OsConstants.S_IFDIR));
+
+        assertFalse(S_ISDIR(OsConstants.S_IFBLK));
+        assertFalse(S_ISDIR(OsConstants.S_IFCHR));
+        assertFalse(S_ISDIR(OsConstants.S_IFIFO));
+        assertFalse(S_ISDIR(OsConstants.S_IFLNK));
+        assertFalse(S_ISDIR(OsConstants.S_IFMT));
+        assertFalse(S_ISDIR(OsConstants.S_IFREG));
+        assertFalse(S_ISDIR(OsConstants.S_IFSOCK));
+        assertFalse(S_ISDIR(OsConstants.S_IRGRP));
+        assertFalse(S_ISDIR(OsConstants.S_IROTH));
+        assertFalse(S_ISDIR(OsConstants.S_IRUSR));
+        assertFalse(S_ISDIR(OsConstants.S_IRWXG));
+        assertFalse(S_ISDIR(OsConstants.S_IRWXO));
+        assertFalse(S_ISDIR(OsConstants.S_IRWXU));
+        assertFalse(S_ISDIR(OsConstants.S_ISGID));
+        assertFalse(S_ISDIR(OsConstants.S_ISUID));
+        assertFalse(S_ISDIR(OsConstants.S_ISVTX));
+        assertFalse(S_ISDIR(OsConstants.S_IWGRP));
+        assertFalse(S_ISDIR(OsConstants.S_IWOTH));
+        assertFalse(S_ISDIR(OsConstants.S_IWUSR));
+        assertFalse(S_ISDIR(OsConstants.S_IXGRP));
+        assertFalse(S_ISDIR(OsConstants.S_IXOTH));
+        assertFalse(S_ISDIR(OsConstants.S_IXUSR));
+    }
+
+    @Test
+    public void test_S_ISCHR() {
+        assertTrue(S_ISCHR(OsConstants.S_IFCHR));
+
+        assertFalse(S_ISCHR(OsConstants.S_IFBLK));
+        assertFalse(S_ISCHR(OsConstants.S_IFDIR));
+        assertFalse(S_ISCHR(OsConstants.S_IFIFO));
+        assertFalse(S_ISCHR(OsConstants.S_IFLNK));
+        assertFalse(S_ISCHR(OsConstants.S_IFMT));
+        assertFalse(S_ISCHR(OsConstants.S_IFREG));
+        assertFalse(S_ISCHR(OsConstants.S_IFSOCK));
+        assertFalse(S_ISCHR(OsConstants.S_IRGRP));
+        assertFalse(S_ISCHR(OsConstants.S_IROTH));
+        assertFalse(S_ISCHR(OsConstants.S_IRUSR));
+        assertFalse(S_ISCHR(OsConstants.S_IRWXG));
+        assertFalse(S_ISCHR(OsConstants.S_IRWXO));
+        assertFalse(S_ISCHR(OsConstants.S_IRWXU));
+        assertFalse(S_ISCHR(OsConstants.S_ISGID));
+        assertFalse(S_ISCHR(OsConstants.S_ISUID));
+        assertFalse(S_ISCHR(OsConstants.S_ISVTX));
+        assertFalse(S_ISCHR(OsConstants.S_IWGRP));
+        assertFalse(S_ISCHR(OsConstants.S_IWOTH));
+        assertFalse(S_ISCHR(OsConstants.S_IWUSR));
+        assertFalse(S_ISCHR(OsConstants.S_IXGRP));
+        assertFalse(S_ISCHR(OsConstants.S_IXOTH));
+        assertFalse(S_ISCHR(OsConstants.S_IXUSR));
+    }
+
+    @Test
+    public void test_S_ISBLK() {
+        assertTrue (S_ISBLK(OsConstants.S_IFBLK));
+
+        assertFalse(S_ISBLK(OsConstants.S_IFCHR));
+        assertFalse(S_ISBLK(OsConstants.S_IFDIR));
+        assertFalse(S_ISBLK(OsConstants.S_IFIFO));
+        assertFalse(S_ISBLK(OsConstants.S_IFLNK));
+        assertFalse(S_ISBLK(OsConstants.S_IFMT));
+        assertFalse(S_ISBLK(OsConstants.S_IFREG));
+        assertFalse(S_ISBLK(OsConstants.S_IFSOCK));
+        assertFalse(S_ISBLK(OsConstants.S_IRGRP));
+        assertFalse(S_ISBLK(OsConstants.S_IROTH));
+        assertFalse(S_ISBLK(OsConstants.S_IRUSR));
+        assertFalse(S_ISBLK(OsConstants.S_IRWXG));
+        assertFalse(S_ISBLK(OsConstants.S_IRWXO));
+        assertFalse(S_ISBLK(OsConstants.S_IRWXU));
+        assertFalse(S_ISBLK(OsConstants.S_ISGID));
+        assertFalse(S_ISBLK(OsConstants.S_ISUID));
+        assertFalse(S_ISBLK(OsConstants.S_ISVTX));
+        assertFalse(S_ISBLK(OsConstants.S_IWGRP));
+        assertFalse(S_ISBLK(OsConstants.S_IWOTH));
+        assertFalse(S_ISBLK(OsConstants.S_IWUSR));
+        assertFalse(S_ISBLK(OsConstants.S_IXGRP));
+        assertFalse(S_ISBLK(OsConstants.S_IXOTH));
+        assertFalse(S_ISBLK(OsConstants.S_IXUSR));
+    }
+
+    @Test
+    public void test_S_ISFIFO() {
+        assertTrue(S_ISFIFO(OsConstants.S_IFIFO));
+
+        assertFalse(S_ISFIFO(OsConstants.S_IFBLK));
+        assertFalse(S_ISFIFO(OsConstants.S_IFCHR));
+        assertFalse(S_ISFIFO(OsConstants.S_IFDIR));
+        assertFalse(S_ISFIFO(OsConstants.S_IFLNK));
+        assertFalse(S_ISFIFO(OsConstants.S_IFMT));
+        assertFalse(S_ISFIFO(OsConstants.S_IFREG));
+        assertFalse(S_ISFIFO(OsConstants.S_IFSOCK));
+        assertFalse(S_ISFIFO(OsConstants.S_IRGRP));
+        assertFalse(S_ISFIFO(OsConstants.S_IROTH));
+        assertFalse(S_ISFIFO(OsConstants.S_IRUSR));
+        assertFalse(S_ISFIFO(OsConstants.S_IRWXG));
+        assertFalse(S_ISFIFO(OsConstants.S_IRWXO));
+        assertFalse(S_ISFIFO(OsConstants.S_IRWXU));
+        assertFalse(S_ISFIFO(OsConstants.S_ISGID));
+        assertFalse(S_ISFIFO(OsConstants.S_ISUID));
+        assertFalse(S_ISFIFO(OsConstants.S_ISVTX));
+        assertFalse(S_ISFIFO(OsConstants.S_IWGRP));
+        assertFalse(S_ISFIFO(OsConstants.S_IWOTH));
+        assertFalse(S_ISFIFO(OsConstants.S_IWUSR));
+        assertFalse(S_ISFIFO(OsConstants.S_IXGRP));
+        assertFalse(S_ISFIFO(OsConstants.S_IXOTH));
+        assertFalse(S_ISFIFO(OsConstants.S_IXUSR));
+    }
+
+    @Test
+    public void test_S_ISSOCK() {
+        assertTrue(S_ISSOCK(OsConstants.S_IFSOCK));
+
+        assertFalse(S_ISSOCK(OsConstants.S_IFBLK));
+        assertFalse(S_ISSOCK(OsConstants.S_IFCHR));
+        assertFalse(S_ISSOCK(OsConstants.S_IFDIR));
+        assertFalse(S_ISSOCK(OsConstants.S_IFIFO));
+        assertFalse(S_ISSOCK(OsConstants.S_IFLNK));
+        assertFalse(S_ISSOCK(OsConstants.S_IFMT));
+        assertFalse(S_ISSOCK(OsConstants.S_IFREG));
+        assertFalse(S_ISSOCK(OsConstants.S_IRGRP));
+        assertFalse(S_ISSOCK(OsConstants.S_IROTH));
+        assertFalse(S_ISSOCK(OsConstants.S_IRUSR));
+        assertFalse(S_ISSOCK(OsConstants.S_IRWXG));
+        assertFalse(S_ISSOCK(OsConstants.S_IRWXO));
+        assertFalse(S_ISSOCK(OsConstants.S_IRWXU));
+        assertFalse(S_ISSOCK(OsConstants.S_ISGID));
+        assertFalse(S_ISSOCK(OsConstants.S_ISUID));
+        assertFalse(S_ISSOCK(OsConstants.S_ISVTX));
+        assertFalse(S_ISSOCK(OsConstants.S_IWGRP));
+        assertFalse(S_ISSOCK(OsConstants.S_IWOTH));
+        assertFalse(S_ISSOCK(OsConstants.S_IWUSR));
+        assertFalse(S_ISSOCK(OsConstants.S_IXGRP));
+        assertFalse(S_ISSOCK(OsConstants.S_IXOTH));
+        assertFalse(S_ISSOCK(OsConstants.S_IXUSR));
+    }
+
+    @Test
+    public void test_WEXITSTATUS() {
+        assertEquals(0, WEXITSTATUS(0x0000));
+        assertEquals(0, WEXITSTATUS(0x00DE));
+        assertEquals(0xF0, WEXITSTATUS(0xF000));
+        assertEquals(0xAB, WEXITSTATUS(0xAB12));
+    }
+
+    @Test
+    public void test_WCOREDUMP() {
+        assertFalse(WCOREDUMP(0));
+        assertTrue(WCOREDUMP(0x80));
+    }
+
+    @Test
+    public void test_WTERMSIG() {
+        assertEquals(0, WTERMSIG(0));
+        assertEquals(0x7f, WTERMSIG(0x7f));
+    }
+
+    @Test
+    public void test_WSTOPSIG() {
+        assertEquals(0, WSTOPSIG(0x0000));
+        assertEquals(0, WSTOPSIG(0x00DE));
+        assertEquals(0xF0, WSTOPSIG(0xF000));
+        assertEquals(0xAB, WSTOPSIG(0xAB12));
+    }
+
+
+    @Test
+    public void test_WIFEXITED() {
+        assertTrue(WIFEXITED(0));
+        assertFalse(WIFEXITED(0x7f));
+    }
+
+    @Test
+    public void test_WIFSTOPPED() {
+        assertFalse(WIFSTOPPED(0));
+        assertTrue(WIFSTOPPED(0x7f));
+    }
+
+    @Test
+    public void test_WIFSIGNALED() {
+        assertFalse(WIFSIGNALED(0));
+        assertTrue(WIFSIGNALED(1));
+    }
+}
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java b/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
new file mode 100644
index 0000000..b5038e6
--- /dev/null
+++ b/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.runtimetest;
+
+import static org.junit.Assert.assertEquals;
+
+import android.system.Os;
+import android.system.OsConstants;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.ravenwood.common.JvmWorkaround;
+import com.android.ravenwood.common.RavenwoodRuntimeNative;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+@RunWith(AndroidJUnit4.class)
+public class OsTest {
+    public interface ConsumerWithThrow<T> {
+        void accept(T var1) throws Exception;
+    }
+
+    private void withTestFile(ConsumerWithThrow<FileDescriptor> consumer) throws Exception {
+        File file = File.createTempFile("osTest", "bin");
+        try (var raf = new RandomAccessFile(file, "rw")) {
+            var fd = raf.getFD();
+
+            try (var os = new FileOutputStream(fd)) {
+                os.write(1);
+                os.write(2);
+                os.write(3);
+                os.write(4);
+
+                consumer.accept(fd);
+            }
+        }
+    }
+
+    @Test
+    public void testLseek() throws Exception {
+        withTestFile((fd) -> {
+            assertEquals(4, Os.lseek(fd, 4, OsConstants.SEEK_SET));
+            assertEquals(4, Os.lseek(fd, 0, OsConstants.SEEK_CUR));
+            assertEquals(6, Os.lseek(fd, 2, OsConstants.SEEK_CUR));
+        });
+    }
+
+    @Test
+    public void testDup() throws Exception {
+        withTestFile((fd) -> {
+            var dup = Os.dup(fd);
+
+            checkAreDup(fd, dup);
+        });
+    }
+
+    @Test
+    public void testPipe2() throws Exception {
+        var fds = Os.pipe2(0);
+
+        write(fds[1], 123);
+        assertEquals(123, read(fds[0]));
+    }
+
+    @Test
+    public void testFcntlInt() throws Exception {
+        withTestFile((fd) -> {
+            var dupInt = Os.fcntlInt(fd, 0, 0);
+
+            var dup = new FileDescriptor();
+            JvmWorkaround.getInstance().setFdInt(dup, dupInt);
+
+            checkAreDup(fd, dup);
+        });
+    }
+
+    private static void write(FileDescriptor fd, int oneByte)  throws IOException {
+        // Create a dup to avoid closing the FD.
+        try (var dup = new FileOutputStream(RavenwoodRuntimeNative.dup(fd))) {
+            dup.write(oneByte);
+        }
+    }
+
+    private static int read(FileDescriptor fd) throws IOException {
+        // Create a dup to avoid closing the FD.
+        try (var dup = new FileInputStream(RavenwoodRuntimeNative.dup(fd))) {
+            return dup.read();
+        }
+    }
+
+    private static void checkAreDup(FileDescriptor fd1, FileDescriptor fd2) throws Exception {
+        assertEquals(4, Os.lseek(fd1, 4, OsConstants.SEEK_SET));
+        assertEquals(4, Os.lseek(fd1, 0, OsConstants.SEEK_CUR));
+
+        // Dup'ed FD shares the same position.
+        assertEquals(4, Os.lseek(fd2, 0, OsConstants.SEEK_CUR));
+
+        assertEquals(6, Os.lseek(fd1, 2, OsConstants.SEEK_CUR));
+        assertEquals(6, Os.lseek(fd2, 0, OsConstants.SEEK_CUR));
+    }
+}
diff --git a/services/autofill/java/com/android/server/autofill/SaveEventLogger.java b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java
index b7f12ad..c41a8cd 100644
--- a/services/autofill/java/com/android/server/autofill/SaveEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/SaveEventLogger.java
@@ -23,6 +23,7 @@
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NONE;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NO_SAVE_INFO;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_NO_VALUE_CHANGED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_SCREEN_HAS_CREDMAN_FIELD;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_SESSION_DESTROYED;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_UNKNOWN;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG;
@@ -112,6 +113,8 @@
       AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_SESSION_DESTROYED;
   public static final int NO_SAVE_REASON_WITH_DONT_SAVE_ON_FINISH_FLAG =
       AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_WITH_DONT_SAVE_ON_FINISH_FLAG;
+  public static final int NO_SAVE_REASON_SCREEN_HAS_CREDMAN_FIELD =
+      AUTOFILL_SAVE_EVENT_REPORTED__SAVE_UI_NOT_SHOWN_REASON__NO_SAVE_REASON_SCREEN_HAS_CREDMAN_FIELD;
 
   public static final long UNINITIATED_TIMESTAMP = Long.MIN_VALUE;
 
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index a8b1235..c46464b 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -89,6 +89,7 @@
 import static com.android.server.autofill.SaveEventLogger.NO_SAVE_REASON_NONE;
 import static com.android.server.autofill.SaveEventLogger.NO_SAVE_REASON_NO_SAVE_INFO;
 import static com.android.server.autofill.SaveEventLogger.NO_SAVE_REASON_NO_VALUE_CHANGED;
+import static com.android.server.autofill.SaveEventLogger.NO_SAVE_REASON_SCREEN_HAS_CREDMAN_FIELD;
 import static com.android.server.autofill.SaveEventLogger.NO_SAVE_REASON_SESSION_DESTROYED;
 import static com.android.server.autofill.SaveEventLogger.NO_SAVE_REASON_WITH_DELAY_SAVE_FLAG;
 import static com.android.server.autofill.SaveEventLogger.NO_SAVE_REASON_WITH_DONT_SAVE_ON_FINISH_FLAG;
@@ -3673,6 +3674,8 @@
                 Slog.v(TAG, "Call to Session#showSaveLocked() rejected - "
                         + "there is credman field in screen");
             }
+            mSaveEventLogger.maybeSetSaveUiNotShownReason(NO_SAVE_REASON_SCREEN_HAS_CREDMAN_FIELD);
+            mSaveEventLogger.logAndEndEvent();
             return new SaveResult(/* logSaveShown= */ false, /* removeSession= */ true,
                     Event.NO_SAVE_UI_REASON_NONE);
         }
diff --git a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
index b414b25..2d99c96 100644
--- a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
+++ b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java
@@ -270,7 +270,8 @@
                             PackageManagerInternal.class);
                     RestorePolicy restorePolicy = tarBackupReader.chooseRestorePolicy(
                             mBackupManagerService.getPackageManager(), allowApks, info, signatures,
-                            pmi, mUserId, mBackupEligibilityRules);
+                            pmi, mUserId, mBackupEligibilityRules,
+                            mBackupManagerService.getContext());
                     mManifestSignatures.put(info.packageName, signatures);
                     mPackagePolicies.put(pkg, restorePolicy);
                     mPackageInstallers.put(pkg, info.installerPackageName);
diff --git a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
index 78a9952..4860a27 100644
--- a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
+++ b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java
@@ -31,6 +31,7 @@
 import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_MISSING_SIGNATURE;
 import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_RESTORE_ANY_VERSION;
 import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_SYSTEM_APP_NO_AGENT;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_V_TO_U_RESTORE_PKG_ELIGIBLE;
 import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSIONS_MATCH;
 import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER;
 
@@ -53,17 +54,22 @@
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.Signature;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.provider.Settings;
 import android.util.Slog;
 
 import com.android.server.backup.FileMetadata;
+import com.android.server.backup.Flags;
 import com.android.server.backup.restore.RestorePolicy;
 
 import java.io.ByteArrayInputStream;
 import java.io.DataInputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.Arrays;
+import java.util.List;
 
 /**
  * Utility methods to read backup tar file.
@@ -390,7 +396,7 @@
             boolean allowApks, FileMetadata info, Signature[] signatures,
             PackageManagerInternal pmi, int userId, Context context) {
         return chooseRestorePolicy(packageManager, allowApks, info, signatures, pmi, userId,
-                BackupEligibilityRules.forBackup(packageManager, pmi, userId, context));
+                BackupEligibilityRules.forBackup(packageManager, pmi, userId, context), context);
     }
 
     /**
@@ -406,7 +412,8 @@
      */
     public RestorePolicy chooseRestorePolicy(PackageManager packageManager,
             boolean allowApks, FileMetadata info, Signature[] signatures,
-            PackageManagerInternal pmi, int userId, BackupEligibilityRules eligibilityRules) {
+            PackageManagerInternal pmi, int userId, BackupEligibilityRules eligibilityRules,
+            Context context) {
         if (signatures == null) {
             return RestorePolicy.IGNORE;
         }
@@ -448,6 +455,16 @@
                                     pkgInfo,
                                     LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
                                     null);
+                        } else if (isAllowlistedForVToURestore(info, pkgInfo, userId, context)) {
+                            Slog.i(TAG, "Performing a V to U downgrade; package: "
+                                            + info.packageName
+                                            + " is allowlisted");
+                            policy = RestorePolicy.ACCEPT;
+                            mBackupManagerMonitorEventSender.monitorEvent(
+                                    LOG_EVENT_ID_V_TO_U_RESTORE_PKG_ELIGIBLE,
+                                    pkgInfo,
+                                    LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+                                    null);
                         } else {
                             // The data is from a newer version of the app than
                             // is presently installed.  That means we can only
@@ -751,6 +768,36 @@
         return true;
     }
 
+    // checks the sdk of the target/source device for a B&R operation.
+    // system components can opt in of V->U restore via allowlist.
+    @SuppressWarnings("AndroidFrameworkCompatChange")
+    private boolean isAllowlistedForVToURestore(FileMetadata backupFileInfo,
+            PackageInfo installedPackageInfo,
+            int userId, Context context) {
+        // We assume that the package version matches the sdk (e.g. version 35 means V).
+        // This is true for most of the system components ( and it is specifically true for those
+        // that are in the allowlist)
+        // In order to check if this is a V to U transfer we check if the package version from the
+        // backup is 35 and on the target is 34.
+        // We don't need to check the  V to U denylist here since a package can only make it
+        // to TarBackupReader if allowed and not denied (from PerformUnifiedRestoreTask)
+
+        String vToUAllowlist = getVToUAllowlist(context, userId);
+        List<String> mVToUAllowlist = Arrays.asList(vToUAllowlist.split(","));
+        return Flags.enableVToURestoreForSystemComponentsInAllowlist()
+                && (installedPackageInfo.getLongVersionCode()
+                == Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+                && (backupFileInfo.version > Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+                && (mVToUAllowlist.contains(installedPackageInfo.packageName));
+    }
+
+    private String getVToUAllowlist(Context context, int userId) {
+        return Settings.Secure.getStringForUser(
+                context.getContentResolver(),
+                Settings.Secure.V_TO_U_RESTORE_ALLOWLIST,
+                userId);
+    }
+
     private static long extractRadix(byte[] data, int offset, int maxChars, int radix)
             throws IOException {
         long value = 0;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 4c8f416..f7278e9 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -40,6 +40,8 @@
 import static android.app.ActivityManager.RESTRICTION_LEVEL_FORCE_STOPPED;
 import static android.app.ActivityManager.RESTRICTION_REASON_DEFAULT;
 import static android.app.ActivityManager.RESTRICTION_REASON_USAGE;
+import static android.app.ActivityManager.RESTRICTION_SOURCE_SYSTEM;
+import static android.app.ActivityManager.RESTRICTION_SOURCE_USER;
 import static android.app.ActivityManager.StopUserOnSwitch;
 import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_FROZEN;
 import static android.app.ActivityManager.UidFrozenStateChangedCallback.UID_FROZEN_STATE_UNFROZEN;
@@ -5148,7 +5150,7 @@
         if (android.app.Flags.appRestrictionsApi() && wasForceStopped) {
             noteAppRestrictionEnabled(app.info.packageName, app.uid,
                     RESTRICTION_LEVEL_FORCE_STOPPED, false,
-                    RESTRICTION_REASON_USAGE, "unknown", 0L);
+                    RESTRICTION_REASON_USAGE, "unknown", RESTRICTION_SOURCE_USER, 0L);
         }
 
         if (!sendBroadcast) {
@@ -6023,46 +6025,47 @@
         }
 
         synchronized (mProcLock) {
-            synchronized (mPidsSelfLocked) {
-                int newestTimeIndex = -1;
-                long newestTime = Long.MIN_VALUE;
-                for (int i = 0; i < pids.length; i++) {
-                    ProcessRecord pr = mPidsSelfLocked.get(pids[i]);
-                    if (pr != null) {
-                        final long pendingTopTime =
-                                mPendingStartActivityUids.getPendingTopPidTime(pr.uid, pids[i]);
-                        if (pendingTopTime != PendingStartActivityUids.INVALID_TIME) {
-                            // The uid in mPendingStartActivityUids gets the TOP process state.
-                            states[i] = PROCESS_STATE_TOP;
-                            if (scores != null) {
-                                // The uid in mPendingStartActivityUids gets a better score.
-                                scores[i] = ProcessList.FOREGROUND_APP_ADJ - 1;
-                            }
-                            if (pendingTopTime > newestTime) {
-                                newestTimeIndex = i;
-                                newestTime = pendingTopTime;
-                            }
-                        } else {
-                            states[i] = pr.mState.getCurProcState();
-                            if (scores != null) {
-                                scores[i] = pr.mState.getCurAdj();
-                            }
+            int newestTimeIndex = -1;
+            long newestTime = Long.MIN_VALUE;
+            for (int i = 0; i < pids.length; i++) {
+                final ProcessRecord pr;
+                synchronized (mPidsSelfLocked) {
+                    pr = mPidsSelfLocked.get(pids[i]);
+                }
+                if (pr != null) {
+                    final long pendingTopTime =
+                            mPendingStartActivityUids.getPendingTopPidTime(pr.uid, pids[i]);
+                    if (pendingTopTime != PendingStartActivityUids.INVALID_TIME) {
+                        // The uid in mPendingStartActivityUids gets the TOP process state.
+                        states[i] = PROCESS_STATE_TOP;
+                        if (scores != null) {
+                            // The uid in mPendingStartActivityUids gets a better score.
+                            scores[i] = ProcessList.FOREGROUND_APP_ADJ - 1;
+                        }
+                        if (pendingTopTime > newestTime) {
+                            newestTimeIndex = i;
+                            newestTime = pendingTopTime;
                         }
                     } else {
-                        states[i] = PROCESS_STATE_NONEXISTENT;
+                        states[i] = pr.mState.getCurProcState();
                         if (scores != null) {
-                            scores[i] = ProcessList.INVALID_ADJ;
+                            scores[i] = pr.mState.getCurAdj();
                         }
                     }
-                }
-                // The uid with the newest timestamp in mPendingStartActivityUids gets the best
-                // score.
-                if (newestTimeIndex != -1) {
+                } else {
+                    states[i] = PROCESS_STATE_NONEXISTENT;
                     if (scores != null) {
-                        scores[newestTimeIndex] = ProcessList.FOREGROUND_APP_ADJ - 2;
+                        scores[i] = ProcessList.INVALID_ADJ;
                     }
                 }
             }
+            // The uid with the newest timestamp in mPendingStartActivityUids gets the best
+            // score.
+            if (newestTimeIndex != -1) {
+                if (scores != null) {
+                    scores[newestTimeIndex] = ProcessList.FOREGROUND_APP_ADJ - 2;
+                }
+            }
         }
     }
 
@@ -10304,7 +10307,8 @@
         command.add("/system/bin/logcat");
         command.add("-v");
         // This adds a timestamp and thread info to each log line.
-        command.add("threadtime");
+        // Also change the timestamps to use UTC time.
+        command.add("threadtime,UTC");
         for (String buffer : buffers) {
             command.add("-b");
             command.add(buffer);
@@ -14399,7 +14403,8 @@
                     if (wasStopped) {
                         noteAppRestrictionEnabled(app.packageName, app.uid,
                                 RESTRICTION_LEVEL_FORCE_STOPPED, false,
-                                RESTRICTION_REASON_DEFAULT, "restore", 0L);
+                                RESTRICTION_REASON_DEFAULT, "restore",
+                                RESTRICTION_SOURCE_SYSTEM, 0L);
                     }
                 } catch (NameNotFoundException e) {
                     Slog.w(TAG, "No such package", e);
@@ -16953,6 +16958,18 @@
 
         int userId = UserHandle.getCallingUserId();
 
+        if (UserManager.isVisibleBackgroundUsersEnabled() && userId != getCurrentUserId()) {
+            // The check is added mainly for auto devices. On auto devices, it is possible that
+            // multiple users are visible simultaneously using visible background users.
+            // In such cases, it is desired that only the current user (not the visible background
+            // user) can change the locale and other persistent settings of the device.
+            Slog.w(TAG, "Only current user is allowed to update persistent configuration if "
+                    + "visible background users are enabled. Current User" + getCurrentUserId()
+                    + ". Calling User: " + userId);
+            throw new SecurityException("Only current user is allowed to update persistent "
+                    + "configuration.");
+        }
+
         mActivityTaskManager.updatePersistentConfiguration(values, userId);
     }
 
@@ -20304,12 +20321,14 @@
      * Log the reason for changing an app restriction. Purely used for logging purposes and does not
      * cause any change to app state.
      *
-     * @see ActivityManager#noteAppRestrictionEnabled(String, int, int, boolean, int, String, long)
+     * @see ActivityManager#noteAppRestrictionEnabled(String, int, int, boolean, int,
+     *          String, int, long)
      */
     @Override
     public void noteAppRestrictionEnabled(String packageName, int uid,
             @RestrictionLevel int restrictionType, boolean enabled,
-            @ActivityManager.RestrictionReason int reason, String subReason, long threshold) {
+            @ActivityManager.RestrictionReason int reason, String subReason,
+            @ActivityManager.RestrictionSource int source, long threshold) {
         if (!android.app.Flags.appRestrictionsApi()) return;
 
         enforceCallingPermission(android.Manifest.permission.DEVICE_POWER,
@@ -20322,7 +20341,7 @@
                 uid = mPackageManagerInt.getPackageUid(packageName, 0, userId);
             }
             mAppRestrictionController.noteAppRestrictionEnabled(packageName, uid, restrictionType,
-                    enabled, reason, subReason, threshold);
+                    enabled, reason, subReason, source, threshold);
         } finally {
             Binder.restoreCallingIdentity(callingId);
         }
diff --git a/services/core/java/com/android/server/am/AppRestrictionController.java b/services/core/java/com/android/server/am/AppRestrictionController.java
index f5f1928..4a31fd1 100644
--- a/services/core/java/com/android/server/am/AppRestrictionController.java
+++ b/services/core/java/com/android/server/am/AppRestrictionController.java
@@ -31,11 +31,10 @@
 import static android.app.ActivityManager.RESTRICTION_LEVEL_USER_LAUNCH_ONLY;
 import static android.app.ActivityManager.RESTRICTION_REASON_DEFAULT;
 import static android.app.ActivityManager.RESTRICTION_REASON_DORMANT;
-import static android.app.ActivityManager.RESTRICTION_REASON_REMOTE_TRIGGER;
+import static android.app.ActivityManager.RESTRICTION_REASON_POLICY;
 import static android.app.ActivityManager.RESTRICTION_REASON_SYSTEM_HEALTH;
 import static android.app.ActivityManager.RESTRICTION_REASON_USAGE;
 import static android.app.ActivityManager.RESTRICTION_REASON_USER;
-import static android.app.ActivityManager.RESTRICTION_REASON_USER_NUDGED;
 import static android.app.ActivityManager.RESTRICTION_SUBREASON_MAX_LENGTH;
 import static android.app.ActivityManager.UID_OBSERVER_ACTIVE;
 import static android.app.ActivityManager.UID_OBSERVER_GONE;
@@ -103,6 +102,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityManager.RestrictionLevel;
 import android.app.ActivityManager.RestrictionReason;
+import android.app.ActivityManager.RestrictionSource;
 import android.app.ActivityManagerInternal;
 import android.app.ActivityManagerInternal.AppBackgroundRestrictionListener;
 import android.app.AppOpsManager;
@@ -2378,7 +2378,8 @@
      */
     public void noteAppRestrictionEnabled(String packageName, int uid,
             @RestrictionLevel int restrictionType, boolean enabled,
-            @RestrictionReason int reason, String subReason, long threshold) {
+            @RestrictionReason int reason, String subReason, @RestrictionSource int source,
+            long threshold) {
         if (DEBUG_BG_RESTRICTION_CONTROLLER) {
             Slog.i(TAG, (enabled ? "restricted " : "unrestricted ") + packageName + " to "
                     + restrictionType + " reason=" + reason + ", subReason=" + subReason
@@ -2397,7 +2398,8 @@
                 enabled,
                 getRestrictionChangeReasonStatsd(reason, subReason),
                 subReason,
-                threshold);
+                threshold,
+                source);
     }
 
     private int getRestrictionTypeStatsd(@RestrictionLevel int level) {
@@ -2433,12 +2435,10 @@
                     FrameworkStatsLog.APP_RESTRICTION_STATE_CHANGED__MAIN_REASON__REASON_USAGE;
             case RESTRICTION_REASON_USER ->
                     FrameworkStatsLog.APP_RESTRICTION_STATE_CHANGED__MAIN_REASON__REASON_USER;
-            case RESTRICTION_REASON_USER_NUDGED ->
-                    FrameworkStatsLog.APP_RESTRICTION_STATE_CHANGED__MAIN_REASON__REASON_USER_NUDGED;
             case RESTRICTION_REASON_SYSTEM_HEALTH ->
                     FrameworkStatsLog.APP_RESTRICTION_STATE_CHANGED__MAIN_REASON__REASON_SYSTEM_HEALTH;
-            case RESTRICTION_REASON_REMOTE_TRIGGER ->
-                    FrameworkStatsLog.APP_RESTRICTION_STATE_CHANGED__MAIN_REASON__REASON_REMOTE_TRIGGER;
+            case RESTRICTION_REASON_POLICY ->
+                    FrameworkStatsLog.APP_RESTRICTION_STATE_CHANGED__MAIN_REASON__REASON_POLICY;
             default ->
                     FrameworkStatsLog.APP_RESTRICTION_STATE_CHANGED__MAIN_REASON__REASON_OTHER;
         };
diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java
index 79a8518..a8227fa 100644
--- a/services/core/java/com/android/server/am/AppStartInfoTracker.java
+++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java
@@ -83,6 +83,7 @@
     private static final int FOREACH_ACTION_NONE = 0;
     private static final int FOREACH_ACTION_REMOVE_ITEM = 1;
     private static final int FOREACH_ACTION_STOP_ITERATION = 2;
+    private static final int FOREACH_ACTION_REMOVE_AND_STOP_ITERATION = 3;
 
     private static final String MONITORING_MODE_EMPTY_TEXT = "No records";
 
@@ -659,8 +660,13 @@
         }
     }
 
+    /**
+     * Run provided callback for each packake in start info dataset.
+     *
+     * @return whether the for each completed naturally, false if it was stopped manually.
+     */
     @GuardedBy("mLock")
-    private void forEachPackageLocked(
+    private boolean forEachPackageLocked(
             BiFunction<String, SparseArray<AppStartInfoContainer>, Integer> callback) {
         if (callback != null) {
             ArrayMap<String, SparseArray<AppStartInfoContainer>> map = mData.getMap();
@@ -670,14 +676,17 @@
                         map.removeAt(i);
                         break;
                     case FOREACH_ACTION_STOP_ITERATION:
-                        i = 0;
-                        break;
+                        return false;
+                    case FOREACH_ACTION_REMOVE_AND_STOP_ITERATION:
+                        map.removeAt(i);
+                        return false;
                     case FOREACH_ACTION_NONE:
                     default:
                         break;
                 }
             }
         }
+        return true;
     }
 
     @GuardedBy("mLock")
@@ -870,13 +879,14 @@
         }
         AtomicFile af = new AtomicFile(mProcStartInfoFile);
         FileOutputStream out = null;
+        boolean succeeded;
         long now = System.currentTimeMillis();
         try {
             out = af.startWrite();
             ProtoOutputStream proto = new ProtoOutputStream(out);
             proto.write(AppsStartInfoProto.LAST_UPDATE_TIMESTAMP, now);
             synchronized (mLock) {
-                forEachPackageLocked(
+                succeeded = forEachPackageLocked(
                         (packageName, records) -> {
                             long token = proto.start(AppsStartInfoProto.PACKAGES);
                             proto.write(AppsStartInfoProto.Package.PACKAGE_NAME, packageName);
@@ -884,19 +894,30 @@
                             for (int j = 0; j < uidArraySize; j++) {
                                 try {
                                     records.valueAt(j)
-                                        .writeToProto(proto, AppsStartInfoProto.Package.USERS);
+                                            .writeToProto(proto, AppsStartInfoProto.Package.USERS);
                                 } catch (IOException e) {
                                     Slog.w(TAG, "Unable to write app start info into persistent"
                                             + "storage: " + e);
+                                    // There was likely an issue with this record that won't resolve
+                                    // next time we try to persist so remove it. Also stop iteration
+                                    // as we failed the write and need to start again from scratch.
+                                    return AppStartInfoTracker
+                                            .FOREACH_ACTION_REMOVE_AND_STOP_ITERATION;
                                 }
                             }
                             proto.end(token);
                             return AppStartInfoTracker.FOREACH_ACTION_NONE;
                         });
-                mLastAppStartInfoPersistTimestamp = now;
+                if (succeeded) {
+                    mLastAppStartInfoPersistTimestamp = now;
+                }
             }
-            proto.flush();
-            af.finishWrite(out);
+            if (succeeded) {
+                proto.flush();
+                af.finishWrite(out);
+            } else {
+                af.failWrite(out);
+            }
         } catch (IOException e) {
             Slog.w(TAG, "Unable to write historical app start info into persistent storage: " + e);
             af.failWrite(out);
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index 211f952..db4840d 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -83,6 +83,8 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.ServiceThread;
 
+import dalvik.annotation.optimization.NeverCompile;
+
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -98,8 +100,6 @@
 import java.util.Random;
 import java.util.Set;
 
-import dalvik.annotation.optimization.NeverCompile;
-
 public final class CachedAppOptimizer {
 
     // Flags stored in the DeviceConfig API.
@@ -2633,7 +2633,7 @@
     public void binderError(int debugPid, ProcessRecord app, int code, int flags, int err) {
         Slog.w(TAG_AM, "pid " + debugPid + " " + (app == null ? "null" : app.processName)
                 + " sent binder code " + code + " with flags " + flags
-                + " to frozen apps and got error " + err);
+                + " and got error " + err);
 
         // Do nothing if the binder error callback is not enabled.
         // That means the frozen apps in a wrong state will be killed when they are unfrozen later.
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index fa8832c..219de70 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -36,6 +36,7 @@
 import static android.os.Process.startWebView;
 import static android.system.OsConstants.EAGAIN;
 
+import static com.android.sdksandbox.flags.Flags.selinuxInputSelector;
 import static com.android.sdksandbox.flags.Flags.selinuxSdkSandboxAudit;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_LRU;
 import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_NETWORK;
@@ -2066,11 +2067,16 @@
             }
         }
 
-        return app.info.seInfo
-                + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser) + extraInfo;
+        // The order of selectors in seInfo matters, the string is terminated by the word complete.
+        if (selinuxInputSelector()) {
+            return app.info.seInfo + extraInfo + TextUtils.emptyIfNull(app.info.seInfoUser);
+        } else {
+            return app.info.seInfo
+                    + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser)
+                    + extraInfo;
+        }
     }
 
-
     @GuardedBy("mService")
     boolean startProcessLocked(HostingRecord hostingRecord, String entryPoint, ProcessRecord app,
             int uid, int[] gids, int runtimeFlags, int zygotePolicyFlags, int mountExternal,
diff --git a/services/core/java/com/android/server/apphibernation/AppHibernationService.java b/services/core/java/com/android/server/apphibernation/AppHibernationService.java
index e066c23..fb6895b 100644
--- a/services/core/java/com/android/server/apphibernation/AppHibernationService.java
+++ b/services/core/java/com/android/server/apphibernation/AppHibernationService.java
@@ -571,6 +571,7 @@
                 mIActivityManager.noteAppRestrictionEnabled(
                         packageName, uid, ActivityManager.RESTRICTION_LEVEL_FORCE_STOPPED,
                         true, ActivityManager.RESTRICTION_REASON_DORMANT, null,
+                        ActivityManager.RESTRICTION_SOURCE_SYSTEM,
                         /* TODO: fetch actual timeout - 90 days */ 90 * 24 * 60 * 60_000L);
             }
             // No need to log the unhibernate case as an unstop is logged already in ActivityMS
diff --git a/services/core/java/com/android/server/appop/AppOpsRecentAccessPersistence.java b/services/core/java/com/android/server/appop/AppOpsRecentAccessPersistence.java
new file mode 100644
index 0000000..238d9b9
--- /dev/null
+++ b/services/core/java/com/android/server/appop/AppOpsRecentAccessPersistence.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appop;
+
+import static android.app.AppOpsManager.extractFlagsFromKey;
+import static android.app.AppOpsManager.extractUidStateFromKey;
+import static android.companion.virtual.VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.companion.virtual.VirtualDeviceManager;
+import android.os.Process;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.Xml;
+
+import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * This class manages the read/write of AppOp recent accesses between memory and disk.
+ */
+final class AppOpsRecentAccessPersistence {
+    static final String TAG = "AppOpsRecentAccessPersistence";
+    final AtomicFile mRecentAccessesFile;
+    final AppOpsService mAppOpsService;
+
+    private static final String TAG_APP_OPS = "app-ops";
+    private static final String TAG_PACKAGE = "pkg";
+    private static final String TAG_UID = "uid";
+    private static final String TAG_OP = "op";
+    private static final String TAG_ATTRIBUTION_OP = "st";
+
+    private static final String ATTR_NAME = "n";
+    private static final String ATTR_ID = "id";
+    private static final String ATTR_DEVICE_ID = "dv";
+    private static final String ATTR_ACCESS_TIME = "t";
+    private static final String ATTR_REJECT_TIME = "r";
+    private static final String ATTR_ACCESS_DURATION = "d";
+    private static final String ATTR_PROXY_PACKAGE = "pp";
+    private static final String ATTR_PROXY_UID = "pu";
+    private static final String ATTR_PROXY_ATTRIBUTION_TAG = "pc";
+    private static final String ATTR_PROXY_DEVICE_ID = "pdv";
+
+    /**
+     * Version of the mRecentAccessesFile.
+     * Increment by one every time an upgrade step is added at boot, none currently exists.
+     */
+    private static final int CURRENT_VERSION = 1;
+
+    AppOpsRecentAccessPersistence(
+            @NonNull AtomicFile recentAccessesFile, @NonNull AppOpsService appOpsService) {
+        mRecentAccessesFile = recentAccessesFile;
+        mAppOpsService = appOpsService;
+    }
+
+    /**
+     * Load AppOp recent access data from disk into uidStates. The target uidStates will first clear
+     * itself before loading.
+     *
+     * @param uidStates The in-memory object where you want to populate data from disk
+     */
+    void readRecentAccesses(@NonNull SparseArray<AppOpsService.UidState> uidStates) {
+        synchronized (mRecentAccessesFile) {
+            FileInputStream stream;
+            try {
+                stream = mRecentAccessesFile.openRead();
+            } catch (FileNotFoundException e) {
+                Slog.i(
+                        TAG,
+                        "No existing app ops "
+                                + mRecentAccessesFile.getBaseFile()
+                                + "; starting empty");
+                return;
+            }
+            boolean success = false;
+            uidStates.clear();
+            mAppOpsService.mAppOpsCheckingService.clearAllModes();
+            try {
+                TypedXmlPullParser parser = Xml.resolvePullParser(stream);
+                int type;
+                while ((type = parser.next()) != XmlPullParser.START_TAG
+                        && type != XmlPullParser.END_DOCUMENT) {
+                    // Parse next until we reach the start or end
+                }
+
+                if (type != XmlPullParser.START_TAG) {
+                    throw new IllegalStateException("no start tag found");
+                }
+
+                int outerDepth = parser.getDepth();
+                while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                        && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+                    if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                        continue;
+                    }
+
+                    String tagName = parser.getName();
+                    if (tagName.equals(TAG_PACKAGE)) {
+                        readPackage(parser, uidStates);
+                    } else if (tagName.equals(TAG_UID)) {
+                        // uid tag may be present during migration, don't print warning.
+                        XmlUtils.skipCurrentTag(parser);
+                    } else {
+                        Slog.w(TAG, "Unknown element under <app-ops>: " + parser.getName());
+                        XmlUtils.skipCurrentTag(parser);
+                    }
+                }
+
+                success = true;
+            } catch (IllegalStateException | NullPointerException | NumberFormatException
+                     | XmlPullParserException | IOException | IndexOutOfBoundsException e) {
+                Slog.w(TAG, "Failed parsing " + e);
+            } finally {
+                if (!success) {
+                    uidStates.clear();
+                    mAppOpsService.mAppOpsCheckingService.clearAllModes();
+                }
+                try {
+                    stream.close();
+                } catch (IOException ignored) {
+                }
+            }
+        }
+    }
+
+    private void readPackage(
+            TypedXmlPullParser parser, SparseArray<AppOpsService.UidState> uidStates)
+            throws NumberFormatException, XmlPullParserException, IOException {
+        String pkgName = parser.getAttributeValue(null, ATTR_NAME);
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+
+            String tagName = parser.getName();
+            if (tagName.equals(TAG_UID)) {
+                readUid(parser, pkgName, uidStates);
+            } else {
+                Slog.w(TAG, "Unknown element under <pkg>: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+    }
+
+    private void readUid(TypedXmlPullParser parser, @NonNull String pkgName,
+            SparseArray<AppOpsService.UidState> uidStates)
+            throws NumberFormatException, XmlPullParserException, IOException {
+        int uid = parser.getAttributeInt(null, ATTR_NAME);
+        final AppOpsService.UidState uidState = mAppOpsService.new UidState(uid);
+        uidStates.put(uid, uidState);
+
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+            String tagName = parser.getName();
+            if (tagName.equals(TAG_OP)) {
+                readOp(parser, uidState, pkgName);
+            } else {
+                Slog.w(TAG, "Unknown element under <pkg>: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+    }
+
+    private void readOp(TypedXmlPullParser parser,
+            @NonNull AppOpsService.UidState uidState, @NonNull String pkgName)
+            throws NumberFormatException, XmlPullParserException, IOException {
+        int opCode = parser.getAttributeInt(null, ATTR_NAME);
+        AppOpsService.Op op = mAppOpsService.new Op(uidState, pkgName, opCode, uidState.uid);
+
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+            String tagName = parser.getName();
+            if (tagName.equals(TAG_ATTRIBUTION_OP)) {
+                readAttributionOp(parser, op, XmlUtils.readStringAttribute(parser, ATTR_ID));
+            } else {
+                Slog.w(TAG, "Unknown element under <op>: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+
+        AppOpsService.Ops ops = uidState.pkgOps.get(pkgName);
+        if (ops == null) {
+            ops = new AppOpsService.Ops(pkgName, uidState);
+            uidState.pkgOps.put(pkgName, ops);
+        }
+        ops.put(op.op, op);
+    }
+
+    private void readAttributionOp(TypedXmlPullParser parser, @NonNull AppOpsService.Op parent,
+            @Nullable String attribution)
+            throws NumberFormatException, IOException, XmlPullParserException {
+        final long key = parser.getAttributeLong(null, ATTR_NAME);
+        final int uidState = extractUidStateFromKey(key);
+        final int opFlags = extractFlagsFromKey(key);
+
+        String deviceId = parser.getAttributeValue(null, ATTR_DEVICE_ID);
+        final long accessTime = parser.getAttributeLong(null, ATTR_ACCESS_TIME, 0);
+        final long rejectTime = parser.getAttributeLong(null, ATTR_REJECT_TIME, 0);
+        final long accessDuration = parser.getAttributeLong(null, ATTR_ACCESS_DURATION, -1);
+        final String proxyPkg = XmlUtils.readStringAttribute(parser, ATTR_PROXY_PACKAGE);
+        final int proxyUid = parser.getAttributeInt(null, ATTR_PROXY_UID, Process.INVALID_UID);
+        final String proxyAttributionTag =
+                XmlUtils.readStringAttribute(parser, ATTR_PROXY_ATTRIBUTION_TAG);
+        final String proxyDeviceId = parser.getAttributeValue(null, ATTR_PROXY_DEVICE_ID);
+
+        if (deviceId == null || Objects.equals(deviceId, "")) {
+            deviceId = PERSISTENT_DEVICE_ID_DEFAULT;
+        }
+
+        AttributedOp attributedOp = parent.getOrCreateAttribution(parent, attribution, deviceId);
+
+        if (accessTime > 0) {
+            attributedOp.accessed(accessTime, accessDuration, proxyUid, proxyPkg,
+                    proxyAttributionTag, proxyDeviceId, uidState, opFlags);
+        }
+        if (rejectTime > 0) {
+            attributedOp.rejected(rejectTime, uidState, opFlags);
+        }
+    }
+
+    /**
+     * Write uidStates into an XML file on the disk. It's a complete dump from memory, the XML file
+     * will be re-written.
+     *
+     * @param uidStates The in-memory object that holds all AppOp recent access data.
+     */
+    void writeRecentAccesses(SparseArray<AppOpsService.UidState> uidStates) {
+        synchronized (mRecentAccessesFile) {
+            FileOutputStream stream;
+            try {
+                stream = mRecentAccessesFile.startWrite();
+            } catch (IOException e) {
+                Slog.w(TAG, "Failed to write state: " + e);
+                return;
+            }
+
+            try {
+                TypedXmlSerializer out = Xml.resolveSerializer(stream);
+                out.startDocument(null, true);
+                out.startTag(null, TAG_APP_OPS);
+                out.attributeInt(null, "v", CURRENT_VERSION);
+
+                for (int uidIndex = 0; uidIndex < uidStates.size(); uidIndex++) {
+                    AppOpsService.UidState uidState = uidStates.valueAt(uidIndex);
+                    int uid = uidState.uid;
+
+                    for (int pkgIndex = 0; pkgIndex < uidState.pkgOps.size(); pkgIndex++) {
+                        String packageName = uidState.pkgOps.keyAt(pkgIndex);
+                        AppOpsService.Ops ops = uidState.pkgOps.valueAt(pkgIndex);
+
+                        out.startTag(null, TAG_PACKAGE);
+                        out.attribute(null, ATTR_NAME, packageName);
+                        out.startTag(null, TAG_UID);
+                        out.attributeInt(null, ATTR_NAME, uid);
+
+                        for (int opIndex = 0; opIndex < ops.size(); opIndex++) {
+                            AppOpsService.Op op = ops.valueAt(opIndex);
+
+                            out.startTag(null, TAG_OP);
+                            out.attributeInt(null, ATTR_NAME, op.op);
+
+                            writeDeviceAttributedOps(out, op);
+
+                            out.endTag(null, TAG_OP);
+                        }
+
+                        out.endTag(null, TAG_UID);
+                        out.endTag(null, TAG_PACKAGE);
+                    }
+                }
+
+                out.endTag(null, TAG_APP_OPS);
+                out.endDocument();
+                mRecentAccessesFile.finishWrite(stream);
+            } catch (IOException e) {
+                Slog.w(TAG, "Failed to write state, restoring backup.", e);
+                mRecentAccessesFile.failWrite(stream);
+            }
+        }
+    }
+
+    private void writeDeviceAttributedOps(TypedXmlSerializer out, AppOpsService.Op op)
+            throws IOException {
+        for (String deviceId : op.mDeviceAttributedOps.keySet()) {
+            ArrayMap<String, AttributedOp> attributedOps =
+                    op.mDeviceAttributedOps.get(deviceId);
+
+            for (int attrIndex = 0; attrIndex < attributedOps.size(); attrIndex++) {
+                String attributionTag = attributedOps.keyAt(attrIndex);
+                AppOpsManager.AttributedOpEntry attributedOpEntry =
+                        attributedOps.valueAt(attrIndex).createAttributedOpEntryLocked();
+
+                final ArraySet<Long> keys = attributedOpEntry.collectKeys();
+                for (int k = 0; k < keys.size(); k++) {
+                    final long key = keys.valueAt(k);
+
+                    final int uidState = AppOpsManager.extractUidStateFromKey(key);
+                    final int flags = AppOpsManager.extractFlagsFromKey(key);
+
+                    final long accessTime =
+                            attributedOpEntry.getLastAccessTime(uidState, uidState, flags);
+                    final long rejectTime =
+                            attributedOpEntry.getLastRejectTime(uidState, uidState, flags);
+                    final long accessDuration =
+                            attributedOpEntry.getLastDuration(uidState, uidState, flags);
+
+                    // Proxy information for rejections is not backed up
+                    final AppOpsManager.OpEventProxyInfo proxy =
+                            attributedOpEntry.getLastProxyInfo(uidState, uidState, flags);
+
+                    if (accessTime <= 0 && rejectTime <= 0 && accessDuration <= 0
+                            && proxy == null) {
+                        continue;
+                    }
+
+                    out.startTag(null, TAG_ATTRIBUTION_OP);
+                    if (attributionTag != null) {
+                        out.attribute(null, ATTR_ID, attributionTag);
+                    }
+                    out.attributeLong(null, ATTR_NAME, key);
+
+                    if (!Objects.equals(
+                            deviceId, VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
+                        out.attribute(null, ATTR_DEVICE_ID, deviceId);
+                    }
+                    if (accessTime > 0) {
+                        out.attributeLong(null, ATTR_ACCESS_TIME, accessTime);
+                    }
+                    if (rejectTime > 0) {
+                        out.attributeLong(null, ATTR_REJECT_TIME, rejectTime);
+                    }
+                    if (accessDuration > 0) {
+                        out.attributeLong(null, ATTR_ACCESS_DURATION, accessDuration);
+                    }
+                    if (proxy != null) {
+                        out.attributeInt(null, ATTR_PROXY_UID, proxy.getUid());
+
+                        if (proxy.getPackageName() != null) {
+                            out.attribute(null, ATTR_PROXY_PACKAGE, proxy.getPackageName());
+                        }
+                        if (proxy.getAttributionTag() != null) {
+                            out.attribute(
+                                    null, ATTR_PROXY_ATTRIBUTION_TAG, proxy.getAttributionTag());
+                        }
+                        if (proxy.getDeviceId() != null
+                                && !Objects.equals(
+                                        proxy.getDeviceId(),
+                                        VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
+                            out.attribute(null, ATTR_PROXY_DEVICE_ID, proxy.getDeviceId());
+                        }
+                    }
+
+                    out.endTag(null, TAG_ATTRIBUTION_OP);
+                }
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 1acd300..1bb7922 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -71,6 +71,7 @@
 import static android.content.Intent.EXTRA_REPLACING;
 import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
 import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
+import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled;
 
 import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
 
@@ -261,6 +262,7 @@
     private final @Nullable File mNoteOpCallerStacktracesFile;
     final Handler mHandler;
 
+    private final AppOpsRecentAccessPersistence mRecentAccessPersistence;
     /**
      * Pool for {@link AttributedOp.OpEventProxyInfoPool} to avoid to constantly reallocate new
      * objects
@@ -408,7 +410,7 @@
     private @Nullable UserManagerInternal mUserManagerInternal;
 
     /** Interface for app-op modes.*/
-    @VisibleForTesting
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     AppOpsCheckingServiceInterface mAppOpsCheckingService;
 
     /** Interface for app-op restrictions.*/
@@ -528,7 +530,7 @@
     @VisibleForTesting
     final Constants mConstants;
 
-    @VisibleForTesting
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     final class UidState {
         public final int uid;
 
@@ -642,7 +644,7 @@
             }
         }
 
-        private @NonNull AttributedOp getOrCreateAttribution(@NonNull Op parent,
+        @NonNull AttributedOp getOrCreateAttribution(@NonNull Op parent,
                 @Nullable String attributionTag, String persistentDeviceId) {
             ArrayMap<String, AttributedOp> attributedOps = mDeviceAttributedOps.get(
                     persistentDeviceId);
@@ -1003,6 +1005,7 @@
         LockGuard.installLock(this, LockGuard.INDEX_APP_OPS);
         mStorageFile = new AtomicFile(storageFile, "appops_legacy");
         mRecentAccessesFile = new AtomicFile(recentAccessesFile, "appops_accesses");
+        mRecentAccessPersistence = new AppOpsRecentAccessPersistence(mRecentAccessesFile, this);
 
         if (AppOpsManager.NOTE_OP_COLLECTION_ENABLED) {
             mNoteOpCallerStacktracesFile = new File(SystemServiceManager.ensureSystemDir(),
@@ -4910,7 +4913,13 @@
         if (!mRecentAccessesFile.exists()) {
             readRecentAccesses(mStorageFile);
         } else {
-            readRecentAccesses(mRecentAccessesFile);
+            if (deviceAwareAppOpNewSchemaEnabled()) {
+                synchronized (this) {
+                    mRecentAccessPersistence.readRecentAccesses(mUidStates);
+                }
+            } else {
+                readRecentAccesses(mRecentAccessesFile);
+            }
         }
     }
 
@@ -5091,6 +5100,14 @@
 
     @VisibleForTesting
     void writeRecentAccesses() {
+        if (deviceAwareAppOpNewSchemaEnabled()) {
+            synchronized (this) {
+                mRecentAccessPersistence.writeRecentAccesses(mUidStates);
+            }
+            mHistoricalRegistry.writeAndClearDiscreteHistory();
+            return;
+        }
+
         synchronized (mRecentAccessesFile) {
             FileOutputStream stream;
             try {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index c9612ca..e0790da 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -293,6 +293,7 @@
             Iterator<Entry<Pair<Integer, String>, AdiDeviceState>> iterator =
                     mDeviceInventory.entrySet().iterator();
             if (iterator.hasNext()) {
+                iterator.next();
                 iterator.remove();
             }
         }
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 7deef2f..72c5254 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -2123,7 +2123,10 @@
         return AudioProductStrategy.getAudioProductStrategies();
     }
 
-    @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
+    @android.annotation.EnforcePermission(anyOf = {
+            MODIFY_AUDIO_SETTINGS_PRIVILEGED,
+            android.Manifest.permission.MODIFY_AUDIO_ROUTING
+    })
     /**
      * @return the List of {@link android.media.audiopolicy.AudioVolumeGroup} discovered from the
      * platform configuration file.
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 30d12e6..1949e6f 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -56,10 +56,12 @@
 import com.android.server.display.brightness.BrightnessEvent;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.config.HysteresisLevels;
+import com.android.server.display.feature.DisplayManagerFlags;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Manages the associated display brightness when in auto-brightness mode. This is also
@@ -206,7 +208,7 @@
     private float mScreenBrighteningThreshold;
     private float mScreenDarkeningThreshold;
     // The most recent light sample.
-    private float mLastObservedLux = INVALID_LUX;
+    private float mLastObservedLux;
 
     // The time of the most light recent sample.
     private long mLastObservedLuxTime;
@@ -277,6 +279,8 @@
     private Clock mClock;
     private final Injector mInjector;
 
+    private final DisplayManagerFlags mDisplayManagerFlags;
+
     AutomaticBrightnessController(Callbacks callbacks, Looper looper,
             SensorManager sensorManager, Sensor lightSensor,
             SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
@@ -291,7 +295,8 @@
             BrightnessRangeController brightnessModeController,
             BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
             int ambientLightHorizonLong, float userLux, float userNits,
-            BrightnessClamperController brightnessClamperController) {
+            BrightnessClamperController brightnessClamperController,
+            DisplayManagerFlags displayManagerFlags) {
         this(new Injector(), callbacks, looper, sensorManager, lightSensor,
                 brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin, brightnessMax,
                 dozeScaleFactor, lightSensorRate, initialLightSensorRate,
@@ -301,7 +306,7 @@
                 screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
                 screenBrightnessThresholdsIdle, context, brightnessModeController,
                 brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
-                userNits, brightnessClamperController
+                userNits, brightnessClamperController, displayManagerFlags
         );
     }
 
@@ -320,9 +325,10 @@
             BrightnessRangeController brightnessRangeController,
             BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
             int ambientLightHorizonLong, float userLux, float userNits,
-            BrightnessClamperController brightnessClamperController) {
+            BrightnessClamperController brightnessClamperController,
+            DisplayManagerFlags displayManagerFlags) {
         mInjector = injector;
-        mClock = injector.createClock();
+        mClock = injector.createClock(displayManagerFlags.offloadControlsDozeAutoBrightness());
         mContext = context;
         mCallbacks = callbacks;
         mSensorManager = sensorManager;
@@ -367,6 +373,7 @@
         mBrightnessClamperController = brightnessClamperController;
         mBrightnessThrottler = brightnessThrottler;
         mBrightnessMappingStrategyMap = brightnessMappingStrategyMap;
+        mDisplayManagerFlags = displayManagerFlags;
 
         // Use the given short-term model
         if (userNits != BrightnessMappingStrategy.INVALID_NITS) {
@@ -429,34 +436,6 @@
         return mRawScreenAutoBrightness;
     }
 
-    /**
-     * Get the automatic screen brightness based on the last observed lux reading. Used e.g. when
-     * entering doze - we disable the light sensor, invalidate the lux, but we still need to set
-     * the initial brightness in doze mode.
-     */
-    public float getAutomaticScreenBrightnessBasedOnLastUsedLux(
-            BrightnessEvent brightnessEvent) {
-        float lastUsedLux = mAmbientLux;
-        if (lastUsedLux == INVALID_LUX) {
-            return PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        }
-
-        float brightness = mCurrentBrightnessMapper.getBrightness(lastUsedLux,
-                mForegroundAppPackageName, mForegroundAppCategory);
-        if (shouldApplyDozeScaleFactor()) {
-            brightness *= mDozeScaleFactor;
-        }
-
-        if (brightnessEvent != null) {
-            brightnessEvent.setLux(lastUsedLux);
-            brightnessEvent.setRecommendedBrightness(brightness);
-            brightnessEvent.setFlags(brightnessEvent.getFlags()
-                    | (shouldApplyDozeScaleFactor() ? BrightnessEvent.FLAG_DOZE_SCALE : 0));
-            brightnessEvent.setAutoBrightnessMode(getMode());
-        }
-        return brightness;
-    }
-
     public boolean hasValidAmbientLux() {
         return mAmbientLuxValid;
     }
@@ -747,7 +726,6 @@
         mRecentLightSamples++;
         mAmbientLightRingBuffer.prune(time - mAmbientLightHorizonLong);
         mAmbientLightRingBuffer.push(time, lux);
-
         // Remember this sample value.
         mLastObservedLux = lux;
         mLastObservedLuxTime = time;
@@ -891,7 +869,7 @@
     }
 
     private void updateAmbientLux() {
-        long time = mClock.uptimeMillis();
+        long time = mClock.getSensorEventScaleTime();
         mAmbientLightRingBuffer.prune(time - mAmbientLightHorizonLong);
         updateAmbientLux(time);
     }
@@ -968,7 +946,16 @@
             Slog.d(TAG, "updateAmbientLux: Scheduling ambient lux update for " +
                     nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
         }
-        mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX, nextTransitionTime);
+
+        // The nextTransitionTime is computed as elapsedTime(Which also accounts for the time when
+        // android was sleeping) as the main reference. However, handlers work on the uptime(Not
+        // accounting for the time when android was sleeping)
+        mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX,
+                convertToUptime(nextTransitionTime));
+    }
+
+    private long convertToUptime(long time) {
+        return time - mClock.getSensorEventScaleTime() + mClock.uptimeMillis();
     }
 
     private void updateAutoBrightness(boolean sendUpdate, boolean isManuallySet) {
@@ -1185,15 +1172,13 @@
             }
             mPausedShortTermModel.copyFrom(tempShortTermModel);
         }
-
-        update();
     }
 
     /**
      * Responsible for switching the AutomaticBrightnessMode of the associated display. Also takes
      * care of resetting the short term model wherever required
      */
-    public void switchMode(@AutomaticBrightnessMode int mode) {
+    public void switchMode(@AutomaticBrightnessMode int mode, boolean sendUpdate) {
         if (!mBrightnessMappingStrategyMap.contains(mode)) {
             return;
         }
@@ -1208,6 +1193,11 @@
             resetShortTermModel();
             mCurrentBrightnessMapper = mBrightnessMappingStrategyMap.get(mode);
         }
+        if (sendUpdate) {
+            update();
+        } else {
+            updateAutoBrightness(/* sendUpdate= */ false, /* isManuallySet= */ false);
+        }
     }
 
     float getUserLux() {
@@ -1391,7 +1381,9 @@
         @Override
         public void onSensorChanged(SensorEvent event) {
             if (mLightSensorEnabled) {
-                final long time = mClock.uptimeMillis();
+                // The time received from the sensor is in nano seconds, hence changing it to ms
+                final long time = (mDisplayManagerFlags.offloadControlsDozeAutoBrightness())
+                        ? TimeUnit.NANOSECONDS.toMillis(event.timestamp) : mClock.uptimeMillis();
                 final float lux = event.values[0];
                 handleLightSensorEvent(time, lux);
             }
@@ -1424,6 +1416,12 @@
          * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
          */
         long uptimeMillis();
+
+        /**
+         * Gets the time on either the elapsedTime or the uptime scale, depending on how we
+         * processing the events from the sensor
+         */
+        long getSensorEventScaleTime();
     }
 
     /**
@@ -1571,7 +1569,8 @@
             StringBuilder buf = new StringBuilder();
             buf.append('[');
             for (int i = 0; i < mCount; i++) {
-                final long next = i + 1 < mCount ? getTime(i + 1) : mClock.uptimeMillis();
+                final long next = i + 1 < mCount ? getTime(i + 1)
+                        : mClock.getSensorEventScaleTime();
                 if (i != 0) {
                     buf.append(", ");
                 }
@@ -1596,13 +1595,31 @@
         }
     }
 
+    private static class RealClock implements Clock {
+        private final boolean mOffloadControlsDozeBrightness;
+
+        RealClock(boolean offloadControlsDozeBrightness) {
+            mOffloadControlsDozeBrightness = offloadControlsDozeBrightness;
+        }
+
+        @Override
+        public long uptimeMillis() {
+            return SystemClock.uptimeMillis();
+        }
+
+        public long getSensorEventScaleTime() {
+            return (mOffloadControlsDozeBrightness)
+                    ? SystemClock.elapsedRealtime() : uptimeMillis();
+        }
+    }
+
     public static class Injector {
         public Handler getBackgroundThreadHandler() {
             return BackgroundThread.getHandler();
         }
 
-        Clock createClock() {
-            return SystemClock::uptimeMillis;
+        Clock createClock(boolean offloadControlsDozeBrightness) {
+            return new RealClock(offloadControlsDozeBrightness);
         }
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 27ea1cd..d4c0b01 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1745,6 +1745,7 @@
                 if (projection != null) {
                     IBinder taskWindowContainerToken = projection.getLaunchCookie() == null ? null
                             : projection.getLaunchCookie().binder;
+                    int taskId = projection.getTaskId();
                     if (taskWindowContainerToken == null) {
                         // Record a particular display.
                         session = ContentRecordingSession.createDisplaySession(
@@ -1752,7 +1753,7 @@
                     } else {
                         // Record a single task indicated by the launch cookie.
                         session = ContentRecordingSession.createTaskSession(
-                                taskWindowContainerToken);
+                                taskWindowContainerToken, taskId);
                     }
                 }
             } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
index 65c9f35..f77a360 100644
--- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
+++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
@@ -52,6 +52,14 @@
     }
 
     @Override
+    public boolean allowAutoBrightnessInDoze() {
+        if (mDisplayOffloader == null) {
+            return false;
+        }
+        return mDisplayOffloader.allowAutoBrightnessInDoze();
+    }
+
+    @Override
     public void updateBrightness(float brightness) {
         if (mIsActive) {
             mDisplayPowerController.setBrightnessFromOffload(brightness);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 0fcdf19..7d482f7 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1115,7 +1115,7 @@
                     screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
                     mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
                     mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits,
-                    mBrightnessClamperController);
+                    mBrightnessClamperController, mFlags);
             mDisplayBrightnessController.setUpAutoBrightness(
                     mAutomaticBrightnessController, mSensorManager, mDisplayDeviceConfig, mHandler,
                     defaultModeBrightnessMapper, mIsEnabled, mLeadDisplayId);
@@ -1185,7 +1185,8 @@
             @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
         boolean isIdle = mode == AUTO_BRIGHTNESS_MODE_IDLE;
         if (mAutomaticBrightnessController != null) {
-            mAutomaticBrightnessController.switchMode(mode);
+            // Set sendUpdate to true to make sure that updatePowerState() gets called
+            mAutomaticBrightnessController.switchMode(mode, /* sendUpdate= */ true);
             setAnimatorRampSpeeds(isIdle);
         }
         Message msg = mHandler.obtainMessage();
@@ -1334,7 +1335,6 @@
                 mDisplayStateController.shouldPerformScreenOffTransition());
         state = mPowerState.getScreenState();
 
-
         DisplayBrightnessState displayBrightnessState = mDisplayBrightnessController
                 .updateBrightness(mPowerRequest, state);
         float brightnessState = displayBrightnessState.getBrightness();
@@ -1366,17 +1366,26 @@
         // request changes.
         final boolean wasShortTermModelActive =
                 mAutomaticBrightnessStrategy.isShortTermModelActive();
+        boolean allowAutoBrightnessWhileDozing =
+                mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig();
+        if (mFlags.offloadControlsDozeAutoBrightness() && mFlags.isDisplayOffloadEnabled()
+                && mDisplayOffloadSession != null) {
+            allowAutoBrightnessWhileDozing &= mDisplayOffloadSession.allowAutoBrightnessInDoze();
+        }
         if (!mFlags.isRefactorDisplayPowerControllerEnabled()) {
             // Switch to doze auto-brightness mode if needed
             if (mFlags.areAutoBrightnessModesEnabled() && mAutomaticBrightnessController != null
                     && !mAutomaticBrightnessController.isInIdleMode()) {
+                // Set sendUpdate to false, we're already in updatePowerState() so there's no need
+                // to trigger it again
                 mAutomaticBrightnessController.switchMode(Display.isDozeState(state)
-                        ? AUTO_BRIGHTNESS_MODE_DOZE : AUTO_BRIGHTNESS_MODE_DEFAULT);
+                        ? AUTO_BRIGHTNESS_MODE_DOZE : AUTO_BRIGHTNESS_MODE_DEFAULT,
+                        /* sendUpdate= */ false);
             }
 
             mAutomaticBrightnessStrategy.setAutoBrightnessState(state,
-                    mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig(),
-                    mBrightnessReasonTemp.getReason(), mPowerRequest.policy,
+                    allowAutoBrightnessWhileDozing, mBrightnessReasonTemp.getReason(),
+                    mPowerRequest.policy,
                     mDisplayBrightnessController.getLastUserSetScreenBrightness(),
                     userSetBrightnessChanged);
         }
@@ -1443,47 +1452,27 @@
         }
 
         if (Display.isDozeState(state)) {
-            // If there's an offload session, we need to set the initial doze brightness before
-            // the offload session starts controlling the brightness.
-            // During the transition DOZE_SUSPEND -> DOZE -> DOZE_SUSPEND, this brightness strategy
-            // will be selected again, meaning that no new brightness will be sent to the hardware
-            // and the display will stay at the brightness level set by the offload session.
+            // TODO(b/329676661): Introduce a config property to choose between this brightness
+            //  strategy and DOZE_DEFAULT
+            // On some devices, when auto-brightness is disabled and the device is dozing, we use
+            // the current brightness setting scaled by the doze scale factor
             if ((Float.isNaN(brightnessState)
                     || displayBrightnessState.getDisplayBrightnessStrategyName()
                     .equals(DisplayBrightnessStrategyConstants.FALLBACK_BRIGHTNESS_STRATEGY_NAME))
                     && mFlags.isDisplayOffloadEnabled()
-                    && mDisplayOffloadSession != null) {
-                if (mAutomaticBrightnessController != null
-                        && mAutomaticBrightnessStrategy.shouldUseAutoBrightness()) {
-                    // Use the auto-brightness curve and the last observed lux
-                    rawBrightnessState = mAutomaticBrightnessController
-                            .getAutomaticScreenBrightnessBasedOnLastUsedLux(
-                                    mTempBrightnessEvent);
-                } else {
-                    rawBrightnessState = getDozeBrightnessForOffload();
-                    mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()
-                            | BrightnessEvent.FLAG_DOZE_SCALE);
-                }
-
-                if (BrightnessUtils.isValidBrightnessValue(rawBrightnessState)) {
-                    brightnessState = clampScreenBrightness(rawBrightnessState);
-                    mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_INITIAL);
-
-                    if (mAutomaticBrightnessController != null
-                            && mAutomaticBrightnessStrategy.shouldUseAutoBrightness()) {
-                        // Keep the brightness in the setting so that we can use it after the screen
-                        // turns on, until a lux sample becomes available. We don't do this when
-                        // auto-brightness is disabled - in that situation we still want to use
-                        // the last brightness from when the screen was on.
-                        updateScreenBrightnessSetting = currentBrightnessSetting != brightnessState;
-                    }
-                }
+                    && mDisplayOffloadSession != null
+                    && (mAutomaticBrightnessController == null
+                    || !mAutomaticBrightnessStrategy.shouldUseAutoBrightness())) {
+                rawBrightnessState = getDozeBrightnessForOffload();
+                brightnessState = clampScreenBrightness(rawBrightnessState);
+                mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_MANUAL);
+                mTempBrightnessEvent.setFlags(
+                        mTempBrightnessEvent.getFlags() | BrightnessEvent.FLAG_DOZE_SCALE);
             }
 
             // Use default brightness when dozing unless overridden.
-            if (Float.isNaN(brightnessState)
-                    || displayBrightnessState.getDisplayBrightnessStrategyName()
-                    .equals(DisplayBrightnessStrategyConstants.FALLBACK_BRIGHTNESS_STRATEGY_NAME)) {
+            if (Float.isNaN(brightnessState) && Display.isDozeState(state)
+                    && !mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig()) {
                 rawBrightnessState = mScreenBrightnessDozeConfig;
                 brightnessState = clampScreenBrightness(rawBrightnessState);
                 mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_DEFAULT);
@@ -3169,7 +3158,8 @@
                 BrightnessRangeController brightnessModeController,
                 BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
                 int ambientLightHorizonLong, float userLux, float userNits,
-                BrightnessClamperController brightnessClamperController) {
+                BrightnessClamperController brightnessClamperController,
+                DisplayManagerFlags displayManagerFlags) {
 
             return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
                     brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
@@ -3180,7 +3170,7 @@
                     screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
                     screenBrightnessThresholdsIdle, context, brightnessModeController,
                     brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
-                    userNits, brightnessClamperController);
+                    userNits, brightnessClamperController, displayManagerFlags);
         }
 
         BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
index b24caf4..44c8d1c 100644
--- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
+++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
@@ -136,6 +136,9 @@
                     handleExternalDisplayConnectedLocked(logicalDisplay);
                 }
             }
+            if (!mDisplayIdsWaitingForBootCompletion.isEmpty()) {
+                mLogicalDisplayMapper.updateLogicalDisplaysLocked();
+            }
             mDisplayIdsWaitingForBootCompletion.clear();
         }
 
@@ -222,7 +225,7 @@
         } else {
             // As external display is enabled by default, need to disable it now.
             // TODO(b/292196201) Remove when the display can be disabled before DPC is created.
-            mLogicalDisplayMapper.setDisplayEnabledLocked(logicalDisplay, false);
+            mLogicalDisplayMapper.setEnabledLocked(logicalDisplay, false);
         }
 
         if (!isExternalDisplayAllowed()) {
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 01485cb..e645e98 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -1195,7 +1195,6 @@
         return display;
     }
 
-    @VisibleForTesting
     void setEnabledLocked(LogicalDisplay display, boolean isEnabled) {
         final int displayId = display.getDisplayIdLocked();
         final DisplayInfo info = display.getDisplayInfoLocked();
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessReason.java b/services/core/java/com/android/server/display/brightness/BrightnessReason.java
index fc95d15..9bf10a7 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessReason.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessReason.java
@@ -40,8 +40,8 @@
     public static final int REASON_SCREEN_OFF_BRIGHTNESS_SENSOR = 9;
     public static final int REASON_FOLLOWER = 10;
     public static final int REASON_OFFLOAD = 11;
-    public static final int REASON_DOZE_INITIAL = 12;
-    public static final int REASON_MAX = REASON_DOZE_INITIAL;
+    public static final int REASON_DOZE_MANUAL = 12;
+    public static final int REASON_MAX = REASON_DOZE_MANUAL;
 
     public static final int MODIFIER_DIMMED = 0x1;
     public static final int MODIFIER_LOW_POWER = 0x2;
@@ -208,8 +208,8 @@
                 return "follower";
             case REASON_OFFLOAD:
                 return "offload";
-            case REASON_DOZE_INITIAL:
-                return "doze_initial";
+            case REASON_DOZE_MANUAL:
+                return "doze_manual";
             default:
                 return Integer.toString(reason);
         }
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
index 37b6931..2907364 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
@@ -15,8 +15,6 @@
  */
 package com.android.server.display.brightness.strategy;
 
-import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
-
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 
@@ -129,9 +127,11 @@
     public void setAutoBrightnessState(int targetDisplayState,
             boolean allowAutoBrightnessWhileDozingConfig, int brightnessReason, int policy,
             float lastUserSetScreenBrightness, boolean userSetBrightnessChanged) {
-        switchMode(targetDisplayState);
+        // We are still in the process of updating the power state, so there's no need to trigger
+        // an update again
+        switchMode(targetDisplayState, /* sendUpdate= */ false);
         final boolean autoBrightnessEnabledInDoze =
-                allowAutoBrightnessWhileDozingConfig && policy == POLICY_DOZE;
+                allowAutoBrightnessWhileDozingConfig && Display.isDozeState(targetDisplayState);
         mIsAutoBrightnessEnabled = shouldUseAutoBrightness()
                 && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze)
                 && brightnessReason != BrightnessReason.REASON_OVERRIDE
@@ -371,23 +371,6 @@
     }
 
     /**
-     * Get the automatic screen brightness based on the last observed lux reading. Used e.g. when
-     * entering doze - we disable the light sensor, invalidate the lux, but we still need to set
-     * the initial brightness in doze mode.
-     * @param brightnessEvent Event object to populate with details about why the specific
-     *                        brightness was chosen.
-     */
-    public float getAutomaticScreenBrightnessBasedOnLastUsedLux(
-            BrightnessEvent brightnessEvent) {
-        float brightness = (mAutomaticBrightnessController != null)
-                ? mAutomaticBrightnessController
-                .getAutomaticScreenBrightnessBasedOnLastUsedLux(brightnessEvent)
-                : PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        adjustAutomaticBrightnessStateIfValid(brightness);
-        return brightness;
-    }
-
-    /**
      * Returns if the auto brightness has been applied
      */
     public boolean hasAppliedAutoBrightness() {
@@ -495,14 +478,12 @@
             mIsShortTermModelActive = mAutomaticBrightnessController.hasUserDataPoints();
         }
     }
-
-
-    private void switchMode(int state) {
+    private void switchMode(int state, boolean sendUpdate) {
         if (mDisplayManagerFlags.areAutoBrightnessModesEnabled()
                 && mAutomaticBrightnessController != null
                 && !mAutomaticBrightnessController.isInIdleMode()) {
             mAutomaticBrightnessController.switchMode(Display.isDozeState(state)
-                    ? AUTO_BRIGHTNESS_MODE_DOZE : AUTO_BRIGHTNESS_MODE_DEFAULT);
+                    ? AUTO_BRIGHTNESS_MODE_DOZE : AUTO_BRIGHTNESS_MODE_DEFAULT, sendUpdate);
         }
     }
 
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
index 58670c9..4d9c18a 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
@@ -15,8 +15,6 @@
  */
 package com.android.server.display.brightness.strategy;
 
-import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
-
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.display.BrightnessConfiguration;
@@ -110,7 +108,7 @@
             boolean allowAutoBrightnessWhileDozingConfig, int brightnessReason, int policy,
             float lastUserSetScreenBrightness, boolean userSetBrightnessChanged) {
         final boolean autoBrightnessEnabledInDoze =
-                allowAutoBrightnessWhileDozingConfig && policy == POLICY_DOZE;
+                allowAutoBrightnessWhileDozingConfig && Display.isDozeState(targetDisplayState);
         mIsAutoBrightnessEnabled = shouldUseAutoBrightness()
                 && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze)
                 && brightnessReason != BrightnessReason.REASON_OVERRIDE
@@ -273,23 +271,6 @@
     }
 
     /**
-     * Get the automatic screen brightness based on the last observed lux reading. Used e.g. when
-     * entering doze - we disable the light sensor, invalidate the lux, but we still need to set
-     * the initial brightness in doze mode.
-     * @param brightnessEvent Event object to populate with details about why the specific
-     *                        brightness was chosen.
-     */
-    public float getAutomaticScreenBrightnessBasedOnLastUsedLux(
-            BrightnessEvent brightnessEvent) {
-        float brightness = (mAutomaticBrightnessController != null)
-                ? mAutomaticBrightnessController
-                .getAutomaticScreenBrightnessBasedOnLastUsedLux(brightnessEvent)
-                : PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        adjustAutomaticBrightnessStateIfValid(brightness);
-        return brightness;
-    }
-
-    /**
      * Gets the auto-brightness adjustment flag change reason
      */
     public int getAutoBrightnessAdjustmentReasonsFlags() {
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index a5414fc..8f775a5 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -154,6 +154,11 @@
             Flags::useFusionProxSensor
     );
 
+    private final FlagState mOffloadControlsDozeAutoBrightness = new FlagState(
+            Flags.FLAG_OFFLOAD_CONTROLS_DOZE_AUTO_BRIGHTNESS,
+            Flags::offloadControlsDozeAutoBrightness
+    );
+
     private final FlagState mPeakRefreshRatePhysicalLimit = new FlagState(
             Flags.FLAG_ENABLE_PEAK_REFRESH_RATE_PHYSICAL_LIMIT,
             Flags::enablePeakRefreshRatePhysicalLimit
@@ -327,6 +332,13 @@
         return mUseFusionProxSensor.getName();
     }
 
+    /**
+     * @return Whether DisplayOffload should control auto-brightness in doze
+     */
+    public boolean offloadControlsDozeAutoBrightness() {
+        return mOffloadControlsDozeAutoBrightness.isEnabled();
+    }
+
     public boolean isPeakRefreshRatePhysicalLimitEnabled() {
         return mPeakRefreshRatePhysicalLimit.isEnabled();
     }
@@ -373,6 +385,7 @@
         pw.println(" " + mRefactorDisplayPowerController);
         pw.println(" " + mResolutionBackupRestore);
         pw.println(" " + mUseFusionProxSensor);
+        pw.println(" " + mOffloadControlsDozeAutoBrightness);
         pw.println(" " + mPeakRefreshRatePhysicalLimit);
         pw.println(" " + mIgnoreAppPreferredRefreshRate);
         pw.println(" " + mSynthetic60hzModes);
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index 316b6db..697218d 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -246,6 +246,17 @@
 }
 
 flag {
+    name: "offload_controls_doze_auto_brightness"
+    namespace: "display_manager"
+    description: "Allows the registered DisplayOffloader to control if auto-brightness is used in doze"
+    bug: "327392714"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "enable_peak_refresh_rate_physical_limit"
     namespace: "display_manager"
     description: "Flag for adding physical refresh rate limit if smooth display setting is on "
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index e20ac73..76a2827 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -1116,6 +1116,9 @@
             if (mIsLowPower) {
                 for (int i = 0; i < mDisplayDeviceConfigByDisplay.size(); i++) {
                     DisplayDeviceConfig config = mDisplayDeviceConfigByDisplay.valueAt(i);
+                    if (config == null) {
+                        continue;
+                    }
                     List<SupportedModeData> supportedModes = config
                             .getRefreshRateData().lowPowerSupportedModes;
                     mVotesStorage.updateVote(
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
index f992a23..88da6fb 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecNetwork.java
@@ -100,6 +100,9 @@
     // Map from port ID to HdmiDeviceInfo.
     private UnmodifiableSparseArray<HdmiDeviceInfo> mPortDeviceMap;
 
+    // Cached physical address.
+    private int mPhysicalAddress = Constants.INVALID_PHYSICAL_ADDRESS;
+
     HdmiCecNetwork(HdmiControlService hdmiControlService,
             HdmiCecController hdmiCecController,
             HdmiMhlControllerStub hdmiMhlController) {
@@ -431,6 +434,8 @@
         // each port. Return empty array if CEC HAL didn't provide the info.
         if (mHdmiCecController != null) {
             cecPortInfo = mHdmiCecController.getPortInfos();
+            // Invalid cached physical address.
+            mPhysicalAddress = Constants.INVALID_PHYSICAL_ADDRESS;
         }
         if (cecPortInfo == null) {
             return;
@@ -856,7 +861,10 @@
     }
 
     public int getPhysicalAddress() {
-        return mHdmiCecController.getPhysicalAddress();
+        if (mPhysicalAddress == Constants.INVALID_PHYSICAL_ADDRESS) {
+            mPhysicalAddress = mHdmiCecController.getPhysicalAddress();
+        }
+        return mPhysicalAddress;
     }
 
     @ServiceThreadOnly
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index cca73b5..dbd1e65 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1567,7 +1567,7 @@
      * Returns physical address of the device.
      */
     int getPhysicalAddress() {
-        return mCecController.getPhysicalAddress();
+        return mHdmiCecNetwork.getPhysicalAddress();
     }
 
     /**
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java b/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java
index 4ee2e99..6da7a65 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java
@@ -172,17 +172,22 @@
 
     @Override
     public String toString() {
-        String out = ContextHubTransaction.typeToString(mTransactionType, true /* upperCase */)
-                + " (";
+        StringBuilder out = new StringBuilder();
+        out.append(ContextHubTransaction.typeToString(mTransactionType,
+                /* upperCase= */ true));
+        out.append(" (");
         if (mNanoAppId != null) {
-            out += "appId = 0x" + Long.toHexString(mNanoAppId) + ", ";
+            out.append("appId = 0x");
+            out.append(Long.toHexString(mNanoAppId));
+            out.append(", ");
         }
-        out += "package = " + mPackage;
+        out.append("package = ");
+        out.append(mPackage);
         if (mMessageSequenceNumber != null) {
-            out += ", messageSequenceNumber = " + mMessageSequenceNumber;
+            out.append(", messageSequenceNumber = ");
+            out.append(mMessageSequenceNumber);
         }
-        out += ")";
-
-        return out;
+        out.append(")");
+        return out.toString();
     }
 }
diff --git a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
index 39df5be..286e789 100644
--- a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
+++ b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java
@@ -1181,7 +1181,7 @@
                         GnssPsdsDownloader.LONG_TERM_PSDS_SERVER_INDEX));
             }
         } else if ("request_power_stats".equals(command)) {
-            mGnssNative.requestPowerStats();
+            mGnssNative.requestPowerStats(Runnable::run, powerStats -> {});
         } else {
             Log.w(TAG, "sendExtraCommand: unknown command " + command);
         }
diff --git a/services/core/java/com/android/server/location/gnss/GnssManagerService.java b/services/core/java/com/android/server/location/gnss/GnssManagerService.java
index 133704d..6a72cc7 100644
--- a/services/core/java/com/android/server/location/gnss/GnssManagerService.java
+++ b/services/core/java/com/android/server/location/gnss/GnssManagerService.java
@@ -314,9 +314,9 @@
             ipw.decreaseIndent();
         }
 
-        GnssPowerStats powerStats = mGnssNative.getPowerStats();
+        GnssPowerStats powerStats = mGnssNative.getLastKnownPowerStats();
         if (powerStats != null) {
-            ipw.println("Last Power Stats:");
+            ipw.println("Last Known Power Stats:");
             ipw.increaseIndent();
             powerStats.dump(fd, ipw, args, mGnssNative.getCapabilities());
             ipw.decreaseIndent();
diff --git a/services/core/java/com/android/server/location/gnss/GnssMetrics.java b/services/core/java/com/android/server/location/gnss/GnssMetrics.java
index dbc903d..ae79b01 100644
--- a/services/core/java/com/android/server/location/gnss/GnssMetrics.java
+++ b/services/core/java/com/android/server/location/gnss/GnssMetrics.java
@@ -16,6 +16,7 @@
 
 package com.android.server.location.gnss;
 
+import android.annotation.NonNull;
 import android.app.StatsManager;
 import android.content.Context;
 import android.location.GnssSignalQuality;
@@ -60,7 +61,6 @@
     private static final double L5_CARRIER_FREQ_RANGE_LOW_HZ = 1164 * 1e6;
     private static final double L5_CARRIER_FREQ_RANGE_HIGH_HZ = 1189 * 1e6;
 
-
     private long mLogStartInElapsedRealtimeMs;
 
     GnssPowerMetrics mGnssPowerMetrics;
@@ -608,64 +608,72 @@
         }
 
         @Override
-        public int onPullAtom(int atomTag, List<StatsEvent> data) {
-            if (atomTag == FrameworkStatsLog.GNSS_STATS) {
-                data.add(FrameworkStatsLog.buildStatsEvent(atomTag,
-                        mLocationFailureReportsStatistics.getCount(),
-                        mLocationFailureReportsStatistics.getLongSum(),
-                        mTimeToFirstFixMilliSReportsStatistics.getCount(),
-                        mTimeToFirstFixMilliSReportsStatistics.getLongSum(),
-                        mPositionAccuracyMetersReportsStatistics.getCount(),
-                        mPositionAccuracyMetersReportsStatistics.getLongSum(),
-                        mTopFourAverageCn0DbmHzReportsStatistics.getCount(),
-                        mTopFourAverageCn0DbmHzReportsStatistics.getLongSum(),
-                        mL5TopFourAverageCn0DbmHzReportsStatistics.getCount(),
-                        mL5TopFourAverageCn0DbmHzReportsStatistics.getLongSum(), mSvStatusReports,
-                        mSvStatusReportsUsedInFix, mL5SvStatusReports,
-                        mL5SvStatusReportsUsedInFix));
-            } else if (atomTag == FrameworkStatsLog.GNSS_POWER_STATS) {
-                mGnssNative.requestPowerStats();
-                GnssPowerStats gnssPowerStats = mGnssNative.getPowerStats();
-                if (gnssPowerStats == null) {
-                    return StatsManager.PULL_SKIP;
-                }
-                double[] otherModesEnergyMilliJoule = new double[VENDOR_SPECIFIC_POWER_MODES_SIZE];
-                double[] tempGnssPowerStatsOtherModes =
-                        gnssPowerStats.getOtherModesEnergyMilliJoule();
-                if (tempGnssPowerStatsOtherModes.length < VENDOR_SPECIFIC_POWER_MODES_SIZE) {
-                    System.arraycopy(tempGnssPowerStatsOtherModes, 0,
-                            otherModesEnergyMilliJoule, 0,
-                            tempGnssPowerStatsOtherModes.length);
-                } else {
-                    System.arraycopy(tempGnssPowerStatsOtherModes, 0,
-                            otherModesEnergyMilliJoule, 0,
-                            VENDOR_SPECIFIC_POWER_MODES_SIZE);
-                }
-                data.add(FrameworkStatsLog.buildStatsEvent(atomTag,
-                        (long) (gnssPowerStats.getElapsedRealtimeUncertaintyNanos()),
-                        (long) (gnssPowerStats.getTotalEnergyMilliJoule() * CONVERT_MILLI_TO_MICRO),
-                        (long) (gnssPowerStats.getSinglebandTrackingModeEnergyMilliJoule()
-                                * CONVERT_MILLI_TO_MICRO),
-                        (long) (gnssPowerStats.getMultibandTrackingModeEnergyMilliJoule()
-                                * CONVERT_MILLI_TO_MICRO),
-                        (long) (gnssPowerStats.getSinglebandAcquisitionModeEnergyMilliJoule()
-                                * CONVERT_MILLI_TO_MICRO),
-                        (long) (gnssPowerStats.getMultibandAcquisitionModeEnergyMilliJoule()
-                                * CONVERT_MILLI_TO_MICRO),
-                        (long) (otherModesEnergyMilliJoule[0] * CONVERT_MILLI_TO_MICRO),
-                        (long) (otherModesEnergyMilliJoule[1] * CONVERT_MILLI_TO_MICRO),
-                        (long) (otherModesEnergyMilliJoule[2] * CONVERT_MILLI_TO_MICRO),
-                        (long) (otherModesEnergyMilliJoule[3] * CONVERT_MILLI_TO_MICRO),
-                        (long) (otherModesEnergyMilliJoule[4] * CONVERT_MILLI_TO_MICRO),
-                        (long) (otherModesEnergyMilliJoule[5] * CONVERT_MILLI_TO_MICRO),
-                        (long) (otherModesEnergyMilliJoule[6] * CONVERT_MILLI_TO_MICRO),
-                        (long) (otherModesEnergyMilliJoule[7] * CONVERT_MILLI_TO_MICRO),
-                        (long) (otherModesEnergyMilliJoule[8] * CONVERT_MILLI_TO_MICRO),
-                        (long) (otherModesEnergyMilliJoule[9] * CONVERT_MILLI_TO_MICRO)));
-            } else {
-                throw new UnsupportedOperationException("Unknown tagId = " + atomTag);
+        public int onPullAtom(int atomTag, @NonNull List<StatsEvent> data) {
+            switch (atomTag) {
+                case FrameworkStatsLog.GNSS_STATS:
+                    return pullGnssStats(atomTag, data);
+                case FrameworkStatsLog.GNSS_POWER_STATS:
+                    return pullGnssPowerStats(atomTag, data);
+                default:
+                    throw new UnsupportedOperationException("Unknown tagId = " + atomTag);
             }
+        }
+    }
+
+    private int pullGnssStats(int atomTag, List<StatsEvent> data) {
+        data.add(FrameworkStatsLog.buildStatsEvent(atomTag,
+                mLocationFailureReportsStatistics.getCount(),
+                mLocationFailureReportsStatistics.getLongSum(),
+                mTimeToFirstFixMilliSReportsStatistics.getCount(),
+                mTimeToFirstFixMilliSReportsStatistics.getLongSum(),
+                mPositionAccuracyMetersReportsStatistics.getCount(),
+                mPositionAccuracyMetersReportsStatistics.getLongSum(),
+                mTopFourAverageCn0DbmHzReportsStatistics.getCount(),
+                mTopFourAverageCn0DbmHzReportsStatistics.getLongSum(),
+                mL5TopFourAverageCn0DbmHzReportsStatistics.getCount(),
+                mL5TopFourAverageCn0DbmHzReportsStatistics.getLongSum(), mSvStatusReports,
+                mSvStatusReportsUsedInFix, mL5SvStatusReports,
+                mL5SvStatusReportsUsedInFix));
+        return StatsManager.PULL_SUCCESS;
+    }
+
+    private int pullGnssPowerStats(int atomTag, List<StatsEvent> data) {
+        GnssPowerStats powerStats = mGnssNative.requestPowerStatsBlocking();
+        if (powerStats == null) {
+            return StatsManager.PULL_SKIP;
+        } else {
+            data.add(createPowerStatsEvent(atomTag, powerStats));
             return StatsManager.PULL_SUCCESS;
         }
     }
+
+    private static StatsEvent createPowerStatsEvent(int atomTag,
+            @NonNull GnssPowerStats powerStats) {
+        double[] otherModesEnergyMilliJoule = new double[VENDOR_SPECIFIC_POWER_MODES_SIZE];
+        double[] tempGnssPowerStatsOtherModes = powerStats.getOtherModesEnergyMilliJoule();
+        System.arraycopy(tempGnssPowerStatsOtherModes, 0,
+                otherModesEnergyMilliJoule, 0,
+                Math.min(tempGnssPowerStatsOtherModes.length, VENDOR_SPECIFIC_POWER_MODES_SIZE));
+        return FrameworkStatsLog.buildStatsEvent(atomTag,
+                (long) (powerStats.getElapsedRealtimeUncertaintyNanos()),
+                (long) (powerStats.getTotalEnergyMilliJoule() * CONVERT_MILLI_TO_MICRO),
+                (long) (powerStats.getSinglebandTrackingModeEnergyMilliJoule()
+                        * CONVERT_MILLI_TO_MICRO),
+                (long) (powerStats.getMultibandTrackingModeEnergyMilliJoule()
+                        * CONVERT_MILLI_TO_MICRO),
+                (long) (powerStats.getSinglebandAcquisitionModeEnergyMilliJoule()
+                        * CONVERT_MILLI_TO_MICRO),
+                (long) (powerStats.getMultibandAcquisitionModeEnergyMilliJoule()
+                        * CONVERT_MILLI_TO_MICRO),
+                (long) (otherModesEnergyMilliJoule[0] * CONVERT_MILLI_TO_MICRO),
+                (long) (otherModesEnergyMilliJoule[1] * CONVERT_MILLI_TO_MICRO),
+                (long) (otherModesEnergyMilliJoule[2] * CONVERT_MILLI_TO_MICRO),
+                (long) (otherModesEnergyMilliJoule[3] * CONVERT_MILLI_TO_MICRO),
+                (long) (otherModesEnergyMilliJoule[4] * CONVERT_MILLI_TO_MICRO),
+                (long) (otherModesEnergyMilliJoule[5] * CONVERT_MILLI_TO_MICRO),
+                (long) (otherModesEnergyMilliJoule[6] * CONVERT_MILLI_TO_MICRO),
+                (long) (otherModesEnergyMilliJoule[7] * CONVERT_MILLI_TO_MICRO),
+                (long) (otherModesEnergyMilliJoule[8] * CONVERT_MILLI_TO_MICRO),
+                (long) (otherModesEnergyMilliJoule[9] * CONVERT_MILLI_TO_MICRO));
+    }
 }
\ No newline at end of file
diff --git a/services/core/java/com/android/server/location/gnss/hal/GnssNative.java b/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
index bdd4885..c79a21a 100644
--- a/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
+++ b/services/core/java/com/android/server/location/gnss/hal/GnssNative.java
@@ -18,7 +18,9 @@
 
 import static com.android.server.location.gnss.GnssManagerService.TAG;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.location.GnssAntennaInfo;
 import android.location.GnssCapabilities;
@@ -29,6 +31,7 @@
 import android.location.GnssStatus;
 import android.location.Location;
 import android.os.Binder;
+import android.os.Handler;
 import android.os.SystemClock;
 import android.util.Log;
 
@@ -46,9 +49,13 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Entry point for most GNSS HAL commands and callbacks.
@@ -140,6 +147,8 @@
     public static final int AGPS_SETID_TYPE_IMSI = 1;
     public static final int AGPS_SETID_TYPE_MSISDN = 2;
 
+    private static final int POWER_STATS_REQUEST_TIMEOUT_MILLIS = 100;
+
     @IntDef(prefix = "AGPS_SETID_TYPE_", value = {AGPS_SETID_TYPE_NONE, AGPS_SETID_TYPE_IMSI,
             AGPS_SETID_TYPE_MSISDN})
     @Retention(RetentionPolicy.SOURCE)
@@ -289,6 +298,15 @@
                 byte responseType, boolean inEmergencyMode, boolean isCachedLocation);
     }
 
+    /** Callback for reporting {@link GnssPowerStats} */
+    public interface PowerStatsCallback {
+        /**
+         * Called when power stats are reported.
+         * @param powerStats non-null value when power stats are available, {@code null} otherwise.
+         */
+        void onReportPowerStats(@Nullable GnssPowerStats powerStats);
+    }
+
     // set lower than the current ITAR limit of 600m/s to allow this to trigger even if GPS HAL
     // stops output right at 600m/s, depriving this of the information of a device that reaches
     // greater than 600m/s, and higher than the speed of sound to avoid impacting most use cases.
@@ -311,6 +329,8 @@
     @GuardedBy("GnssNative.class")
     private static GnssNative sInstance;
 
+    private final Handler mHandler;
+
     /**
      * Sets GnssHal instance to use for testing.
      */
@@ -367,6 +387,14 @@
     private NavigationMessageCallbacks[] mNavigationMessageCallbacks =
             new NavigationMessageCallbacks[0];
 
+    private @Nullable GnssPowerStats mLastKnownPowerStats = null;
+    private final Object mPowerStatsLock = new Object();
+    private final Runnable mPowerStatsTimeoutCallback = () -> {
+        Log.d(TAG, "Request for power stats timed out");
+        reportGnssPowerStats(null);
+    };
+    private final List<PowerStatsCallback> mPendingPowerStatsCallbacks = new ArrayList<>();
+
     // these callbacks may only have a single implementation
     private GeofenceCallbacks mGeofenceCallbacks;
     private TimeCallbacks mTimeCallbacks;
@@ -381,7 +409,6 @@
 
     private GnssCapabilities mCapabilities = new GnssCapabilities.Builder().build();
     private @GnssCapabilities.TopHalCapabilityFlags int mTopFlags;
-    private @Nullable GnssPowerStats mPowerStats = null;
     private int mHardwareYear = 0;
     private @Nullable String mHardwareModelName = null;
     private long mStartRealtimeMs = 0;
@@ -391,6 +418,7 @@
         mGnssHal = Objects.requireNonNull(gnssHal);
         mEmergencyHelper = injector.getEmergencyHelper();
         mConfiguration = configuration;
+        mHandler = FgThread.getHandler();
     }
 
     public void addBaseCallbacks(BaseCallbacks callbacks) {
@@ -532,8 +560,8 @@
     /**
      * Returns the latest power stats from the GNSS HAL.
      */
-    public @Nullable GnssPowerStats getPowerStats() {
-        return mPowerStats;
+    public @Nullable GnssPowerStats getLastKnownPowerStats() {
+        return mLastKnownPowerStats;
     }
 
     /**
@@ -931,10 +959,49 @@
 
     /**
      * Request an eventual update of GNSS power statistics.
+     *
+     * @param executor Executor that will run {@code callback}
+     * @param callback Called with non-null power stats if they were obtained in time, called with
+     *                 {@code null} if stats could not be obtained in time.
      */
-    public void requestPowerStats() {
+    public void requestPowerStats(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull PowerStatsCallback callback) {
         Preconditions.checkState(mRegistered);
-        mGnssHal.requestPowerStats();
+        synchronized (mPowerStatsLock) {
+            mPendingPowerStatsCallbacks.add(powerStats -> {
+                Binder.withCleanCallingIdentity(
+                        () -> executor.execute(() -> callback.onReportPowerStats(powerStats)));
+            });
+            if (mPendingPowerStatsCallbacks.size() == 1) {
+                mGnssHal.requestPowerStats();
+                mHandler.postDelayed(mPowerStatsTimeoutCallback,
+                        POWER_STATS_REQUEST_TIMEOUT_MILLIS);
+            }
+        }
+    }
+
+    /**
+     * Request GNSS power statistics and blocks for a short time waiting for the result.
+     *
+     * @return non-null power stats, or {@code null} if stats could not be obtained in time.
+     */
+    public @Nullable GnssPowerStats requestPowerStatsBlocking() {
+        AtomicReference<GnssPowerStats> statsWrapper = new AtomicReference<>();
+        CountDownLatch latch = new CountDownLatch(1);
+        requestPowerStats(Runnable::run, powerStats -> {
+            statsWrapper.set(powerStats);
+            latch.countDown();
+        });
+
+        try {
+            latch.await(POWER_STATS_REQUEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            Log.d(TAG, "Interrupted while waiting for power stats");
+            Thread.currentThread().interrupt();
+        }
+
+        return statsWrapper.get();
     }
 
     /**
@@ -1167,7 +1234,14 @@
 
     @NativeEntryPoint
     void reportGnssPowerStats(GnssPowerStats powerStats) {
-        mPowerStats = powerStats;
+        synchronized (mPowerStatsLock) {
+            mHandler.removeCallbacks(mPowerStatsTimeoutCallback);
+            if (powerStats != null) {
+                mLastKnownPowerStats = powerStats;
+            }
+            mPendingPowerStatsCallbacks.forEach(cb -> cb.onReportPowerStats(powerStats));
+            mPendingPowerStatsCallbacks.clear();
+        }
     }
 
     @NativeEntryPoint
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index bbb19e3..e3d5c54 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -553,7 +553,8 @@
                             mProjectionGrant.getLaunchCookie() == null ? null
                                     : mProjectionGrant.getLaunchCookie().binder;
                     setReviewedConsentSessionLocked(
-                            ContentRecordingSession.createTaskSession(taskWindowContainerToken));
+                            ContentRecordingSession.createTaskSession(
+                                    taskWindowContainerToken, mProjectionGrant.mTaskId));
                     break;
             }
         }
@@ -977,6 +978,7 @@
         private IBinder mToken;
         private IBinder.DeathRecipient mDeathEater;
         private boolean mRestoreSystemAlertWindow;
+        private int mTaskId = -1;
         private LaunchCookie mLaunchCookie = null;
 
         // Values for tracking token validity.
@@ -1197,12 +1199,26 @@
 
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)
         @Override // Binder call
+        public void setTaskId(int taskId) {
+            setTaskId_enforcePermission();
+            mTaskId = taskId;
+        }
+
+        @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)
+        @Override // Binder call
         public LaunchCookie getLaunchCookie() {
             getLaunchCookie_enforcePermission();
             return mLaunchCookie;
         }
 
         @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)
+        @Override // Binder call
+        public int getTaskId() {
+            getTaskId_enforcePermission();
+            return mTaskId;
+        }
+
+        @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION)
         @Override
         public boolean isValid() {
             isValid_enforcePermission();
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index 13429db..a7e14d9 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -1540,9 +1540,7 @@
 
         private boolean isAvalancheExemptedFullVolume(final NotificationRecord record) {
             // important conversation
-            if (record.isConversation()
-                    && (record.getImportance() > NotificationManager.IMPORTANCE_DEFAULT
-                    || record.getChannel().isImportantConversation())) {
+            if (record.isConversation() && record.getChannel().isImportantConversation()) {
                 return true;
             }
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 44e7694..c7b0f7d 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -5246,28 +5246,8 @@
         @Override
         public void cancelNotificationFromListener(INotificationListener token, String pkg,
                 String tag, int id) {
-            final int callingUid = Binder.getCallingUid();
-            final int callingPid = Binder.getCallingPid();
-            final long identity = Binder.clearCallingIdentity();
-            try {
-                synchronized (mNotificationLock) {
-                    final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
-                    int cancelReason = REASON_LISTENER_CANCEL;
-                    if (mAssistants.isServiceTokenValidLocked(token)) {
-                        cancelReason = REASON_ASSISTANT_CANCEL;
-                    }
-                    if (info.supportsProfiles()) {
-                        Slog.e(TAG, "Ignoring deprecated cancelNotification(pkg, tag, id) "
-                                + "from " + info.component
-                                + " use cancelNotification(key) instead.");
-                    } else {
-                        cancelNotificationFromListenerLocked(info, callingUid, callingPid,
-                                pkg, tag, id, info.userid, cancelReason);
-                    }
-                }
-            } finally {
-                Binder.restoreCallingIdentity(identity);
-            }
+            Slog.e(TAG, "Ignoring deprecated cancelNotification(pkg, tag, id) use " +
+                    "cancelNotification(key) instead.");
         }
 
         /**
@@ -7794,25 +7774,45 @@
     }
 
     private void checkRemoteViews(String pkg, String tag, int id, Notification notification) {
-        if (removeRemoteView(pkg, tag, id, notification.contentView)) {
+        if (android.app.Flags.removeRemoteViews()) {
+            if (notification.contentView != null || notification.bigContentView != null
+                    ||  notification.headsUpContentView != null
+                    || (notification.publicVersion != null
+                    && (notification.publicVersion.contentView != null
+                    || notification.publicVersion.bigContentView != null
+                    || notification.publicVersion.headsUpContentView != null))) {
+                Slog.i(TAG, "Removed customViews for " + pkg);
+                mUsageStats.registerImageRemoved(pkg);
+            }
             notification.contentView = null;
-        }
-        if (removeRemoteView(pkg, tag, id, notification.bigContentView)) {
             notification.bigContentView = null;
-        }
-        if (removeRemoteView(pkg, tag, id, notification.headsUpContentView)) {
             notification.headsUpContentView = null;
-        }
-        if (notification.publicVersion != null) {
-            if (removeRemoteView(pkg, tag, id, notification.publicVersion.contentView)) {
+            if (notification.publicVersion != null) {
                 notification.publicVersion.contentView = null;
-            }
-            if (removeRemoteView(pkg, tag, id, notification.publicVersion.bigContentView)) {
                 notification.publicVersion.bigContentView = null;
-            }
-            if (removeRemoteView(pkg, tag, id, notification.publicVersion.headsUpContentView)) {
                 notification.publicVersion.headsUpContentView = null;
             }
+        } else {
+            if (removeRemoteView(pkg, tag, id, notification.contentView)) {
+                notification.contentView = null;
+            }
+            if (removeRemoteView(pkg, tag, id, notification.bigContentView)) {
+                notification.bigContentView = null;
+            }
+            if (removeRemoteView(pkg, tag, id, notification.headsUpContentView)) {
+                notification.headsUpContentView = null;
+            }
+            if (notification.publicVersion != null) {
+                if (removeRemoteView(pkg, tag, id, notification.publicVersion.contentView)) {
+                    notification.publicVersion.contentView = null;
+                }
+                if (removeRemoteView(pkg, tag, id, notification.publicVersion.bigContentView)) {
+                    notification.publicVersion.bigContentView = null;
+                }
+                if (removeRemoteView(pkg, tag, id, notification.publicVersion.headsUpContentView)) {
+                    notification.publicVersion.headsUpContentView = null;
+                }
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index ebdca5b..29acbcd 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2308,7 +2308,7 @@
     }
 
     @Override
-    public @NonNull int getProfileAccessibilityLabelResId(@UserIdInt int userId) {
+    public @StringRes int getProfileAccessibilityLabelResId(@UserIdInt int userId) {
         checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId,
                 "getProfileAccessibilityLabelResId");
         final UserInfo userInfo = getUserInfoNoChecks(userId);
diff --git a/services/core/java/com/android/server/pm/UserTypeDetails.java b/services/core/java/com/android/server/pm/UserTypeDetails.java
index c6f36bf..19410e5 100644
--- a/services/core/java/com/android/server/pm/UserTypeDetails.java
+++ b/services/core/java/com/android/server/pm/UserTypeDetails.java
@@ -164,7 +164,7 @@
      * Resource ID ({@link StringRes}) of the accessibility string that describes the user type.
      * This is used by accessibility services like Talkback.
      */
-    private final @Nullable int mAccessibilityString;
+    private final @StringRes int mAccessibilityString;
 
     /**
      * The default {@link UserProperties} for the user type.
@@ -183,7 +183,7 @@
             @Nullable Bundle defaultSystemSettings,
             @Nullable Bundle defaultSecureSettings,
             @Nullable List<DefaultCrossProfileIntentFilter> defaultCrossProfileIntentFilters,
-            @Nullable int accessibilityString,
+            @StringRes int accessibilityString,
             @NonNull UserProperties defaultUserProperties) {
         this.mName = name;
         this.mEnabled = enabled;
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index db18276..12e5180 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -341,10 +341,11 @@
     static final int SHORT_PRESS_SLEEP_GO_TO_SLEEP = 0;
     static final int SHORT_PRESS_SLEEP_GO_TO_SLEEP_AND_GO_HOME = 1;
 
-    // must match: config_shortPressOnSettingsBehavior in config.xml
-    static final int SHORT_PRESS_SETTINGS_NOTHING = 0;
-    static final int SHORT_PRESS_SETTINGS_NOTIFICATION_PANEL = 1;
-    static final int LAST_SHORT_PRESS_SETTINGS_BEHAVIOR = SHORT_PRESS_SETTINGS_NOTIFICATION_PANEL;
+    // must match: config_settingsKeyBehavior in config.xml
+    static final int SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0;
+    static final int SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1;
+    static final int SETTINGS_KEY_BEHAVIOR_NOTHING = 2;
+    static final int LAST_SETTINGS_KEY_BEHAVIOR = SETTINGS_KEY_BEHAVIOR_NOTHING;
 
     static final int PENDING_KEY_NULL = -1;
 
@@ -370,8 +371,8 @@
     static final int TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY = 1;
 
     // Must match: config_searchKeyBehavior in config.xml
-    static final int SEARCH_BEHAVIOR_DEFAULT_SEARCH = 0;
-    static final int SEARCH_BEHAVIOR_TARGET_ACTIVITY = 1;
+    static final int SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH = 0;
+    static final int SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY = 1;
 
     static public final String SYSTEM_DIALOG_REASON_KEY = "reason";
     static public final String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions";
@@ -643,8 +644,8 @@
     // What we do when the user double-taps on home
     int mDoubleTapOnHomeBehavior;
 
-    // What we do when the user presses on settings
-    int mShortPressOnSettingsBehavior;
+    // What we do when the user presses the settings key
+    int mSettingsKeyBehavior;
 
     // Must match config_primaryShortPressTargetActivity in config.xml
     ComponentName mPrimaryShortPressTargetActivity;
@@ -2858,11 +2859,11 @@
             mShortPressOnWindowBehavior = SHORT_PRESS_WINDOW_PICTURE_IN_PICTURE;
         }
 
-        mShortPressOnSettingsBehavior = res.getInteger(
-                com.android.internal.R.integer.config_shortPressOnSettingsBehavior);
-        if (mShortPressOnSettingsBehavior < SHORT_PRESS_SETTINGS_NOTHING
-                || mShortPressOnSettingsBehavior > LAST_SHORT_PRESS_SETTINGS_BEHAVIOR) {
-            mShortPressOnSettingsBehavior = SHORT_PRESS_SETTINGS_NOTHING;
+        mSettingsKeyBehavior = res.getInteger(
+                com.android.internal.R.integer.config_settingsKeyBehavior);
+        if (mSettingsKeyBehavior < SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY
+                || mSettingsKeyBehavior > LAST_SETTINGS_KEY_BEHAVIOR) {
+            mSettingsKeyBehavior = SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY;
         }
     }
 
@@ -3694,12 +3695,12 @@
             case KeyEvent.KEYCODE_SEARCH:
                 if (firstDown && !keyguardOn) {
                     switch (mSearchKeyBehavior) {
-                        case SEARCH_BEHAVIOR_TARGET_ACTIVITY: {
+                        case SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY: {
                             launchTargetSearchActivity();
                             logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_SEARCH);
                             return true;
                         }
-                        case SEARCH_BEHAVIOR_DEFAULT_SEARCH:
+                        case SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH:
                         default:
                             break;
                     }
@@ -3778,14 +3779,16 @@
                         + " interceptKeyBeforeQueueing");
                 return true;
             case KeyEvent.KEYCODE_SETTINGS:
-                if (mShortPressOnSettingsBehavior == SHORT_PRESS_SETTINGS_NOTIFICATION_PANEL) {
-                    if (!down) {
+                if (firstDown) {
+                    if (mSettingsKeyBehavior == SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL) {
                         toggleNotificationPanel();
                         logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL);
+                    } else if (mSettingsKeyBehavior == SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY) {
+                        showSystemSettings();
+                        logKeyboardSystemsEvent(event, KeyboardLogEvent.LAUNCH_SYSTEM_SETTINGS);
                     }
-                    return true;
                 }
-                break;
+                return true;
             case KeyEvent.KEYCODE_STEM_PRIMARY:
                 if (prepareToSendSystemKeyToApplication(focusedToken, event)) {
                     // Send to app.
@@ -6563,8 +6566,8 @@
                 pw.print("mLongPressOnPowerBehavior=");
                 pw.println(longPressOnPowerBehaviorToString(mLongPressOnPowerBehavior));
         pw.print(prefix);
-        pw.print("mShortPressOnSettingsBehavior=");
-        pw.println(shortPressOnSettingsBehaviorToString(mShortPressOnSettingsBehavior));
+        pw.print("mSettingsKeyBehavior=");
+        pw.println(settingsKeyBehaviorToString(mSettingsKeyBehavior));
         pw.print(prefix);
         pw.print("mLongPressOnPowerAssistantTimeoutMs=");
         pw.println(mLongPressOnPowerAssistantTimeoutMs);
@@ -6766,12 +6769,14 @@
         }
     }
 
-    private static String shortPressOnSettingsBehaviorToString(int behavior) {
+    private static String settingsKeyBehaviorToString(int behavior) {
         switch (behavior) {
-            case SHORT_PRESS_SETTINGS_NOTHING:
-                return "SHORT_PRESS_SETTINGS_NOTHING";
-            case SHORT_PRESS_SETTINGS_NOTIFICATION_PANEL:
-                return "SHORT_PRESS_SETTINGS_NOTIFICATION_PANEL";
+            case SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY:
+                return "SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY";
+            case SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL:
+                return "SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL";
+            case SETTINGS_KEY_BEHAVIOR_NOTHING:
+                return "SETTINGS_KEY_BEHAVIOR_NOTHING";
             default:
                 return Integer.toString(behavior);
         }
diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java
index afcf49d..6b7f2fa 100644
--- a/services/core/java/com/android/server/power/ShutdownThread.java
+++ b/services/core/java/com/android/server/power/ShutdownThread.java
@@ -54,6 +54,7 @@
 import android.util.Log;
 import android.util.Slog;
 import android.util.TimingsTraceLog;
+import android.view.SurfaceControl;
 import android.view.WindowManager;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -459,6 +460,10 @@
         metricShutdownStart();
         metricStarted(METRIC_SYSTEM_SERVER);
 
+        // Notify SurfaceFlinger that the device is shutting down.
+        // Transaction traces should be captured at this stage.
+        SurfaceControl.notifyShutdown();
+
         // Start dumping check points for this shutdown in a separate thread.
         Thread dumpCheckPointsThread = ShutdownCheckPoints.newDumpThread(
                 new File(CHECK_POINTS_FILE_BASENAME));
diff --git a/services/core/java/com/android/server/power/stats/AggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/AggregatedPowerStats.java
index e1b4b88..fbdba4f 100644
--- a/services/core/java/com/android/server/power/stats/AggregatedPowerStats.java
+++ b/services/core/java/com/android/server/power/stats/AggregatedPowerStats.java
@@ -19,6 +19,7 @@
 import android.annotation.CurrentTimeMillisLong;
 import android.annotation.DurationMillisLong;
 import android.annotation.NonNull;
+import android.os.BatteryStats;
 import android.os.UserHandle;
 import android.text.format.DateFormat;
 import android.util.IndentingPrintWriter;
@@ -155,11 +156,17 @@
         int powerComponentId = powerStats.descriptor.powerComponentId;
         for (PowerComponentAggregatedPowerStats stats : mPowerComponentStats) {
             if (stats.powerComponentId == powerComponentId) {
-                stats.addPowerStats(powerStats, time);
+                stats.getConfig().getProcessor().addPowerStats(stats, powerStats, time);
             }
         }
     }
 
+    public void noteStateChange(BatteryStats.HistoryItem item) {
+        for (PowerComponentAggregatedPowerStats stats : mPowerComponentStats) {
+            stats.getConfig().getProcessor().noteStateChange(stats, item);
+        }
+    }
+
     void reset() {
         mClockUpdates.clear();
         mDurationMs = 0;
diff --git a/services/core/java/com/android/server/power/stats/AggregatedPowerStatsConfig.java b/services/core/java/com/android/server/power/stats/AggregatedPowerStatsConfig.java
index 884c26c..1ff7cb7 100644
--- a/services/core/java/com/android/server/power/stats/AggregatedPowerStatsConfig.java
+++ b/services/core/java/com/android/server/power/stats/AggregatedPowerStatsConfig.java
@@ -205,7 +205,7 @@
 
     private static final PowerStatsProcessor NO_OP_PROCESSOR = new PowerStatsProcessor() {
         @Override
-        void finish(PowerComponentAggregatedPowerStats stats) {
+        void finish(PowerComponentAggregatedPowerStats stats, long timestampMs) {
         }
     };
 }
diff --git a/services/core/java/com/android/server/power/stats/BinaryStatePowerStatsLayout.java b/services/core/java/com/android/server/power/stats/BinaryStatePowerStatsLayout.java
new file mode 100644
index 0000000..64c3446
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/BinaryStatePowerStatsLayout.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.power.stats;
+
+class BinaryStatePowerStatsLayout extends PowerStatsLayout {
+    BinaryStatePowerStatsLayout() {
+        addDeviceSectionUsageDuration();
+        // Add a section for consumed energy, even if the specific device does not
+        // have support EnergyConsumers.  This is done to guarantee format compatibility between
+        // PowerStats created by a PowerStatsCollector and those produced by a PowerStatsProcessor.
+        addDeviceSectionEnergyConsumers(1);
+        addDeviceSectionPowerEstimate();
+
+        addUidSectionUsageDuration();
+        addUidSectionPowerEstimate();
+    }
+}
diff --git a/services/core/java/com/android/server/power/stats/BinaryStatePowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/BinaryStatePowerStatsProcessor.java
new file mode 100644
index 0000000..490bd5e
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/BinaryStatePowerStatsProcessor.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.power.stats;
+
+import android.annotation.IntDef;
+import android.os.BatteryStats;
+import android.os.PersistableBundle;
+import android.os.Process;
+
+import com.android.internal.os.PowerStats;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+abstract class BinaryStatePowerStatsProcessor extends PowerStatsProcessor {
+    static final int STATE_OFF = 0;
+    static final int STATE_ON = 1;
+
+    @IntDef(flag = true, prefix = {"STATE_"}, value = {
+            STATE_OFF,
+            STATE_ON,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    protected @interface BinaryState {
+    }
+
+    private final int mPowerComponentId;
+    private final PowerStatsUidResolver mUidResolver;
+    private final UsageBasedPowerEstimator mUsageBasedPowerEstimator;
+    private boolean mEnergyConsumerSupported;
+    private int mInitiatingUid = Process.INVALID_UID;
+    private @BinaryState int mLastState = STATE_OFF;
+    private long mLastStateTimestamp;
+    private long mLastUpdateTimestamp;
+
+    private PowerStats.Descriptor mDescriptor;
+    private final BinaryStatePowerStatsLayout mStatsLayout = new BinaryStatePowerStatsLayout();
+    private PowerStats mPowerStats;
+    private PowerEstimationPlan mPlan;
+    private long[] mTmpDeviceStatsArray;
+    private long[] mTmpUidStatsArray;
+
+    BinaryStatePowerStatsProcessor(int powerComponentId,
+            PowerStatsUidResolver uidResolver, double averagePowerMilliAmp) {
+        mPowerComponentId = powerComponentId;
+        mUsageBasedPowerEstimator = new UsageBasedPowerEstimator(averagePowerMilliAmp);
+        mUidResolver = uidResolver;
+    }
+
+    protected abstract @BinaryState int getBinaryState(BatteryStats.HistoryItem item);
+
+    private void ensureInitialized() {
+        if (mDescriptor != null) {
+            return;
+        }
+
+        PersistableBundle extras = new PersistableBundle();
+        mStatsLayout.toExtras(extras);
+        mDescriptor = new PowerStats.Descriptor(mPowerComponentId,
+                mStatsLayout.getDeviceStatsArrayLength(), null, 0,
+                mStatsLayout.getUidStatsArrayLength(), extras);
+        mPowerStats = new PowerStats(mDescriptor);
+        mPowerStats.stats = new long[mDescriptor.statsArrayLength];
+        mTmpDeviceStatsArray = new long[mDescriptor.statsArrayLength];
+        mTmpUidStatsArray = new long[mDescriptor.uidStatsArrayLength];
+    }
+
+    @Override
+    void start(PowerComponentAggregatedPowerStats stats, long timestampMs) {
+        ensureInitialized();
+
+        // Establish a baseline at the beginning of an accumulation pass
+        mLastState = STATE_OFF;
+        mLastStateTimestamp = timestampMs;
+        mInitiatingUid = Process.INVALID_UID;
+        flushPowerStats(stats, mLastStateTimestamp);
+    }
+
+    @Override
+    void noteStateChange(PowerComponentAggregatedPowerStats stats,
+            BatteryStats.HistoryItem item) {
+        @BinaryState int state = getBinaryState(item);
+        if (state == mLastState) {
+            return;
+        }
+
+        if (state == STATE_ON) {
+            if (item.eventCode == (BatteryStats.HistoryItem.EVENT_STATE_CHANGE
+                    | BatteryStats.HistoryItem.EVENT_FLAG_START)) {
+                mInitiatingUid = mUidResolver.mapUid(item.eventTag.uid);
+            }
+        } else {
+            recordUsageDuration(item.time);
+            mInitiatingUid = Process.INVALID_UID;
+            if (!mEnergyConsumerSupported) {
+                flushPowerStats(stats, item.time);
+            }
+        }
+        mLastStateTimestamp = item.time;
+        mLastState = state;
+    }
+
+    private void recordUsageDuration(long time) {
+        if (mLastState == STATE_OFF) {
+            return;
+        }
+
+        long durationMs = time - mLastStateTimestamp;
+        mStatsLayout.setUsageDuration(mPowerStats.stats,
+                mStatsLayout.getUsageDuration(mPowerStats.stats) + durationMs);
+
+        if (mInitiatingUid != Process.INVALID_UID) {
+            long[] uidStats = mPowerStats.uidStats.get(mInitiatingUid);
+            if (uidStats == null) {
+                uidStats = new long[mDescriptor.uidStatsArrayLength];
+                mPowerStats.uidStats.put(mInitiatingUid, uidStats);
+                mStatsLayout.setUidUsageDuration(uidStats, durationMs);
+            } else {
+                mStatsLayout.setUsageDuration(mPowerStats.stats,
+                        mStatsLayout.getUsageDuration(mPowerStats.stats) + durationMs);
+            }
+        }
+        mLastStateTimestamp = time;
+    }
+
+    void addPowerStats(PowerComponentAggregatedPowerStats stats, PowerStats powerStats,
+            long timestampMs) {
+        ensureInitialized();
+        recordUsageDuration(timestampMs);
+        long consumedEnergy = mStatsLayout.getConsumedEnergy(powerStats.stats, 0);
+        if (consumedEnergy != BatteryStats.POWER_DATA_UNAVAILABLE) {
+            mEnergyConsumerSupported = true;
+            mStatsLayout.setConsumedEnergy(mPowerStats.stats, 0, consumedEnergy);
+        }
+
+        flushPowerStats(stats, timestampMs);
+    }
+
+    private void flushPowerStats(PowerComponentAggregatedPowerStats stats, long timestamp) {
+        mPowerStats.durationMs = timestamp - mLastUpdateTimestamp;
+        stats.addPowerStats(mPowerStats, timestamp);
+
+        Arrays.fill(mPowerStats.stats, 0);
+        mPowerStats.uidStats.clear();
+        mLastUpdateTimestamp = timestamp;
+    }
+
+    private static class Intermediates {
+        public long duration;
+        public double power;
+    }
+
+    @Override
+    void finish(PowerComponentAggregatedPowerStats stats, long timestampMs) {
+        recordUsageDuration(timestampMs);
+        flushPowerStats(stats, timestampMs);
+
+        if (mPlan == null) {
+            mPlan = new PowerEstimationPlan(stats.getConfig());
+        }
+
+        computeDevicePowerEstimates(stats);
+        combineDevicePowerEstimates(stats);
+
+        List<Integer> uids = new ArrayList<>();
+        stats.collectUids(uids);
+
+        computeUidActivityTotals(stats, uids);
+        computeUidPowerEstimates(stats, uids);
+    }
+
+    private void computeDevicePowerEstimates(PowerComponentAggregatedPowerStats stats) {
+        for (int i = mPlan.deviceStateEstimations.size() - 1; i >= 0; i--) {
+            DeviceStateEstimation estimation = mPlan.deviceStateEstimations.get(i);
+            if (!stats.getDeviceStats(mTmpDeviceStatsArray, estimation.stateValues)) {
+                continue;
+            }
+
+            long duration = mStatsLayout.getUsageDuration(mTmpDeviceStatsArray);
+            if (duration > 0) {
+                double power;
+                if (mEnergyConsumerSupported) {
+                    power = uCtoMah(mStatsLayout.getConsumedEnergy(mTmpDeviceStatsArray, 0));
+                } else {
+                    power = mUsageBasedPowerEstimator.calculatePower(duration);
+                }
+                mStatsLayout.setDevicePowerEstimate(mTmpDeviceStatsArray, power);
+                stats.setDeviceStats(estimation.stateValues, mTmpDeviceStatsArray);
+            }
+        }
+    }
+
+    private void combineDevicePowerEstimates(PowerComponentAggregatedPowerStats stats) {
+        for (int i = mPlan.combinedDeviceStateEstimations.size() - 1; i >= 0; i--) {
+            CombinedDeviceStateEstimate estimation =
+                    mPlan.combinedDeviceStateEstimations.get(i);
+            Intermediates intermediates = new Intermediates();
+            estimation.intermediates = intermediates;
+            for (int j = estimation.deviceStateEstimations.size() - 1; j >= 0; j--) {
+                DeviceStateEstimation deviceStateEstimation =
+                        estimation.deviceStateEstimations.get(j);
+                if (!stats.getDeviceStats(mTmpDeviceStatsArray,
+                        deviceStateEstimation.stateValues)) {
+                    continue;
+                }
+                intermediates.power += mStatsLayout.getDevicePowerEstimate(mTmpDeviceStatsArray);
+            }
+        }
+    }
+
+    private void computeUidActivityTotals(PowerComponentAggregatedPowerStats stats,
+            List<Integer> uids) {
+        for (int i = mPlan.uidStateEstimates.size() - 1; i >= 0; i--) {
+            UidStateEstimate uidStateEstimate = mPlan.uidStateEstimates.get(i);
+            Intermediates intermediates =
+                    (Intermediates) uidStateEstimate.combinedDeviceStateEstimate.intermediates;
+            for (int j = uids.size() - 1; j >= 0; j--) {
+                int uid = uids.get(j);
+                for (UidStateProportionalEstimate proportionalEstimate :
+                        uidStateEstimate.proportionalEstimates) {
+                    if (stats.getUidStats(mTmpUidStatsArray, uid,
+                            proportionalEstimate.stateValues)) {
+                        intermediates.duration +=
+                                mStatsLayout.getUidUsageDuration(mTmpUidStatsArray);
+                    }
+                }
+            }
+        }
+    }
+
+    private void computeUidPowerEstimates(PowerComponentAggregatedPowerStats stats,
+            List<Integer> uids) {
+        for (int i = mPlan.uidStateEstimates.size() - 1; i >= 0; i--) {
+            UidStateEstimate uidStateEstimate = mPlan.uidStateEstimates.get(i);
+            Intermediates intermediates =
+                    (Intermediates) uidStateEstimate.combinedDeviceStateEstimate.intermediates;
+            if (intermediates.duration == 0) {
+                continue;
+            }
+            List<UidStateProportionalEstimate> proportionalEstimates =
+                    uidStateEstimate.proportionalEstimates;
+            for (int j = proportionalEstimates.size() - 1; j >= 0; j--) {
+                UidStateProportionalEstimate proportionalEstimate = proportionalEstimates.get(j);
+                for (int k = uids.size() - 1; k >= 0; k--) {
+                    int uid = uids.get(k);
+                    if (stats.getUidStats(mTmpUidStatsArray, uid,
+                            proportionalEstimate.stateValues)) {
+                        double power = intermediates.power
+                                * mStatsLayout.getUidUsageDuration(mTmpUidStatsArray)
+                                / intermediates.duration;
+                        mStatsLayout.setUidPowerEstimate(mTmpUidStatsArray, power);
+                        stats.setUidStats(uid, proportionalEstimate.stateValues,
+                                mTmpUidStatsArray);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/power/stats/BluetoothPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsProcessor.java
index 4d6db97..077b057 100644
--- a/services/core/java/com/android/server/power/stats/BluetoothPowerStatsProcessor.java
+++ b/services/core/java/com/android/server/power/stats/BluetoothPowerStatsProcessor.java
@@ -86,7 +86,7 @@
     }
 
     @Override
-    void finish(PowerComponentAggregatedPowerStats stats) {
+    void finish(PowerComponentAggregatedPowerStats stats, long timestampMs) {
         if (stats.getPowerStatsDescriptor() == null) {
             return;
         }
diff --git a/services/core/java/com/android/server/power/stats/CpuPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/CpuPowerStatsProcessor.java
index 57b7259..6da0a8f 100644
--- a/services/core/java/com/android/server/power/stats/CpuPowerStatsProcessor.java
+++ b/services/core/java/com/android/server/power/stats/CpuPowerStatsProcessor.java
@@ -138,7 +138,7 @@
     }
 
     @Override
-    public void finish(PowerComponentAggregatedPowerStats stats) {
+    public void finish(PowerComponentAggregatedPowerStats stats, long timestampMs) {
         if (stats.getPowerStatsDescriptor() == null) {
             return;
         }
diff --git a/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsProcessor.java
index eebed2f..dcce562 100644
--- a/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsProcessor.java
+++ b/services/core/java/com/android/server/power/stats/MobileRadioPowerStatsProcessor.java
@@ -166,7 +166,7 @@
     }
 
     @Override
-    void finish(PowerComponentAggregatedPowerStats stats) {
+    void finish(PowerComponentAggregatedPowerStats stats, long timestampMs) {
         if (stats.getPowerStatsDescriptor() == null) {
             return;
         }
diff --git a/services/core/java/com/android/server/power/stats/MultiStateStats.java b/services/core/java/com/android/server/power/stats/MultiStateStats.java
index a822281..c3a0aeb 100644
--- a/services/core/java/com/android/server/power/stats/MultiStateStats.java
+++ b/services/core/java/com/android/server/power/stats/MultiStateStats.java
@@ -41,6 +41,7 @@
     private static final String TAG = "MultiStateStats";
 
     private static final String XML_TAG_STATS = "stats";
+    public static final int STATE_DOES_NOT_EXIST = -1;
 
     /**
      * A set of states, e.g. on-battery, screen-on, procstate.  The state values are integers
@@ -70,6 +71,18 @@
         }
 
         /**
+         * Finds state by name in the provided array. If not found, returns STATE_DOES_NOT_EXIST.
+         */
+        public static int findTrackedStateByName(MultiStateStats.States[] states, String name) {
+            for (int i = 0; i < states.length; i++) {
+                if (states[i].getName().equals(name)) {
+                    return i;
+                }
+            }
+            return STATE_DOES_NOT_EXIST;
+        }
+
+        /**
          * Iterates over all combinations of tracked states and invokes <code>consumer</code>
          * for each of them.
          */
diff --git a/services/core/java/com/android/server/power/stats/PhoneCallPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/PhoneCallPowerStatsProcessor.java
index 5c545fd..ec23fa0 100644
--- a/services/core/java/com/android/server/power/stats/PhoneCallPowerStatsProcessor.java
+++ b/services/core/java/com/android/server/power/stats/PhoneCallPowerStatsProcessor.java
@@ -39,7 +39,7 @@
     }
 
     @Override
-    void finish(PowerComponentAggregatedPowerStats stats) {
+    void finish(PowerComponentAggregatedPowerStats stats, long timestampMs) {
         stats.setPowerStatsDescriptor(mDescriptor);
 
         PowerComponentAggregatedPowerStats mobileRadioStats =
diff --git a/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java b/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java
index 0528733..85a2293 100644
--- a/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java
+++ b/services/core/java/com/android/server/power/stats/PowerComponentAggregatedPowerStats.java
@@ -16,6 +16,8 @@
 
 package com.android.server.power.stats;
 
+import static com.android.server.power.stats.MultiStateStats.STATE_DOES_NOT_EXIST;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.UserHandle;
@@ -68,10 +70,12 @@
     private MultiStateStats mDeviceStats;
     private final SparseArray<MultiStateStats> mStateStats = new SparseArray<>();
     private final SparseArray<UidStats> mUidStats = new SparseArray<>();
+    private long[] mZeroArray;
 
     private static class UidStats {
         public int[] states;
         public MultiStateStats stats;
+        public boolean updated;
     }
 
     PowerComponentAggregatedPowerStats(@NonNull AggregatedPowerStats aggregatedPowerStats,
@@ -122,16 +126,18 @@
             }
         }
 
-        if (mUidStateConfig[stateId].isTracked()) {
+        int uidStateId = MultiStateStats.States
+                .findTrackedStateByName(mUidStateConfig, mDeviceStateConfig[stateId].getName());
+        if (uidStateId != STATE_DOES_NOT_EXIST && mUidStateConfig[uidStateId].isTracked()) {
             for (int i = mUidStats.size() - 1; i >= 0; i--) {
                 PowerComponentAggregatedPowerStats.UidStats uidStats = mUidStats.valueAt(i);
                 if (uidStats.stats == null) {
                     createUidStats(uidStats, timestampMs);
                 }
 
-                uidStats.states[stateId] = state;
+                uidStats.states[uidStateId] = state;
                 if (uidStats.stats != null) {
-                    uidStats.stats.setState(stateId, state, timestampMs);
+                    uidStats.stats.setState(uidStateId, state, timestampMs);
                 }
             }
         }
@@ -196,6 +202,22 @@
                 createUidStats(uidStats, timestampMs);
             }
             uidStats.stats.increment(powerStats.uidStats.valueAt(i), timestampMs);
+            uidStats.updated = true;
+        }
+
+        // For UIDs not mentioned in the PowerStats object, we must assume a 0 increment.
+        // It is essential to call `stats.increment(zero)` in order to record the new
+        // timestamp, which will ensure correct proportional attribution across all UIDs
+        for (int i = mUidStats.size() - 1; i >= 0; i--) {
+            PowerComponentAggregatedPowerStats.UidStats uidStats = mUidStats.valueAt(i);
+            if (!uidStats.updated && uidStats.stats != null) {
+                if (mZeroArray == null
+                        || mZeroArray.length != mPowerStatsDescriptor.uidStatsArrayLength) {
+                    mZeroArray = new long[mPowerStatsDescriptor.uidStatsArrayLength];
+                }
+                uidStats.stats.increment(mZeroArray, timestampMs);
+            }
+            uidStats.updated = false;
         }
 
         mPowerStatsTimestamp = timestampMs;
@@ -217,10 +239,13 @@
             uidStats = new UidStats();
             uidStats.states = new int[mUidStateConfig.length];
             for (int stateId = 0; stateId < mUidStateConfig.length; stateId++) {
-                if (mUidStateConfig[stateId].isTracked()
-                        && stateId < mDeviceStateConfig.length
-                        && mDeviceStateConfig[stateId].isTracked()) {
-                    uidStats.states[stateId] = mDeviceStates[stateId];
+                if (mUidStateConfig[stateId].isTracked()) {
+                    int deviceStateId = MultiStateStats.States.findTrackedStateByName(
+                            mDeviceStateConfig, mUidStateConfig[stateId].getName());
+                    if (deviceStateId != STATE_DOES_NOT_EXIST
+                            && mDeviceStateConfig[deviceStateId].isTracked()) {
+                        uidStats.states[stateId] = mDeviceStates[deviceStateId];
+                    }
                 }
             }
             mUidStats.put(uid, uidStats);
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
index 6a4c1f0..6e7fdf1 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java
@@ -71,9 +71,13 @@
                 mStats = new AggregatedPowerStats(mAggregatedPowerStatsConfig);
             }
 
+            start(mStats, startTimeMs);
+
             boolean clockUpdateAdded = false;
             long baseTime = startTimeMs > 0 ? startTimeMs : UNINITIALIZED;
             long lastTime = 0;
+            int lastStates = 0xFFFFFFFF;
+            int lastStates2 = 0xFFFFFFFF;
             try (BatteryStatsHistoryIterator iterator = mHistory.iterate(startTimeMs, endTimeMs)) {
                 while (iterator.hasNext()) {
                     BatteryStats.HistoryItem item = iterator.next();
@@ -111,6 +115,19 @@
                         mCurrentScreenState = screenState;
                     }
 
+                    if ((item.states
+                            & BatteryStats.HistoryItem.IMPORTANT_FOR_POWER_STATS_STATES)
+                            != lastStates
+                            || (item.states2
+                            & BatteryStats.HistoryItem.IMPORTANT_FOR_POWER_STATS_STATES2)
+                            != lastStates2) {
+                        mStats.noteStateChange(item);
+                        lastStates = item.states
+                                & BatteryStats.HistoryItem.IMPORTANT_FOR_POWER_STATS_STATES;
+                        lastStates2 = item.states2
+                                & BatteryStats.HistoryItem.IMPORTANT_FOR_POWER_STATS_STATES2;
+                    }
+
                     if (item.processStateChange != null) {
                         mStats.setUidState(item.processStateChange.uid,
                                 AggregatedPowerStatsConfig.STATE_PROCESS_STATE,
@@ -121,7 +138,7 @@
                         if (!mStats.isCompatible(item.powerStats)) {
                             if (lastTime > baseTime) {
                                 mStats.setDuration(lastTime - baseTime);
-                                finish(mStats);
+                                finish(mStats, lastTime);
                                 consumer.accept(mStats);
                             }
                             mStats.reset();
@@ -134,7 +151,7 @@
             }
             if (lastTime > baseTime) {
                 mStats.setDuration(lastTime - baseTime);
-                finish(mStats);
+                finish(mStats, lastTime);
                 consumer.accept(mStats);
             }
 
@@ -142,12 +159,22 @@
         }
     }
 
-    private void finish(AggregatedPowerStats stats) {
+    private void start(AggregatedPowerStats stats, long timestampMs) {
         for (int i = 0; i < mProcessors.size(); i++) {
             PowerComponentAggregatedPowerStats component =
                     stats.getPowerComponentStats(mProcessors.keyAt(i));
             if (component != null) {
-                mProcessors.valueAt(i).finish(component);
+                mProcessors.valueAt(i).start(component, timestampMs);
+            }
+        }
+    }
+
+    private void finish(AggregatedPowerStats stats, long timestampMs) {
+        for (int i = 0; i < mProcessors.size(); i++) {
+            PowerComponentAggregatedPowerStats component =
+                    stats.getPowerComponentStats(mProcessors.keyAt(i));
+            if (component != null) {
+                mProcessors.valueAt(i).finish(component, timestampMs);
             }
         }
     }
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsLayout.java b/services/core/java/com/android/server/power/stats/PowerStatsLayout.java
index 58efd94..9624fd2 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsLayout.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsLayout.java
@@ -31,6 +31,7 @@
     private static final String EXTRA_DEVICE_DURATION_POSITION = "dd";
     private static final String EXTRA_DEVICE_ENERGY_CONSUMERS_POSITION = "de";
     private static final String EXTRA_DEVICE_ENERGY_CONSUMERS_COUNT = "dec";
+    private static final String EXTRA_UID_DURATION_POSITION = "ud";
     private static final String EXTRA_UID_POWER_POSITION = "up";
 
     protected static final int UNSUPPORTED = -1;
@@ -51,6 +52,7 @@
     private int mDeviceEnergyConsumerPosition;
     private int mDeviceEnergyConsumerCount;
     private int mDevicePowerEstimatePosition = UNSUPPORTED;
+    private int mUidDurationPosition = UNSUPPORTED;
     private int mUidPowerEstimatePosition = UNSUPPORTED;
 
     public PowerStatsLayout() {
@@ -158,7 +160,8 @@
      * PowerStatsService.
      */
     public void addDeviceSectionEnergyConsumers(int energyConsumerCount) {
-        mDeviceEnergyConsumerPosition = addDeviceSection(energyConsumerCount, "energy");
+        mDeviceEnergyConsumerPosition = addDeviceSection(energyConsumerCount, "energy",
+                FLAG_OPTIONAL);
         mDeviceEnergyConsumerCount = energyConsumerCount;
     }
 
@@ -206,6 +209,13 @@
     }
 
     /**
+     * Declare that the UID stats array has a section capturing usage duration
+     */
+    public void addUidSectionUsageDuration() {
+        mUidDurationPosition = addUidSection(1, "time");
+    }
+
+    /**
      * Declare that the UID stats array has a section capturing a power estimate
      */
     public void addUidSectionPowerEstimate() {
@@ -220,6 +230,20 @@
     }
 
     /**
+     * Saves usage duration it in the corresponding element of <code>stats</code>.
+     */
+    public void setUidUsageDuration(long[] stats, long durationMs) {
+        stats[mUidDurationPosition] = durationMs;
+    }
+
+    /**
+     * Extracts the usage duration from a UID stats array.
+     */
+    public long getUidUsageDuration(long[] stats) {
+        return stats[mUidDurationPosition];
+    }
+
+    /**
      * Converts the supplied mAh power estimate to a long and saves it in the corresponding
      * element of <code>stats</code>.
      */
@@ -244,6 +268,7 @@
         extras.putInt(EXTRA_DEVICE_ENERGY_CONSUMERS_COUNT,
                 mDeviceEnergyConsumerCount);
         extras.putInt(EXTRA_DEVICE_POWER_POSITION, mDevicePowerEstimatePosition);
+        extras.putInt(EXTRA_UID_DURATION_POSITION, mUidDurationPosition);
         extras.putInt(EXTRA_UID_POWER_POSITION, mUidPowerEstimatePosition);
         extras.putString(PowerStats.Descriptor.EXTRA_DEVICE_STATS_FORMAT, mDeviceFormat.toString());
         extras.putString(PowerStats.Descriptor.EXTRA_STATE_STATS_FORMAT, mStateFormat.toString());
@@ -258,6 +283,7 @@
         mDeviceEnergyConsumerPosition = extras.getInt(EXTRA_DEVICE_ENERGY_CONSUMERS_POSITION);
         mDeviceEnergyConsumerCount = extras.getInt(EXTRA_DEVICE_ENERGY_CONSUMERS_COUNT);
         mDevicePowerEstimatePosition = extras.getInt(EXTRA_DEVICE_POWER_POSITION);
+        mUidDurationPosition = extras.getInt(EXTRA_UID_DURATION_POSITION);
         mUidPowerEstimatePosition = extras.getInt(EXTRA_UID_POWER_POSITION);
     }
 
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java
index 2fd0b9a..f257e1a 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsProcessor.java
@@ -15,10 +15,16 @@
  */
 package com.android.server.power.stats;
 
+import static com.android.server.power.stats.MultiStateStats.STATE_DOES_NOT_EXIST;
+import static com.android.server.power.stats.MultiStateStats.States.findTrackedStateByName;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.os.BatteryStats;
 import android.util.Log;
 
+import com.android.internal.os.PowerStats;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -40,10 +46,21 @@
 abstract class PowerStatsProcessor {
     private static final String TAG = "PowerStatsProcessor";
 
-    private static final int INDEX_DOES_NOT_EXIST = -1;
     private static final double MILLIAMPHOUR_PER_MICROCOULOMB = 1.0 / 1000.0 / 60.0 / 60.0;
 
-    abstract void finish(PowerComponentAggregatedPowerStats stats);
+    void start(PowerComponentAggregatedPowerStats stats, long timestampMs) {
+    }
+
+    void noteStateChange(PowerComponentAggregatedPowerStats stats,
+            BatteryStats.HistoryItem item) {
+    }
+
+    void addPowerStats(PowerComponentAggregatedPowerStats stats, PowerStats powerStats,
+            long timestampMs) {
+        stats.addPowerStats(powerStats, timestampMs);
+    }
+
+    abstract void finish(PowerComponentAggregatedPowerStats stats, long timestampMs);
 
     protected static class PowerEstimationPlan {
         private final AggregatedPowerStatsConfig.PowerComponent mConfig;
@@ -79,7 +96,7 @@
                 }
 
                 int index = findTrackedStateByName(uidStateConfig, deviceStateConfig[i].getName());
-                if (index != INDEX_DOES_NOT_EXIST && uidStateConfig[index].isTracked()) {
+                if (index != STATE_DOES_NOT_EXIST && uidStateConfig[index].isTracked()) {
                     deviceStatesTrackedPerUid[i] = deviceStateConfig[i];
                 }
             }
@@ -131,7 +148,7 @@
                 }
 
                 int index = findTrackedStateByName(deviceStateConfig, uidStateConfig[i].getName());
-                if (index != INDEX_DOES_NOT_EXIST && deviceStateConfig[index].isTracked()) {
+                if (index != STATE_DOES_NOT_EXIST && deviceStateConfig[index].isTracked()) {
                     uidStatesTrackedForDevice[i] = uidStateConfig[i];
                 } else {
                     uidStatesNotTrackedForDevice[i] = uidStateConfig[i];
@@ -303,15 +320,6 @@
         }
     }
 
-    private static int findTrackedStateByName(MultiStateStats.States[] states, String name) {
-        for (int i = 0; i < states.length; i++) {
-            if (states[i].getName().equals(name)) {
-                return i;
-            }
-        }
-        return INDEX_DOES_NOT_EXIST;
-    }
-
     @NonNull
     private static String concatLabels(MultiStateStats.States[] config,
             @AggregatedPowerStatsConfig.TrackedState int[] stateValues) {
diff --git a/services/core/java/com/android/server/power/stats/WifiPowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/WifiPowerStatsProcessor.java
index a4a2e18..4e035c3 100644
--- a/services/core/java/com/android/server/power/stats/WifiPowerStatsProcessor.java
+++ b/services/core/java/com/android/server/power/stats/WifiPowerStatsProcessor.java
@@ -114,7 +114,7 @@
     }
 
     @Override
-    void finish(PowerComponentAggregatedPowerStats stats) {
+    void finish(PowerComponentAggregatedPowerStats stats, long timestampMs) {
         if (stats.getPowerStatsDescriptor() == null) {
             return;
         }
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index f62c76e..e00e813 100644
--- a/services/core/java/com/android/server/trust/TrustManagerService.java
+++ b/services/core/java/com/android/server/trust/TrustManagerService.java
@@ -29,6 +29,7 @@
 import android.app.admin.DevicePolicyManager;
 import android.app.trust.ITrustListener;
 import android.app.trust.ITrustManager;
+import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -47,6 +48,8 @@
 import android.hardware.biometrics.SensorProperties;
 import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.location.ISignificantPlaceProvider;
+import android.hardware.location.ISignificantPlaceProviderManager;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -83,6 +86,8 @@
 import com.android.internal.util.DumpUtils;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.SystemService;
+import com.android.server.servicewatcher.CurrentUserServiceSupplier;
+import com.android.server.servicewatcher.ServiceWatcher;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -248,6 +253,9 @@
     private boolean mTrustAgentsCanRun = false;
     private int mCurrentUser = UserHandle.USER_SYSTEM;
 
+    private ServiceWatcher mSignificantPlaceServiceWatcher;
+    private volatile boolean mIsInSignificantPlace = false;
+
     /**
      * A class for providing dependencies to {@link TrustManagerService} in both production and test
      * cases.
@@ -310,6 +318,38 @@
             mTrustAgentsCanRun = true;
             refreshAgentList(UserHandle.USER_ALL);
             refreshDeviceLockedForUser(UserHandle.USER_ALL);
+
+            if (android.security.Flags.significantPlaces()) {
+                mSignificantPlaceServiceWatcher = ServiceWatcher.create(mContext, TAG,
+                        CurrentUserServiceSupplier.create(
+                                mContext,
+                                TrustManager.ACTION_BIND_SIGNIFICANT_PLACE_PROVIDER,
+                                null,
+                                null,
+                                null),
+                        new ServiceWatcher.ServiceListener<>() {
+                            @Override
+                            public void onBind(IBinder binder,
+                                    CurrentUserServiceSupplier.BoundServiceInfo service)
+                                    throws RemoteException {
+                                ISignificantPlaceProvider.Stub.asInterface(binder)
+                                        .setSignificantPlaceProviderManager(
+                                                new ISignificantPlaceProviderManager.Stub() {
+                                                    @Override
+                                                    public void setInSignificantPlace(
+                                                            boolean inSignificantPlace) {
+                                                        mIsInSignificantPlace = inSignificantPlace;
+                                                    }
+                                                });
+                            }
+
+                            @Override
+                            public void onUnbind() {
+                                mIsInSignificantPlace = false;
+                            }
+                        });
+                mSignificantPlaceServiceWatcher.register();
+            }
         } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
             maybeEnableFactoryTrustAgents(UserHandle.USER_SYSTEM);
         }
@@ -1651,6 +1691,11 @@
             }
         }
 
+        @Override
+        public boolean isInSignificantPlace() {
+            return mIsInSignificantPlace;
+        }
+
         private void enforceReportPermission() {
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE, "reporting trust events");
@@ -1680,6 +1725,9 @@
                     for (UserInfo user : userInfos) {
                         dumpUser(fout, user, user.id == mCurrentUser);
                     }
+                    if (mSignificantPlaceServiceWatcher != null) {
+                        mSignificantPlaceServiceWatcher.dump(fout);
+                    }
                 }
             }, 1500);
         }
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
index 110100a..8d14c1d 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
@@ -35,6 +35,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManagerInternal;
 import android.os.Binder;
+import android.os.Build;
 import android.os.ParcelFileDescriptor;
 import android.os.PersistableBundle;
 import android.os.RemoteCallback;
@@ -189,6 +190,9 @@
 
     @Override
     protected int getMaximumTemporaryServiceDurationMs() {
+        if (Build.isDebuggable()) {
+            return Integer.MAX_VALUE;
+        }
         return MAX_TEMPORARY_SERVICE_DURATION_MS;
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index af59567..eb6262c 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -315,6 +315,7 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
+import android.graphics.Insets;
 import android.graphics.PixelFormat;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -356,6 +357,7 @@
 import android.view.Surface.Rotation;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
+import android.view.WindowInsets;
 import android.view.WindowInsets.Type;
 import android.view.WindowManager;
 import android.view.WindowManager.LayoutParams;
@@ -2554,7 +2556,7 @@
         }
 
         final WindowState mainWin = findMainWindow(false /* includeStartingApp */);
-        if (mainWin != null && mainWin.mWinAnimator.getShown()) {
+        if (mainWin != null && mainWin.isDrawn()) {
             // App already has a visible window...why would you want a starting window?
             return false;
         }
@@ -8560,6 +8562,13 @@
         if (isFixedOrientationLetterboxAllowed) {
             resolveFixedOrientationConfiguration(newParentConfiguration);
         }
+        // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds
+        // are already calculated in resolveFixedOrientationConfiguration.
+        // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer.
+        if (!isLetterboxedForFixedOrientationAndAspectRatio()
+                && !mLetterboxUiController.hasFullscreenOverride()) {
+            resolveAspectRatioRestriction(newParentConfiguration);
+        }
         final CompatDisplayInsets compatDisplayInsets = getCompatDisplayInsets();
         if (compatDisplayInsets != null) {
             resolveSizeCompatModeConfiguration(newParentConfiguration, compatDisplayInsets);
@@ -8572,14 +8581,6 @@
             if (!matchParentBounds()) {
                 computeConfigByResolveHint(resolvedConfig, newParentConfiguration);
             }
-        // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds
-        // are already calculated in resolveFixedOrientationConfiguration, or if in size compat
-        // mode, it should already be calculated in resolveSizeCompatModeConfiguration.
-        // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer.
-        }
-        if (!isLetterboxedForFixedOrientationAndAspectRatio() && !mInSizeCompatModeForBounds
-                && !mLetterboxUiController.hasFullscreenOverride()) {
-            resolveAspectRatioRestriction(newParentConfiguration);
         }
 
         if (isFixedOrientationLetterboxAllowed || compatDisplayInsets != null
@@ -8801,7 +8802,7 @@
             return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION;
         }
         // Letterbox for limited aspect ratio.
-        if (mIsAspectRatioApplied) {
+        if (isLetterboxedForAspectRatioOnly()) {
             return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO;
         }
 
@@ -8830,13 +8831,27 @@
         final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds();
         final float screenResolvedBoundsWidth = screenResolvedBounds.width();
         final float parentAppBoundsWidth = parentAppBounds.width();
+        final boolean isImmersiveMode = isImmersiveMode(parentBounds);
+        final Insets navBarInsets;
+        if (isImmersiveMode) {
+            navBarInsets = mDisplayContent.getInsetsStateController()
+                    .getRawInsetsState().calculateInsets(
+                            parentBounds,
+                            WindowInsets.Type.navigationBars(),
+                            true /* ignoreVisibility */);
+        } else {
+            navBarInsets = Insets.NONE;
+        }
         // Horizontal position
         int offsetX = 0;
         if (parentBounds.width() != screenResolvedBoundsWidth) {
             if (screenResolvedBoundsWidth <= parentAppBoundsWidth) {
                 float positionMultiplier = mLetterboxUiController.getHorizontalPositionMultiplier(
                         newParentConfiguration);
-                offsetX = Math.max(0, (int) Math.ceil((parentAppBoundsWidth
+                // If in immersive mode, always align to right and overlap right insets (task bar)
+                // as they are transient and hidden. This removes awkward right spacing.
+                final int appWidth = (int) (parentAppBoundsWidth + navBarInsets.right);
+                offsetX = Math.max(0, (int) Math.ceil((appWidth
                         - screenResolvedBoundsWidth) * positionMultiplier)
                         // This is added to make sure that insets added inside
                         // CompatDisplayInsets#getContainerBounds() do not break the alignment
@@ -8856,9 +8871,8 @@
                         newParentConfiguration);
                 // If in immersive mode, always align to bottom and overlap bottom insets (nav bar,
                 // task bar) as they are transient and hidden. This removes awkward bottom spacing.
-                final float newHeight = mDisplayContent.getDisplayPolicy().isImmersiveMode()
-                        ? parentBoundsHeight : parentAppBoundsHeight;
-                offsetY = Math.max(0, (int) Math.ceil((newHeight
+                final int appHeight = (int) (parentAppBoundsHeight + navBarInsets.bottom);
+                offsetY = Math.max(0, (int) Math.ceil((appHeight
                         - screenResolvedBoundsHeight) * positionMultiplier)
                         // This is added to make sure that insets added inside
                         // CompatDisplayInsets#getContainerBounds() do not break the alignment
@@ -8878,7 +8892,8 @@
 
         // If the top is aligned with parentAppBounds add the vertical insets back so that the app
         // content aligns with the status bar
-        if (resolvedConfig.windowConfiguration.getAppBounds().top == parentAppBounds.top) {
+        if (resolvedConfig.windowConfiguration.getAppBounds().top == parentAppBounds.top
+                && !isImmersiveMode) {
             resolvedConfig.windowConfiguration.getBounds().top = parentBounds.top;
             if (mSizeCompatBounds != null) {
                 mSizeCompatBounds.top = parentBounds.top;
@@ -8901,6 +8916,18 @@
         }
     }
 
+    boolean isImmersiveMode(@NonNull Rect parentBounds) {
+        if (!mResolveConfigHint.mUseOverrideInsetsForConfig) {
+            return false;
+        }
+        final Insets navBarInsets = mDisplayContent.getInsetsStateController()
+                .getRawInsetsState().calculateInsets(
+                        parentBounds,
+                        WindowInsets.Type.navigationBars(),
+                        false /* ignoreVisibility */);
+        return Insets.NONE.equals(navBarInsets);
+    }
+
     @NonNull Rect getScreenResolvedBounds() {
         final Configuration resolvedConfig = getResolvedOverrideConfiguration();
         final Rect resolvedBounds = resolvedConfig.windowConfiguration.getBounds();
@@ -8943,6 +8970,10 @@
         return mLetterboxBoundsForFixedOrientationAndAspectRatio != null;
     }
 
+    boolean isLetterboxedForAspectRatioOnly() {
+        return mLetterboxBoundsForAspectRatio != null;
+    }
+
     boolean isAspectRatioApplied() {
         return mIsAspectRatioApplied;
     }
@@ -9235,11 +9266,11 @@
         // orientation bounds (stored in resolved bounds) instead of parent bounds since the
         // activity will be displayed within them even if it is in size compat mode. They should be
         // saved here before resolved bounds are overridden below.
-        final Rect containerBounds = isLetterboxedForFixedOrientationAndAspectRatio()
+        final Rect containerBounds = isAspectRatioApplied()
                 ? new Rect(resolvedBounds)
                 : newParentConfiguration.windowConfiguration.getBounds();
-        final Rect containerAppBounds = isLetterboxedForFixedOrientationAndAspectRatio()
-                ? new Rect(getResolvedOverrideConfiguration().windowConfiguration.getAppBounds())
+        final Rect containerAppBounds = isAspectRatioApplied()
+                ? new Rect(resolvedConfig.windowConfiguration.getAppBounds())
                 : newParentConfiguration.windowConfiguration.getAppBounds();
 
         final int requestedOrientation = getRequestedConfigurationOrientation();
diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
index 2cccd33..1a9d211 100644
--- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
+++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java
@@ -56,6 +56,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Pair;
+import android.util.Slog;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -75,6 +76,7 @@
  * is no guarantee that other system services are already present.
  */
 class ActivityStartInterceptor {
+    private static final String TAG = "ActivityStartInterceptor";
 
     private final ActivityTaskManagerService mService;
     private final ActivityTaskSupervisor mSupervisor;
@@ -284,6 +286,8 @@
         if (!mUserManager.isQuietModeEnabled(UserHandle.of(mUserId))) {
             return false;
         }
+        Slog.i(TAG, "Intent : " + mIntent + " intercepted for user: " + mUserId
+                + " because quiet mode is enabled.");
 
         IntentSender target = createIntentSenderForOriginalIntent(mCallingUid,
                 FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT);
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index f06d3af..a10f7e7 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -1519,7 +1519,10 @@
         }
 
         try {
-            if ((flags & ActivityManager.MOVE_TASK_NO_USER_ACTION) == 0) {
+            // We allow enter PiP for previous front task if not requested otherwise via options.
+            boolean shouldCauseEnterPip = options == null
+                    || !options.disallowEnterPictureInPictureWhileLaunching();
+            if ((flags & ActivityManager.MOVE_TASK_NO_USER_ACTION) == 0 && shouldCauseEnterPip) {
                 mUserLeaving = true;
             }
 
diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java
index a9450c4..eb85c1a 100644
--- a/services/core/java/com/android/server/wm/AsyncRotationController.java
+++ b/services/core/java/com/android/server/wm/AsyncRotationController.java
@@ -505,10 +505,10 @@
      */
     boolean shouldFreezeInsetsPosition(WindowState w) {
         // Non-change transition (OP_APP_SWITCH) and METHOD_BLAST don't use screenshot so the
-        // insets should keep original position before the start transaction is applied.
+        // insets should keep original position before the window is done with new rotation.
         return mTransitionOp != OP_LEGACY && (isSeamlessTransition()
                 || TransitionController.SYNC_METHOD == BLASTSyncEngine.METHOD_BLAST)
-                && !mIsStartTransactionCommitted && canBeAsync(w.mToken) && isTargetToken(w.mToken);
+                && canBeAsync(w.mToken) && isTargetToken(w.mToken);
     }
 
     /** Returns true if there won't be a screen rotation animation (screenshot-based). */
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index 1128328..50ac801 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -18,8 +18,8 @@
 
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.LaunchParamsModifierUtils.applyLayoutGravity;
-import static com.android.server.wm.LaunchParamsModifierUtils.calculateLayoutBounds;
+import static com.android.server.wm.LaunchParamsUtil.applyLayoutGravity;
+import static com.android.server.wm.LaunchParamsUtil.calculateLayoutBounds;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 84dadab..d0086aa 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -145,6 +145,7 @@
 import com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs;
 import com.android.server.statusbar.StatusBarManagerInternal;
 import com.android.server.wallpaper.WallpaperManagerInternal;
+import com.android.wm.shell.Flags;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -1805,7 +1806,8 @@
         updateConfigurationAndScreenSizeDependentBehaviors();
 
         final boolean shouldAttach =
-                res.getBoolean(R.bool.config_attachNavBarToAppDuringTransition);
+                res.getBoolean(R.bool.config_attachNavBarToAppDuringTransition)
+                        && !Flags.enableTinyTaskbar();
         if (mShouldAttachNavBarToAppDuringTransition != shouldAttach) {
             mShouldAttachNavBarToAppDuringTransition = shouldAttach;
         }
diff --git a/services/core/java/com/android/server/wm/LaunchParamsModifierUtils.java b/services/core/java/com/android/server/wm/LaunchParamsModifierUtils.java
deleted file mode 100644
index db54647..0000000
--- a/services/core/java/com/android/server/wm/LaunchParamsModifierUtils.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.pm.ActivityInfo;
-import android.graphics.Rect;
-import android.util.Size;
-import android.view.Gravity;
-
-class LaunchParamsModifierUtils {
-
-    /**
-     * Calculates bounds based on window layout size manifest values. These can include width,
-     * height, width fraction and height fraction. In the event only one dimension of values are
-     * specified in the manifest (e.g. width but no height value), the corresponding display area
-     * dimension will be used as the default value unless some desired sizes have been specified.
-     */
-    static void calculateLayoutBounds(@NonNull Rect stableBounds,
-            @NonNull ActivityInfo.WindowLayout windowLayout, @NonNull Rect inOutBounds,
-            @Nullable Size desiredSize) {
-        final int defaultWidth = stableBounds.width();
-        final int defaultHeight = stableBounds.height();
-        int width;
-        int height;
-
-        if (desiredSize == null) {
-            // If desired bounds have not been specified, use the exiting default bounds as the
-            // desired.
-            desiredSize = new Size(stableBounds.width(), stableBounds.height());
-        }
-
-        width = desiredSize.getWidth();
-        if (windowLayout.width > 0 && windowLayout.width < defaultWidth) {
-            width = windowLayout.width;
-        } else if (windowLayout.widthFraction > 0 && windowLayout.widthFraction < 1.0f) {
-            width = (int) (defaultWidth * windowLayout.widthFraction);
-        }
-
-        height = desiredSize.getHeight();
-        if (windowLayout.height > 0 && windowLayout.height < defaultHeight) {
-            height = windowLayout.height;
-        } else if (windowLayout.heightFraction > 0 && windowLayout.heightFraction < 1.0f) {
-            height = (int) (defaultHeight * windowLayout.heightFraction);
-        }
-
-        inOutBounds.set(0, 0, width, height);
-    }
-
-    /**
-     * Applies a vertical and horizontal gravity on the inOutBounds in relation to the stableBounds.
-     */
-    static void applyLayoutGravity(int verticalGravity, int horizontalGravity,
-            @NonNull Rect inOutBounds, @NonNull Rect stableBounds) {
-        final int width = inOutBounds.width();
-        final int height = inOutBounds.height();
-
-        final float fractionOfHorizontalOffset;
-        switch (horizontalGravity) {
-            case Gravity.LEFT:
-                fractionOfHorizontalOffset = 0f;
-                break;
-            case Gravity.RIGHT:
-                fractionOfHorizontalOffset = 1f;
-                break;
-            default:
-                fractionOfHorizontalOffset = 0.5f;
-        }
-
-        final float fractionOfVerticalOffset;
-        switch (verticalGravity) {
-            case Gravity.TOP:
-                fractionOfVerticalOffset = 0f;
-                break;
-            case Gravity.BOTTOM:
-                fractionOfVerticalOffset = 1f;
-                break;
-            default:
-                fractionOfVerticalOffset = 0.5f;
-        }
-
-        inOutBounds.offsetTo(stableBounds.left, stableBounds.top);
-        final int xOffset = (int) (fractionOfHorizontalOffset * (stableBounds.width() - width));
-        final int yOffset = (int) (fractionOfVerticalOffset * (stableBounds.height() - height));
-        inOutBounds.offset(xOffset, yOffset);
-    }
-}
diff --git a/services/core/java/com/android/server/wm/LaunchParamsUtil.java b/services/core/java/com/android/server/wm/LaunchParamsUtil.java
index cd071af..9416daf 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsUtil.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsUtil.java
@@ -23,9 +23,11 @@
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.pm.ActivityInfo;
 import android.graphics.Rect;
 import android.util.Size;
+import android.view.Gravity;
 import android.view.View;
 
 /**
@@ -195,4 +197,79 @@
         }
         inOutBounds.offset(dx, dy);
     }
+
+    /**
+     * Calculates bounds based on window layout size manifest values. These can include width,
+     * height, width fraction and height fraction. In the event only one dimension of values are
+     * specified in the manifest (e.g. width but no height value), the corresponding display area
+     * dimension will be used as the default value unless some desired sizes have been specified.
+     */
+    static void calculateLayoutBounds(@NonNull Rect stableBounds,
+            @NonNull ActivityInfo.WindowLayout windowLayout, @NonNull Rect inOutBounds,
+            @Nullable Size desiredSize) {
+        final int defaultWidth = stableBounds.width();
+        final int defaultHeight = stableBounds.height();
+        int width;
+        int height;
+
+        if (desiredSize == null) {
+            // If desired bounds have not been specified, use the exiting default bounds as the
+            // desired.
+            desiredSize = new Size(stableBounds.width(), stableBounds.height());
+        }
+
+        width = desiredSize.getWidth();
+        if (windowLayout.width > 0 && windowLayout.width < defaultWidth) {
+            width = windowLayout.width;
+        } else if (windowLayout.widthFraction > 0 && windowLayout.widthFraction < 1.0f) {
+            width = (int) (defaultWidth * windowLayout.widthFraction);
+        }
+
+        height = desiredSize.getHeight();
+        if (windowLayout.height > 0 && windowLayout.height < defaultHeight) {
+            height = windowLayout.height;
+        } else if (windowLayout.heightFraction > 0 && windowLayout.heightFraction < 1.0f) {
+            height = (int) (defaultHeight * windowLayout.heightFraction);
+        }
+
+        inOutBounds.set(0, 0, width, height);
+    }
+
+    /**
+     * Applies a vertical and horizontal gravity on the inOutBounds in relation to the stableBounds.
+     */
+    static void applyLayoutGravity(int verticalGravity, int horizontalGravity,
+            @NonNull Rect inOutBounds, @NonNull Rect stableBounds) {
+        final int width = inOutBounds.width();
+        final int height = inOutBounds.height();
+
+        final float fractionOfHorizontalOffset;
+        switch (horizontalGravity) {
+            case Gravity.LEFT:
+                fractionOfHorizontalOffset = 0f;
+                break;
+            case Gravity.RIGHT:
+                fractionOfHorizontalOffset = 1f;
+                break;
+            default:
+                fractionOfHorizontalOffset = 0.5f;
+        }
+
+        final float fractionOfVerticalOffset;
+        switch (verticalGravity) {
+            case Gravity.TOP:
+                fractionOfVerticalOffset = 0f;
+                break;
+            case Gravity.BOTTOM:
+                fractionOfVerticalOffset = 1f;
+                break;
+            default:
+                fractionOfVerticalOffset = 0.5f;
+        }
+
+        inOutBounds.offsetTo(stableBounds.left, stableBounds.top);
+        final int xOffset = (int) (fractionOfHorizontalOffset * (stableBounds.width() - width));
+        final int yOffset = (int) (fractionOfVerticalOffset * (stableBounds.height() - height));
+        inOutBounds.offset(xOffset, yOffset);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 6e11e08..b8d0694 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -1704,7 +1704,7 @@
         if (mainWin.isLetterboxedForDisplayCutout()) {
             return "DISPLAY_CUTOUT";
         }
-        if (mActivityRecord.isAspectRatioApplied()) {
+        if (mActivityRecord.isLetterboxedForAspectRatioOnly()) {
             return "ASPECT_RATIO";
         }
         return "UNKNOWN_REASON";
diff --git a/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
index 3606a34..69be0d9 100644
--- a/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
+++ b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java
@@ -16,7 +16,7 @@
 
 package com.android.server.wm;
 
-import static android.tracing.perfetto.DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT;
+import static android.tracing.perfetto.DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP;
 
 import android.annotation.NonNull;
 import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellTransition;
@@ -44,7 +44,7 @@
         DataSourceParams params =
                 new DataSourceParams.Builder()
                         .setBufferExhaustedPolicy(
-                                PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT)
+                                PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP)
                         .build();
         mDataSource.register(params);
     }
diff --git a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
index 1007357..b7944d3 100644
--- a/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
+++ b/services/core/java/com/android/server/wm/SystemGesturesPointerEventListener.java
@@ -107,11 +107,12 @@
 
     void onConfigurationChanged() {
         final Resources r = mContext.getResources();
-        final int defaultThreshold = r.getDimensionPixelSize(
+        final int startThreshold = r.getDimensionPixelSize(
                 com.android.internal.R.dimen.system_gestures_start_threshold);
-        mSwipeStartThreshold.set(defaultThreshold, defaultThreshold, defaultThreshold,
-                defaultThreshold);
-        mSwipeDistanceThreshold = defaultThreshold;
+        mSwipeStartThreshold.set(startThreshold, startThreshold, startThreshold,
+                startThreshold);
+        mSwipeDistanceThreshold = r.getDimensionPixelSize(
+                com.android.internal.R.dimen.system_gestures_distance_threshold);
 
         final Display display = DisplayManagerGlobal.getInstance()
                 .getRealDisplay(Display.DEFAULT_DISPLAY);
diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
index f37638f..5c9a84d 100644
--- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
@@ -40,8 +40,6 @@
 import static com.android.server.wm.ActivityStarter.Request;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.LaunchParamsModifierUtils.applyLayoutGravity;
-import static com.android.server.wm.LaunchParamsModifierUtils.calculateLayoutBounds;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -644,13 +642,13 @@
         displayArea.getStableRect(stableBounds);
 
         if (windowLayout.hasSpecifiedSize()) {
-            calculateLayoutBounds(stableBounds, windowLayout, inOutBounds,
+            LaunchParamsUtil.calculateLayoutBounds(stableBounds, windowLayout, inOutBounds,
                     /* desiredBounds */ null);
         } else if (inOutBounds.isEmpty()) {
             getTaskBounds(root, displayArea, windowLayout, WINDOWING_MODE_FREEFORM,
                     /* hasInitialBounds */ false, inOutBounds);
         }
-        applyLayoutGravity(verticalGravity, horizontalGravity, inOutBounds,
+        LaunchParamsUtil.applyLayoutGravity(verticalGravity, horizontalGravity, inOutBounds,
                 stableBounds);
     }
 
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 2dc439d..0812323 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -113,10 +113,9 @@
     private static final int LEGACY_STATE_READY = 1;
     private static final int LEGACY_STATE_RUNNING = 2;
 
-    private ITransitionPlayer mTransitionPlayer;
+    private final ArrayList<TransitionPlayerRecord> mTransitionPlayers = new ArrayList<>();
     final TransitionMetricsReporter mTransitionMetricsReporter = new TransitionMetricsReporter();
 
-    private WindowProcessController mTransitionPlayerProc;
     final ActivityTaskManagerService mAtm;
     BLASTSyncEngine mSyncEngine;
 
@@ -175,8 +174,6 @@
 
     final Lock mRunningLock = new Lock();
 
-    private final IBinder.DeathRecipient mTransitionPlayerDeath;
-
     static class QueuedTransition {
         final Transition mTransition;
         final OnStartCollect mOnStartCollect;
@@ -243,11 +240,6 @@
     TransitionController(ActivityTaskManagerService atm) {
         mAtm = atm;
         mRemotePlayer = new RemotePlayer(atm);
-        mTransitionPlayerDeath = () -> {
-            synchronized (mAtm.mGlobalLock) {
-                detachPlayer();
-            }
-        };
     }
 
     void setWindowManager(WindowManagerService wms) {
@@ -266,11 +258,10 @@
         mSyncEngine.addOnIdleListener(this::tryStartCollectFromQueue);
     }
 
-    @VisibleForTesting
-    void detachPlayer() {
-        if (mTransitionPlayer == null) return;
-        // Immediately set to null so that nothing inadvertently starts/queues.
-        mTransitionPlayer = null;
+    void flushRunningTransitions() {
+        // Temporarily clear so that nothing gets started/queued while flushing
+        final ArrayList<TransitionPlayerRecord> temp = new ArrayList<>(mTransitionPlayers);
+        mTransitionPlayers.clear();
         // Clean-up/finish any playing transitions. Backwards since they can remove themselves.
         for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
             mPlayingTransitions.get(i).cleanUpOnFailure();
@@ -285,9 +276,10 @@
         if (mCollectingTransition != null) {
             mCollectingTransition.abort();
         }
-        mTransitionPlayerProc = null;
         mRemotePlayer.clear();
         mRunningLock.doNotifyLocked();
+        // Restore the rest of the player stack
+        mTransitionPlayers.addAll(temp);
     }
 
     /** @see #createTransition(int, int) */
@@ -302,7 +294,7 @@
     @NonNull
     Transition createTransition(@WindowManager.TransitionType int type,
             @WindowManager.TransitionFlags int flags) {
-        if (mTransitionPlayer == null) {
+        if (mTransitionPlayers.isEmpty()) {
             throw new IllegalStateException("Shell Transitions not enabled");
         }
         if (mCollectingTransition != null) {
@@ -321,7 +313,7 @@
         if (mCollectingTransition != null) {
             throw new IllegalStateException("Simultaneous transition collection not supported.");
         }
-        if (mTransitionPlayer == null) {
+        if (mTransitionPlayers.isEmpty()) {
             // If sysui has been killed (by a test) or crashed, we can temporarily have no player
             // In this case, abort the transition.
             transition.abort();
@@ -339,30 +331,56 @@
 
     void registerTransitionPlayer(@Nullable ITransitionPlayer player,
             @Nullable WindowProcessController playerProc) {
-        try {
-            // Note: asBinder() can be null if player is same process (likely in a test).
-            if (mTransitionPlayer != null) {
-                if (mTransitionPlayer.asBinder() != null) {
-                    mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0);
-                }
-                detachPlayer();
-            }
-            if (player.asBinder() != null) {
-                player.asBinder().linkToDeath(mTransitionPlayerDeath, 0);
-            }
-            mTransitionPlayer = player;
-            mTransitionPlayerProc = playerProc;
-        } catch (RemoteException e) {
-            throw new RuntimeException("Unable to set transition player");
+        if (!mTransitionPlayers.isEmpty()) {
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Registering transition "
+                    + "player %s over %d other players", player.asBinder(),
+                    mTransitionPlayers.size());
+            // flush currently running transitions so that the new player doesn't get
+            // intermediate state
+            flushRunningTransitions();
+        } else {
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Registering transition "
+                    + "player %s ", player.asBinder());
         }
+        mTransitionPlayers.add(new TransitionPlayerRecord(player, playerProc));
+    }
+
+    @VisibleForTesting
+    void unregisterTransitionPlayer(@NonNull ITransitionPlayer player) {
+        int idx = mTransitionPlayers.size() - 1;
+        for (; idx >= 0; --idx) {
+            if (mTransitionPlayers.get(idx).mPlayer.asBinder() == player.asBinder()) break;
+        }
+        if (idx < 0) {
+            ProtoLog.w(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Attempt to unregister "
+                    + "transition player %s but it isn't registered", player.asBinder());
+            return;
+        }
+        final boolean needsFlush = idx == (mTransitionPlayers.size() - 1);
+        final TransitionPlayerRecord record = mTransitionPlayers.remove(idx);
+        if (needsFlush) {
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Unregistering active "
+                    + "transition player %s at index=%d leaving %d in stack", player.asBinder(),
+                    idx, mTransitionPlayers.size());
+        } else {
+            ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Unregistering transition "
+                    + "player %s at index=%d leaving %d in stack", player.asBinder(), idx,
+                    mTransitionPlayers.size());
+        }
+        record.unlinkToDeath();
+        if (!needsFlush) {
+            // Not the active player, so no need to flush transitions.
+            return;
+        }
+        flushRunningTransitions();
     }
 
     @Nullable ITransitionPlayer getTransitionPlayer() {
-        return mTransitionPlayer;
+        return mTransitionPlayers.isEmpty() ? null : mTransitionPlayers.getLast().mPlayer;
     }
 
     boolean isShellTransitionsEnabled() {
-        return mTransitionPlayer != null;
+        return !mTransitionPlayers.isEmpty();
     }
 
     /** @return {@code true} if using shell-transitions rotation instead of fixed-rotation. */
@@ -677,7 +695,7 @@
     Transition requestTransitionIfNeeded(@WindowManager.TransitionType int type,
             @WindowManager.TransitionFlags int flags, @Nullable WindowContainer trigger,
             @NonNull WindowContainer readyGroupRef) {
-        if (mTransitionPlayer == null) {
+        if (mTransitionPlayers.isEmpty()) {
             return null;
         }
         Transition newTransition = null;
@@ -728,7 +746,7 @@
                     transition.getToken(), null));
             return transition;
         }
-        if (mTransitionPlayer == null || transition.isAborted()) {
+        if (mTransitionPlayers.isEmpty() || transition.isAborted()) {
             // Apparently, some tests will kill(and restart) systemui, so there is a chance that
             // the player might be transiently null.
             if (transition.isCollecting()) {
@@ -757,7 +775,8 @@
 
             transition.mLogger.mRequestTimeNs = SystemClock.elapsedRealtimeNanos();
             transition.mLogger.mRequest = request;
-            mTransitionPlayer.requestStartTransition(transition.getToken(), request);
+            mTransitionPlayers.getLast().mPlayer.requestStartTransition(
+                    transition.getToken(), request);
             if (remoteTransition != null) {
                 transition.setRemoteAnimationApp(remoteTransition.getAppThread());
             }
@@ -773,7 +792,7 @@
      * @return the new transition if it was created for this request, `null` otherwise.
      */
     Transition requestCloseTransitionIfNeeded(@NonNull WindowContainer<?> wc) {
-        if (mTransitionPlayer == null || isCollecting()) return null;
+        if (mTransitionPlayers.isEmpty() || isCollecting()) return null;
         if (!wc.isVisibleRequested()) return null;
         return requestStartTransition(createTransition(TRANSIT_CLOSE, 0 /* flags */), wc.asTask(),
                 null /* remoteTransition */, null /* displayChange */);
@@ -1250,11 +1269,13 @@
 
     /** Updates the process state of animation player. */
     private void updateRunningRemoteAnimation(Transition transition, boolean isPlaying) {
-        if (mTransitionPlayerProc == null) return;
+        if (mTransitionPlayers.isEmpty()) return;
+        final TransitionPlayerRecord record = mTransitionPlayers.getLast();
+        if (record.mPlayerProc == null) return;
         if (isPlaying) {
-            mTransitionPlayerProc.setRunningRemoteAnimation(true);
+            record.mPlayerProc.setRunningRemoteAnimation(true);
         } else if (mPlayingTransitions.isEmpty()) {
-            mTransitionPlayerProc.setRunningRemoteAnimation(false);
+            record.mPlayerProc.setRunningRemoteAnimation(false);
             mRemotePlayer.clear();
         }
     }
@@ -1416,7 +1437,7 @@
      */
     @NonNull
     Transition createAndStartCollecting(int type) {
-        if (mTransitionPlayer == null) {
+        if (mTransitionPlayers.isEmpty()) {
             return null;
         }
         if (!mQueuedTransitions.isEmpty()) {
@@ -1477,6 +1498,40 @@
         void onCollectStarted(boolean deferred);
     }
 
+    private class TransitionPlayerRecord {
+        final ITransitionPlayer mPlayer;
+        IBinder.DeathRecipient mDeath = null;
+        private WindowProcessController mPlayerProc;
+
+        TransitionPlayerRecord(@NonNull ITransitionPlayer player,
+                @Nullable WindowProcessController playerProc) {
+            mPlayer = player;
+            mPlayerProc = playerProc;
+            try {
+                linkToDeath();
+            } catch (RemoteException e) {
+                throw new RuntimeException("Unable to set transition player");
+            }
+        }
+
+        private void linkToDeath() throws RemoteException {
+            // Note: asBinder() can be null if player is same process (likely in a test).
+            if (mPlayer.asBinder() == null) return;
+            mDeath = () -> {
+                synchronized (mAtm.mGlobalLock) {
+                    unregisterTransitionPlayer(mPlayer);
+                }
+            };
+            mPlayer.asBinder().linkToDeath(mDeath, 0);
+        }
+
+        void unlinkToDeath() {
+            if (mPlayer.asBinder() == null || mDeath == null) return;
+            mPlayer.asBinder().unlinkToDeath(mDeath, 0);
+            mDeath = null;
+        }
+    }
+
     /**
      * This manages the animating state of processes that are running remote animations for
      * {@link #mTransitionPlayerProc}.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index b603551..0bf1c88 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -8619,8 +8619,8 @@
                     return true;
                 }
                 // For a task session, find the activity identified by the launch cookie.
-                final WindowContainerInfo wci = getTaskWindowContainerInfoForLaunchCookie(
-                        incomingSession.getTokenToRecord());
+                final WindowContainerInfo wci =
+                        getTaskWindowContainerInfoForRecordingSession(incomingSession);
                 if (wci == null) {
                     Slog.w(TAG, "Handling a new recording session; unable to find the "
                             + "WindowContainerToken");
@@ -9082,35 +9082,58 @@
     }
 
     /**
-     * Retrieve the {@link WindowContainerInfo} of the task that contains the activity started with
-     * the given launch cookie.
+     * Retrieve the {@link WindowContainerInfo} of the task that was launched for MediaProjection.
      *
-     * @param launchCookie the launch cookie set on the {@link ActivityOptions} when starting an
-     *     activity
+     * @param session the {@link ContentRecordingSession} containing the launch cookie and/or
+     *     task id of the Task started for capture.
      * @return a token representing the task containing the activity started with the given launch
      *     cookie, or {@code null} if the token couldn't be found.
      */
     @VisibleForTesting
     @Nullable
-    WindowContainerInfo getTaskWindowContainerInfoForLaunchCookie(@NonNull IBinder launchCookie) {
-        // Find the activity identified by the launch cookie.
-        final ActivityRecord targetActivity =
-                mRoot.getActivity(activity -> activity.mLaunchCookie == launchCookie);
-        if (targetActivity == null) {
-            Slog.w(TAG, "Unable to find the activity for this launch cookie");
-            return null;
+    WindowContainerInfo getTaskWindowContainerInfoForRecordingSession(
+            @NonNull ContentRecordingSession session) {
+        WindowContainerToken taskWindowContainerToken = null;
+        ActivityRecord targetActivity = null;
+        Task targetTask = null;
+
+        // First attempt to find the launched task by looking for the launched activity with the
+        // matching launch cookie.
+        if (session.getTokenToRecord() != null) {
+            IBinder launchCookie = session.getTokenToRecord();
+            targetActivity = mRoot.getActivity(activity -> activity.mLaunchCookie == launchCookie);
+            if (targetActivity == null) {
+                Slog.w(TAG, "Unable to find the activity for this launch cookie");
+            } else {
+                if (targetActivity.getTask() == null) {
+                    Slog.w(TAG, "Unable to find the task for this launch cookie");
+                } else {
+                    targetTask = targetActivity.getTask();
+                    taskWindowContainerToken = targetTask.mRemoteToken.toWindowContainerToken();
+                }
+            }
         }
-        if (targetActivity.getTask() == null) {
-            Slog.w(TAG, "Unable to find the task for this launch cookie");
-            return null;
+
+        // In the case we can't find an activity with a matching launch cookie, it could be due to
+        // the launched activity being closed, but the launched task is still open, so now attempt
+        // to look for the task directly.
+        if (taskWindowContainerToken == null && session.getTaskId() != -1) {
+            int targetTaskId = session.getTaskId();
+            targetTask = mRoot.getTask(task -> task.isTaskId(targetTaskId));
+            if (targetTask == null) {
+                Slog.w(TAG, "Unable to find the task for this projection");
+            } else {
+                taskWindowContainerToken = targetTask.mRemoteToken.toWindowContainerToken();
+            }
         }
-        WindowContainerToken taskWindowContainerToken =
-                targetActivity.getTask().mRemoteToken.toWindowContainerToken();
+
+        // If we were unable to find the launched task in either fashion, then something must have
+        // wrong (i.e. the task was closed before capture started).
         if (taskWindowContainerToken == null) {
-            Slog.w(TAG, "Unable to find the WindowContainerToken for " + targetActivity.getName());
+            Slog.w(TAG, "Unable to find the WindowContainerToken for ContentRecordingSession");
             return null;
         }
-        return new WindowContainerInfo(targetActivity.getUid(), taskWindowContainerToken);
+        return new WindowContainerInfo(targetTask.effectiveUid, taskWindowContainerToken);
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 99c4736..5e932d4 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -2088,6 +2088,19 @@
     }
 
     @Override
+    public void unregisterTransitionPlayer(ITransitionPlayer player) {
+        enforceTaskPermission("unregisterTransitionPlayer()");
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            synchronized (mGlobalLock) {
+                mTransitionController.unregisterTransitionPlayer(player);
+            }
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    @Override
     public ITransitionMetricsReporter getTransitionMetricsReporter() {
         return mTransitionController.mTransitionMetricsReporter;
     }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
index d733762..e763c9e 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
@@ -16,6 +16,7 @@
 
 package com.android.server.devicepolicy;
 
+import static android.app.admin.DevicePolicyIdentifiers.PACKAGES_SUSPENDED_POLICY;
 import static android.app.admin.DevicePolicyIdentifiers.USER_CONTROL_DISABLED_PACKAGES_POLICY;
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_TARGET_USER_ID;
 import static android.app.admin.PolicyUpdateReceiver.EXTRA_POLICY_UPDATE_RESULT_KEY;
@@ -78,6 +79,10 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -260,9 +265,7 @@
                 boolean policyEnforced = Objects.equals(
                         localPolicyState.getCurrentResolvedPolicy(), value);
                 // TODO(b/285532044): remove hack and handle properly
-                if (!policyEnforced
-                        && policyDefinition.getPolicyKey().getIdentifier().equals(
-                        USER_CONTROL_DISABLED_PACKAGES_POLICY)) {
+                if (!policyEnforced && shouldApplyPackageSetUnionPolicyHack(policyDefinition)) {
                     PolicyValue<Set<String>> parsedValue = (PolicyValue<Set<String>>) value;
                     PolicyValue<Set<String>> parsedResolvedValue =
                             (PolicyValue<Set<String>>) localPolicyState.getCurrentResolvedPolicy();
@@ -528,8 +531,7 @@
                         globalPolicyState.getCurrentResolvedPolicy(), value);
                 // TODO(b/285532044): remove hack and handle properly
                 if (!policyAppliedGlobally
-                        && policyDefinition.getPolicyKey().getIdentifier().equals(
-                        USER_CONTROL_DISABLED_PACKAGES_POLICY)) {
+                        && shouldApplyPackageSetUnionPolicyHack(policyDefinition)) {
                     PolicyValue<Set<String>> parsedValue = (PolicyValue<Set<String>>) value;
                     PolicyValue<Set<String>> parsedResolvedValue =
                             (PolicyValue<Set<String>>) globalPolicyState.getCurrentResolvedPolicy();
@@ -666,8 +668,7 @@
 
             }
             // TODO(b/285532044): remove hack and handle properly
-            if (policyDefinition.getPolicyKey().getIdentifier().equals(
-                    USER_CONTROL_DISABLED_PACKAGES_POLICY)) {
+            if (shouldApplyPackageSetUnionPolicyHack(policyDefinition)) {
                 if (!Objects.equals(value, localPolicyState.getCurrentResolvedPolicy())) {
                     PolicyValue<Set<String>> parsedValue = (PolicyValue<Set<String>>) value;
                     PolicyValue<Set<String>> parsedResolvedValue =
@@ -688,6 +689,12 @@
      */
     @Nullable
     <V> V getResolvedPolicy(@NonNull PolicyDefinition<V> policyDefinition, int userId) {
+        PolicyValue<V> resolvedValue = getResolvedPolicyValue(policyDefinition, userId);
+        return resolvedValue == null ? null : resolvedValue.getValue();
+    }
+
+    private <V> PolicyValue<V> getResolvedPolicyValue(@NonNull PolicyDefinition<V> policyDefinition,
+            int userId) {
         Objects.requireNonNull(policyDefinition);
 
         synchronized (mLock) {
@@ -699,11 +706,39 @@
                 resolvedValue = getGlobalPolicyStateLocked(
                         policyDefinition).getCurrentResolvedPolicy();
             }
-            return resolvedValue == null ? null : resolvedValue.getValue();
+            return resolvedValue;
         }
     }
 
     /**
+     * Retrieves resolved policy for the provided {@code policyDefinition} and a list of
+     * users.
+     */
+    @Nullable
+    <V> V getResolvedPolicyAcrossUsers(@NonNull PolicyDefinition<V> policyDefinition,
+            List<Integer> users) {
+        Objects.requireNonNull(policyDefinition);
+
+        List<PolicyValue<V>> adminPolicies = new ArrayList<>();
+        synchronized (mLock) {
+            for (int userId : users) {
+                PolicyValue<V> resolvedValue = getResolvedPolicyValue(policyDefinition, userId);
+                if (resolvedValue != null) {
+                    adminPolicies.add(resolvedValue);
+                }
+            }
+        }
+        // We will be aggregating PolicyValue across multiple admins across multiple users,
+        // including different policies set by the same admin on different users. This is
+        // not supported by ResolutionMechanism generically, instead we need to call the special
+        // resolve() method that doesn't care about admins who set the policy. Note that not every
+        // ResolutionMechanism supports this.
+        PolicyValue<V> resolvedValue =
+                policyDefinition.getResolutionMechanism().resolve(adminPolicies);
+        return resolvedValue == null ? null : resolvedValue.getValue();
+    }
+
+    /**
      * Retrieves the policy set by the admin for the provided {@code policyDefinition} and
      * {@code userId} if one was set, otherwise returns {@code null}.
      */
@@ -1743,6 +1778,18 @@
         }
     }
 
+    /**
+     * Create a backup of the policy engine XML file, so that we can recover previous state
+     * in case some data-loss bug is triggered e.g. during migration.
+     *
+     * Backup is only created if one with the same ID does not exist yet.
+     */
+    void createBackup(String backupId) {
+        synchronized (mLock) {
+            DevicePoliciesReaderWriter.createBackup(backupId);
+        }
+    }
+
     <V> void reapplyAllPoliciesOnBootLocked() {
         for (PolicyKey policy : mGlobalPolicies.keySet()) {
             PolicyState<?> policyState = mGlobalPolicies.get(policy);
@@ -1820,8 +1867,22 @@
         return false;
     }
 
+    /**
+     * For PackageSetUnion policies, we can't simply compare the resolved policy against the admin's
+     * policy for equality to determine if the admin has applied the policy successfully, instead
+     * the admin's policy should be considered applied successfully as long as its policy is subset
+     * of the resolved policy. This method controls which policies should use this special logic.
+     */
+    private <V> boolean shouldApplyPackageSetUnionPolicyHack(PolicyDefinition<V> policy) {
+        String policyKey =  policy.getPolicyKey().getIdentifier();
+        return policyKey.equals(USER_CONTROL_DISABLED_PACKAGES_POLICY)
+                || policyKey.equals(PACKAGES_SUSPENDED_POLICY);
+    }
+
     private class DevicePoliciesReaderWriter {
         private static final String DEVICE_POLICIES_XML = "device_policy_state.xml";
+        private static final String BACKUP_DIRECTORY = "device_policy_backups";
+        private static final String BACKUP_FILENAME = "device_policy_state.%s.xml";
         private static final String TAG_LOCAL_POLICY_ENTRY = "local-policy-entry";
         private static final String TAG_GLOBAL_POLICY_ENTRY = "global-policy-entry";
         private static final String TAG_POLICY_STATE_ENTRY = "policy-state-entry";
@@ -1836,8 +1897,30 @@
 
         private final File mFile;
 
+        private static File getFileName() {
+            return new File(Environment.getDataSystemDirectory(), DEVICE_POLICIES_XML);
+        }
         private DevicePoliciesReaderWriter() {
-            mFile = new File(Environment.getDataSystemDirectory(), DEVICE_POLICIES_XML);
+            mFile = getFileName();
+        }
+
+        public static void createBackup(String backupId) {
+            try {
+                File backupDirectory = new File(Environment.getDataSystemDirectory(),
+                        BACKUP_DIRECTORY);
+                backupDirectory.mkdir();
+                Path backupPath = Path.of(backupDirectory.getPath(),
+                        BACKUP_FILENAME.formatted(backupId));
+                if (backupPath.toFile().exists()) {
+                    Log.w(TAG, "Backup already exist: " + backupPath);
+                } else {
+                    Files.copy(getFileName().toPath(), backupPath,
+                            StandardCopyOption.REPLACE_EXISTING);
+                    Log.i(TAG, "Backup created at " + backupPath);
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "Cannot create backup " + backupId, e);
+            }
         }
 
         void writeToFileLocked() {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 5bf5efd..bdd0730 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -569,6 +569,7 @@
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -2221,32 +2222,6 @@
         return packageNameAndSignature;
     }
 
-    private void unsuspendAppsForQuietProfiles() {
-        PackageManagerInternal pmi = mInjector.getPackageManagerInternal();
-        List<UserInfo> users = mUserManagerInternal.getUsers(true /* excludeDying */);
-
-        for (UserInfo user : users) {
-            if (!user.isManagedProfile() || !user.isQuietModeEnabled()) {
-                continue;
-            }
-            int userId = user.id;
-            var suspendedByAdmin = getPackagesSuspendedByAdmin(userId);
-            var packagesToUnsuspend = mInjector.getPackageManager(userId)
-                    .getInstalledPackages(PackageManager.PackageInfoFlags.of(
-                            MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE))
-                    .stream()
-                    .map(packageInfo -> packageInfo.packageName)
-                    .filter(pkg -> !suspendedByAdmin.contains(pkg))
-                    .toArray(String[]::new);
-
-            Slogf.i(LOG_TAG, "Unsuspending work apps for user %d", userId);
-            // When app suspension was used for quiet mode, the apps were suspended by platform
-            // package, just like when admin suspends them. So although it wasn't admin who
-            // suspended, this method will remove the right suspension record.
-            pmi.setPackagesSuspendedByAdmin(userId, packagesToUnsuspend, false /* suspended */);
-        }
-    }
-
     private Owners makeOwners(Injector injector, PolicyPathProvider pathProvider) {
         return new Owners(
                 injector.getUserManager(), injector.getUserManagerInternal(),
@@ -3470,6 +3445,79 @@
                 new BooleanPolicyValue(true));
     }
 
+    @GuardedBy("getLockObject()")
+    private boolean maybeMigrateRequiredPasswordComplexityLocked(String backupId) {
+        Slog.i(LOG_TAG, "Migrating password complexity to policy engine");
+        if (!Flags.unmanagedModeMigration()) {
+            return false;
+        }
+        if (mOwners.isRequiredPasswordComplexityMigrated()) {
+            return false;
+        }
+        // Create backup if none exists
+        mDevicePolicyEngine.createBackup(backupId);
+        try {
+            iterateThroughDpcAdminsLocked((admin, enforcingAdmin) -> {
+                int userId = enforcingAdmin.getUserId();
+                if (admin.mPasswordComplexity != PASSWORD_COMPLEXITY_NONE) {
+                    mDevicePolicyEngine.setLocalPolicy(
+                            PolicyDefinition.PASSWORD_COMPLEXITY,
+                            enforcingAdmin,
+                            new IntegerPolicyValue(admin.mPasswordComplexity),
+                            userId);
+                }
+                ActiveAdmin parentAdmin = admin.getParentActiveAdmin();
+                if (parentAdmin != null
+                        && parentAdmin.mPasswordComplexity != PASSWORD_COMPLEXITY_NONE) {
+                    mDevicePolicyEngine.setLocalPolicy(
+                            PolicyDefinition.PASSWORD_COMPLEXITY,
+                            enforcingAdmin,
+                            new IntegerPolicyValue(parentAdmin.mPasswordComplexity),
+                            getProfileParentId(userId));
+                }
+            });
+        } catch (Exception e) {
+            Slog.wtf(LOG_TAG, "Failed to migrate password complexity to policy engine", e);
+        }
+
+        Slog.i(LOG_TAG, "Marking password complexity migration complete");
+        mOwners.markRequiredPasswordComplexityMigrated();
+        return true;
+    }
+
+    @GuardedBy("getLockObject()")
+    private boolean maybeMigrateSuspendedPackagesLocked(String backupId) {
+        Slog.i(LOG_TAG, "Migrating suspended packages to policy engine");
+        if (!Flags.unmanagedModeMigration()) {
+            return false;
+        }
+        if (mOwners.isSuspendedPackagesMigrated()) {
+            return false;
+        }
+        // Create backup if none exists
+        mDevicePolicyEngine.createBackup(backupId);
+        try {
+            iterateThroughDpcAdminsLocked((admin, enforcingAdmin) -> {
+                if (admin.suspendedPackages == null || admin.suspendedPackages.size() == 0) {
+                    return;
+                }
+                int userId = enforcingAdmin.getUserId();
+                mDevicePolicyEngine.setLocalPolicy(
+                        PolicyDefinition.PACKAGES_SUSPENDED,
+                        enforcingAdmin,
+                        new PackageSetPolicyValue(new ArraySet<>(admin.suspendedPackages)),
+                        userId);
+            });
+        } catch (Exception e) {
+            Slog.wtf(LOG_TAG, "Failed to migrate suspended packages to policy engine", e);
+        }
+
+        Slog.i(LOG_TAG, "Marking suspended packages migration complete");
+        mOwners.markSuspendedPackagesMigrated();
+        return true;
+    }
+
+
     private void applyManagedSubscriptionsPolicyIfRequired() {
         int copeProfileUserId = getOrganizationOwnedProfileUserId();
         // This policy is relevant only for COPE devices.
@@ -4254,27 +4302,50 @@
         }
 
         final int userId = caller.getUserId();
+        EnforcingAdmin enforcingAdmin = null;
         synchronized (getLockObject()) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(
                     who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, parent);
 
+            if (Flags.unmanagedModeMigration()) {
+                enforcingAdmin = EnforcingAdmin.createEnterpriseEnforcingAdmin(who,
+                        userId,
+                        getActiveAdminForCallerLocked(who,
+                                DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD));
+            }
             // If setPasswordQuality is called on the parent, ensure that
             // the primary admin does not have password complexity state (this is an
             // unsupported state).
             if (parent) {
-                final ActiveAdmin primaryAdmin = getActiveAdminForCallerLocked(
-                        who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, false);
-                final boolean hasComplexitySet =
-                        primaryAdmin.mPasswordComplexity != PASSWORD_COMPLEXITY_NONE;
+                final boolean hasComplexitySet;
+                if (Flags.unmanagedModeMigration()) {
+                    Integer complexity = mDevicePolicyEngine.getLocalPolicySetByAdmin(
+                            PolicyDefinition.PASSWORD_COMPLEXITY,
+                            enforcingAdmin,
+                            userId);
+                    hasComplexitySet = complexity != null && complexity != PASSWORD_COMPLEXITY_NONE;
+                } else {
+                    final ActiveAdmin primaryAdmin = getActiveAdminForCallerLocked(
+                            who, DeviceAdminInfo.USES_POLICY_LIMIT_PASSWORD, false);
+                    hasComplexitySet = primaryAdmin.mPasswordComplexity != PASSWORD_COMPLEXITY_NONE;
+                }
+
                 Preconditions.checkState(!hasComplexitySet,
                         "Cannot set password quality when complexity is set on the primary admin."
                         + " Set the primary admin's complexity to NONE first.");
             }
+            final EnforcingAdmin enforcingAdminFinal = enforcingAdmin;
             mInjector.binderWithCleanCallingIdentity(() -> {
                 final PasswordPolicy passwordPolicy = ap.mPasswordPolicy;
                 if (passwordPolicy.quality != quality) {
                     passwordPolicy.quality = quality;
-                    ap.mPasswordComplexity = PASSWORD_COMPLEXITY_NONE;
+                    if (Flags.unmanagedModeMigration()) {
+                        int affectedUser = parent ? getProfileParentId(userId) : userId;
+                        mDevicePolicyEngine.removeLocalPolicy(PolicyDefinition.PASSWORD_COMPLEXITY,
+                                enforcingAdminFinal, affectedUser);
+                    } else {
+                        ap.mPasswordComplexity = PASSWORD_COMPLEXITY_NONE;
+                    }
                     resetInactivePasswordRequirementsIfRPlus(userId, ap);
                     updatePasswordValidityCheckpointLocked(userId, parent);
                     updatePasswordQualityCacheForUserGroup(userId);
@@ -4405,6 +4476,10 @@
         }
     }
 
+    /**
+     * @deprecated  use {@link #getResolvedLockscreenPolicy(PolicyDefinition, int)} for
+     * coexistable policies
+     */
     @GuardedBy("getLockObject()")
     private List<ActiveAdmin> getActiveAdminsForLockscreenPoliciesLocked(int userHandle) {
         if (isSeparateProfileChallengeEnabled(userHandle)) {
@@ -4428,6 +4503,25 @@
     }
 
     /**
+     * Returns a user's resolved lockscreen policy from all admins. This is different from normal
+     * policy resolution because if the specified user has a work profile with unified challenge,
+     * all policies set on the profile will also affect that user.
+     */
+    private <V> V getResolvedLockscreenPolicy(PolicyDefinition<V> policyDefinition, int userId) {
+        if (isSeparateProfileChallengeEnabled(userId)) {
+            // If this profile has a separate challenge, only return policies targeting itself.
+            return mDevicePolicyEngine.getResolvedPolicy(policyDefinition, userId);
+        }
+        // Otherwise, this user is either a full user, or it's a profile with unified challenge.
+        // In both cases we query the parent user who owns the credential (the parent user of a full
+        // user is itself), plus any profile of the parent user who has unified challenge since
+        // the policy of a unified challenge profile is enforced on the parent.
+        return getResolvedPolicyForUserAndItsManagedProfiles(policyDefinition,
+                getProfileParentId(userId),
+                (user) -> mLockPatternUtils.isProfileWithUnifiedChallenge(user.id));
+
+    }
+    /**
      * Get the list of active admins for an affected user:
      * <ul>
      * <li>The active admins associated with the userHandle itself</li>
@@ -4460,6 +4554,9 @@
      * profile associated with the given user. Optionally also include the admin of each managed
      * profile.
      * <p> Should not be called on a profile user.
+     *
+     * For coexistable policy, please use
+     * {@link #getResolvedPolicyForUserAndItsManagedProfiles(PolicyDefinition, int, Predicate)}
      */
     @GuardedBy("getLockObject()")
     private List<ActiveAdmin> getActiveAdminsForUserAndItsManagedProfilesLocked(int userHandle,
@@ -4486,6 +4583,23 @@
         return admins;
     }
 
+    private <V> V getResolvedPolicyForUserAndItsManagedProfiles(
+            PolicyDefinition<V> policyDefinition, int userHandle,
+            Predicate<UserInfo> shouldIncludeProfile) {
+        List<Integer> users = new ArrayList<>();
+
+        mInjector.binderWithCleanCallingIdentity(() -> {
+            for (UserInfo userInfo : mUserManager.getProfiles(userHandle)) {
+                if (userInfo.id == userHandle) {
+                    users.add(userInfo.id);
+                } else if (userInfo.isManagedProfile() && shouldIncludeProfile.test(userInfo)) {
+                    users.add(userInfo.id);
+                }
+            }
+        });
+        return mDevicePolicyEngine.getResolvedPolicyAcrossUsers(policyDefinition, users);
+    }
+
     /**
      * Returns the list of admins on the given user, as well as parent admins for each managed
      * profile associated with the given user. Optionally also include the admin of each managed
@@ -5266,13 +5380,30 @@
                     /* shouldIncludeProfileAdmins */ (user) -> user.id == profileUser
                     || !mLockPatternUtils.isSeparateProfileChallengeEnabled(user.id));
             ArrayList<PasswordMetrics> adminMetrics = new ArrayList<>(admins.size());
-            int maxRequiredComplexity = PASSWORD_COMPLEXITY_NONE;
-            for (ActiveAdmin admin : admins) {
-                adminMetrics.add(admin.mPasswordPolicy.getMinMetrics());
-                maxRequiredComplexity = Math.max(maxRequiredComplexity, admin.mPasswordComplexity);
+            if (Flags.unmanagedModeMigration()) {
+                for (ActiveAdmin admin: admins) {
+                    adminMetrics.add(admin.mPasswordPolicy.getMinMetrics());
+                }
+                Integer maxRequiredComplexity = getResolvedPolicyForUserAndItsManagedProfiles(
+                        PolicyDefinition.PASSWORD_COMPLEXITY,
+                        userHandle,
+                        /* shouldIncludeProfileAdmins */ (user) -> user.id == profileUser
+                                || !mLockPatternUtils.isSeparateProfileChallengeEnabled(user.id));
+                return PasswordMetrics.validatePasswordMetrics(
+                        PasswordMetrics.merge(adminMetrics),
+                        maxRequiredComplexity != null
+                                ? maxRequiredComplexity : PASSWORD_COMPLEXITY_NONE,
+                        metrics).isEmpty();
+            } else {
+                int maxRequiredComplexity = PASSWORD_COMPLEXITY_NONE;
+                for (ActiveAdmin admin : admins) {
+                    adminMetrics.add(admin.mPasswordPolicy.getMinMetrics());
+                    maxRequiredComplexity = Math.max(maxRequiredComplexity,
+                            admin.mPasswordComplexity);
+                }
+                return PasswordMetrics.validatePasswordMetrics(PasswordMetrics.merge(adminMetrics),
+                        maxRequiredComplexity, metrics).isEmpty();
             }
-            return PasswordMetrics.validatePasswordMetrics(PasswordMetrics.merge(adminMetrics),
-                    maxRequiredComplexity, metrics).isEmpty();
         }
     }
 
@@ -5363,6 +5494,76 @@
         Preconditions.checkArgument(allowedModes.contains(passwordComplexity),
                 "Provided complexity is not one of the allowed values.");
 
+        if (!Flags.unmanagedModeMigration()) {
+            setRequiredPasswordComplexityPreCoexistence(callerPackageName, passwordComplexity,
+                    calledOnParent);
+            return;
+        }
+
+        CallerIdentity caller = getCallerIdentity(callerPackageName);
+        int affectedUser = calledOnParent
+                ? getProfileParentId(caller.getUserId()) : caller.getUserId();
+        EnforcingAdmin admin = enforcePermissionAndGetEnforcingAdmin(null,
+                MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS, caller.getPackageName(),
+                caller.getUserId());
+        Preconditions.checkArgument(!calledOnParent || isProfileOwner(caller));
+
+        ActiveAdmin activeAdmin = admin.getActiveAdmin();
+
+        // We require the caller to explicitly clear any password quality requirements set
+        // on the parent DPM instance, to avoid the case where password requirements are
+        // specified in the form of quality on the parent but complexity on the profile
+        // itself.
+        if (!calledOnParent) {
+            final boolean hasQualityRequirementsOnParent = activeAdmin.hasParentActiveAdmin()
+                    && activeAdmin.getParentActiveAdmin().mPasswordPolicy.quality
+                    != PASSWORD_QUALITY_UNSPECIFIED;
+            Preconditions.checkState(!hasQualityRequirementsOnParent,
+                    "Password quality is set on the parent when attempting to set password"
+                            + "complexity. Clear the quality by setting the password quality "
+                            + "on the parent to PASSWORD_QUALITY_UNSPECIFIED first");
+        }
+
+        if (passwordComplexity != PASSWORD_COMPLEXITY_NONE) {
+            mDevicePolicyEngine.setLocalPolicy(
+                    PolicyDefinition.PASSWORD_COMPLEXITY,
+                    admin,
+                    new IntegerPolicyValue(passwordComplexity),
+                    affectedUser);
+        } else {
+            mDevicePolicyEngine.removeLocalPolicy(
+                    PolicyDefinition.PASSWORD_COMPLEXITY,
+                    admin,
+                    affectedUser);
+        }
+
+        mInjector.binderWithCleanCallingIdentity(() -> {
+            // Reset the password policy.
+            if (calledOnParent) {
+                activeAdmin.getParentActiveAdmin().mPasswordPolicy = new PasswordPolicy();
+            } else {
+                activeAdmin.mPasswordPolicy = new PasswordPolicy();
+            }
+            synchronized (getLockObject()) {
+                updatePasswordValidityCheckpointLocked(caller.getUserId(), calledOnParent);
+            }
+            updatePasswordQualityCacheForUserGroup(caller.getUserId());
+            saveSettingsLocked(caller.getUserId());
+        });
+
+
+        DevicePolicyEventLogger
+                .createEvent(DevicePolicyEnums.SET_PASSWORD_COMPLEXITY)
+                .setAdmin(caller.getPackageName())
+                .setInt(passwordComplexity)
+                .setBoolean(calledOnParent)
+                .write();
+        logPasswordComplexityRequiredIfSecurityLogEnabled(caller.getPackageName(),
+                caller.getUserId(), calledOnParent, passwordComplexity);
+    }
+
+    private void setRequiredPasswordComplexityPreCoexistence(
+            String callerPackageName, int passwordComplexity, boolean calledOnParent) {
         CallerIdentity caller = getCallerIdentity(callerPackageName);
         if (!isPermissionCheckFlagEnabled()) {
             Preconditions.checkCallAuthorization(
@@ -5441,6 +5642,30 @@
     @GuardedBy("getLockObject()")
     private int getAggregatedPasswordComplexityLocked(@UserIdInt int userHandle,
             boolean deviceWideOnly) {
+        if (Flags.unmanagedModeMigration()) {
+            return getAggregatedPasswordComplexity(userHandle, deviceWideOnly);
+        } else {
+            return getAggregatedPasswordComplexityPreCoexistenceLocked(userHandle, deviceWideOnly);
+        }
+    }
+
+    private int getAggregatedPasswordComplexity(@UserIdInt int userHandle, boolean deviceWideOnly) {
+        ensureLocked();
+        Integer result;
+        if (deviceWideOnly) {
+            result = getResolvedPolicyForUserAndItsManagedProfiles(
+                    PolicyDefinition.PASSWORD_COMPLEXITY,
+                    userHandle,
+                    /* shouldIncludeProfile */ (user) -> false);
+        } else {
+            result = getResolvedLockscreenPolicy(PolicyDefinition.PASSWORD_COMPLEXITY, userHandle);
+        }
+        return result != null ? result : PASSWORD_COMPLEXITY_NONE;
+    }
+
+    @GuardedBy("getLockObject()")
+    private int getAggregatedPasswordComplexityPreCoexistenceLocked(@UserIdInt int userHandle,
+            boolean deviceWideOnly) {
         ensureLocked();
         final List<ActiveAdmin> admins;
         if (deviceWideOnly) {
@@ -5462,24 +5687,30 @@
             return PASSWORD_COMPLEXITY_NONE;
         }
 
-        final CallerIdentity caller = getCallerIdentity();
-
-        if (isPermissionCheckFlagEnabled()) {
+        if (Flags.unmanagedModeMigration()) {
+            final CallerIdentity caller = getCallerIdentity(callerPackageName);
             int affectedUser = calledOnParent ? getProfileParentId(caller.getUserId())
                     : caller.getUserId();
             enforcePermission(MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS,
                     callerPackageName, affectedUser);
+
+            Integer complexity = mDevicePolicyEngine.getResolvedPolicy(
+                    PolicyDefinition.PASSWORD_COMPLEXITY,
+                    affectedUser);
+            return complexity != null ? complexity : PASSWORD_COMPLEXITY_NONE;
         } else {
+            final CallerIdentity caller = getCallerIdentity();
+
             Preconditions.checkCallAuthorization(
                     isDefaultDeviceOwner(caller) || isProfileOwner(caller));
 
             Preconditions.checkArgument(!calledOnParent || isProfileOwner(caller));
-        }
 
-        synchronized (getLockObject()) {
-            final ActiveAdmin requiredAdmin = getParentOfAdminIfRequired(
-                    getDeviceOrProfileOwnerAdminLocked(caller.getUserId()), calledOnParent);
-            return requiredAdmin.mPasswordComplexity;
+            synchronized (getLockObject()) {
+                final ActiveAdmin requiredAdmin = getParentOfAdminIfRequired(
+                        getDeviceOrProfileOwnerAdminLocked(caller.getUserId()), calledOnParent);
+                return requiredAdmin.mPasswordComplexity;
+            }
         }
     }
 
@@ -9288,6 +9519,10 @@
                 }
             }
 
+            // TODO: with a quick glance this logic seems incomplete that it doesn't properly handle
+            // the different behaviour between a profile with separate challenge vs a profile with
+            // unified challenge, which was part of getActiveAdminsForLockscreenPoliciesLocked()
+            // before the migration.
             if (isUnicornFlagEnabled()) {
                 Integer features = mDevicePolicyEngine.getResolvedPolicy(
                         PolicyDefinition.KEYGUARD_DISABLED_FEATURES,
@@ -12983,8 +13218,7 @@
         return result;
     }
 
-    @Override
-    public String[] setPackagesSuspended(ComponentName who, String callerPackage,
+    private String[] setPackagesSuspendedPreCoexistence(ComponentName who, String callerPackage,
             String[] packageNames, boolean suspended) {
         final CallerIdentity caller = getCallerIdentity(who, callerPackage);
         ActiveAdmin admin;
@@ -13065,6 +13299,78 @@
         return result;
     }
 
+    @Override
+    public String[] setPackagesSuspended(ComponentName who, String callerPackage,
+            String[] packageNames, boolean suspended) {
+        if (!Flags.unmanagedModeMigration()) {
+            return setPackagesSuspendedPreCoexistence(who, callerPackage, packageNames, suspended);
+        }
+
+        final CallerIdentity caller = getCallerIdentity(who, callerPackage);
+
+        EnforcingAdmin enforcingAdmin = enforcePermissionAndGetEnforcingAdmin(
+                who,
+                MANAGE_DEVICE_POLICY_PACKAGE_STATE,
+                caller.getPackageName(),
+                caller.getUserId());
+        checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_PACKAGES_SUSPENDED);
+
+        Set<String> packages = new ArraySet<>(packageNames);
+        Set<String> suspendedPackagesBefore = mDevicePolicyEngine.getResolvedPolicy(
+                PolicyDefinition.PACKAGES_SUSPENDED, caller.getUserId());
+
+        Set<String> currentPackages = mDevicePolicyEngine.getLocalPolicySetByAdmin(
+                PolicyDefinition.PACKAGES_SUSPENDED,
+                enforcingAdmin,
+                caller.getUserId());
+        if (currentPackages == null) currentPackages = new ArraySet<>();
+        if (suspended) {
+            currentPackages.addAll(packages);
+        } else {
+            currentPackages.removeAll(packages);
+        }
+        if (currentPackages.isEmpty()) {
+            mDevicePolicyEngine.removeLocalPolicy(
+                    PolicyDefinition.PACKAGES_SUSPENDED,
+                    enforcingAdmin,
+                    caller.getUserId());
+        } else {
+            mDevicePolicyEngine.setLocalPolicy(
+                    PolicyDefinition.PACKAGES_SUSPENDED,
+                    enforcingAdmin,
+                    new PackageSetPolicyValue(currentPackages),
+                    caller.getUserId());
+        }
+
+        Set<String> suspendedPackagesAfter = mDevicePolicyEngine.getResolvedPolicy(
+                PolicyDefinition.PACKAGES_SUSPENDED, caller.getUserId());
+
+        PackageSuspender suspender = new PackageSuspender(
+                suspendedPackagesBefore, suspendedPackagesAfter,
+                listPolicyExemptAppsUnchecked(mContext),
+                mInjector.getPackageManagerInternal(), caller.getUserId());
+
+        String[] result;
+        synchronized (getLockObject()) {
+            long id = mInjector.binderClearCallingIdentity();
+            try {
+                result = suspended ? suspender.suspend(packages) : suspender.unsuspend(packages);
+            } finally {
+                mInjector.binderRestoreCallingIdentity(id);
+            }
+        }
+
+        DevicePolicyEventLogger
+                .createEvent(DevicePolicyEnums.SET_PACKAGES_SUSPENDED)
+                .setAdmin(caller.getPackageName())
+                .setBoolean(/* isDelegate */ who == null)
+                .setStrings(packageNames)
+                .write();
+
+        if (VERBOSE_LOG) Slogf.v(LOG_TAG, "Returning %s", Arrays.toString(result));
+        return result;
+    }
+
     /**
      * Returns an array containing the union of the given non-suspended packages and
      * exempt apps. Assumes both parameters are non-null and non-empty.
@@ -13086,7 +13392,7 @@
     public boolean isPackageSuspended(ComponentName who, String callerPackage, String packageName) {
         final CallerIdentity caller = getCallerIdentity(who, callerPackage);
 
-        if (isUnicornFlagEnabled()) {
+        if (Flags.unmanagedModeMigration()) {
             enforcePermission(
                     MANAGE_DEVICE_POLICY_PACKAGE_STATE,
                     caller.getPackageName(),
@@ -15257,17 +15563,6 @@
         }
     }
 
-    private Set<String> getPackagesSuspendedByAdmin(@UserIdInt int userId) {
-        synchronized (getLockObject()) {
-            ActiveAdmin admin = getDeviceOrProfileOwnerAdminLocked(userId);
-            if (admin == null || admin.suspendedPackages == null) {
-                return Collections.emptySet();
-            } else {
-                return new ArraySet<>(admin.suspendedPackages);
-            }
-        }
-    }
-
     /**
      * We need to update the internal state of whether a user has completed setup or a
      * device has paired once. After that, we ignore any changes that reset the
@@ -23523,7 +23818,39 @@
         }
 
         Slog.w(LOG_TAG, "Work apps may have been paused via suspension previously.");
-        unsuspendAppsForQuietProfiles();
+        PackageManagerInternal pmi = mInjector.getPackageManagerInternal();
+        List<UserInfo> users = mUserManagerInternal.getUsers(true /* excludeDying */);
+
+        for (UserInfo user : users) {
+            if (!user.isManagedProfile() || !user.isQuietModeEnabled()) {
+                continue;
+            }
+            int userId = user.id;
+            Set<String> suspendedByAdmin;
+            synchronized (getLockObject()) {
+                ActiveAdmin admin = getDeviceOrProfileOwnerAdminLocked(userId);
+                // This is legacy code from Turn off Work 2.0 which is before setPackagesSuspended
+                // is migrated to PolicyEngine, so we only need to query the legacy ActiveAdmin here
+                if (admin == null || admin.suspendedPackages == null) {
+                    suspendedByAdmin = Collections.emptySet();
+                } else {
+                    suspendedByAdmin = new ArraySet<>(admin.suspendedPackages);
+                }
+            }
+            var packagesToUnsuspend = mInjector.getPackageManager(userId)
+                    .getInstalledPackages(PackageManager.PackageInfoFlags.of(
+                            MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE))
+                    .stream()
+                    .map(packageInfo -> packageInfo.packageName)
+                    .filter(pkg -> !suspendedByAdmin.contains(pkg))
+                    .toArray(String[]::new);
+
+            Slogf.i(LOG_TAG, "Unsuspending work apps for user %d", userId);
+            // When app suspension was used for quiet mode, the apps were suspended by platform
+            // package, just like when admin suspends them. So although it wasn't admin who
+            // suspended, this method will remove the right suspension record.
+            pmi.setPackagesSuspendedByAdmin(userId, packagesToUnsuspend, false /* suspended */);
+        }
     }
 
     public void setMtePolicy(int flags, String callerPackageName) {
@@ -23947,6 +24274,15 @@
     @GuardedBy("getLockObject()")
     private void migratePoliciesToPolicyEngineLocked() {
         maybeMigrateSecurityLoggingPolicyLocked();
+        // ID format: <sdk-int>.<auto_increment_id>.<descriptions>'
+        String unmanagedBackupId = "35.1.unmanaged-mode";
+        boolean migrated = false;
+        migrated = migrated | maybeMigrateRequiredPasswordComplexityLocked(unmanagedBackupId);
+        migrated = migrated | maybeMigrateSuspendedPackagesLocked(unmanagedBackupId);
+        if (migrated) {
+            Slogf.i(LOG_TAG, "Backup made: " + unmanagedBackupId);
+        }
+        // Additional migration steps should repeat the pattern above with a new backupId.
     }
 
     private void migrateAutoTimezonePolicy() {
@@ -24211,6 +24547,23 @@
         });
     }
 
+    @GuardedBy("getLockObject()")
+    private void iterateThroughDpcAdminsLocked(BiConsumer<ActiveAdmin, EnforcingAdmin> runner) {
+        Binder.withCleanCallingIdentity(() -> {
+            List<UserInfo> users = mUserManager.getUsers();
+            for (UserInfo userInfo : users) {
+                ActiveAdmin admin = getProfileOwnerOrDeviceOwnerLocked(userInfo.id);
+                if (admin == null) continue;
+                EnforcingAdmin enforcingAdmin = EnforcingAdmin.createEnterpriseEnforcingAdmin(
+                        admin.info.getComponent(),
+                        userInfo.id,
+                        admin);
+
+                runner.accept(admin, enforcingAdmin);
+            }
+        });
+    }
+
     private List<PackageInfo> getInstalledPackagesOnUser(int userId) {
         return mInjector.binderWithCleanCallingIdentity(() ->
                 mContext.getPackageManager().getInstalledPackagesAsUser(
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/MostRestrictive.java b/services/devicepolicy/java/com/android/server/devicepolicy/MostRestrictive.java
index 7e8eaa7..2066376 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/MostRestrictive.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/MostRestrictive.java
@@ -19,9 +19,9 @@
 import android.annotation.NonNull;
 import android.app.admin.PolicyValue;
 
+import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
 
 final class MostRestrictive<V> extends ResolutionMechanism<V> {
 
@@ -33,18 +33,21 @@
 
     @Override
     PolicyValue<V> resolve(@NonNull LinkedHashMap<EnforcingAdmin, PolicyValue<V>> adminPolicies) {
+        return resolve(new ArrayList<>(adminPolicies.values()));
+    }
+
+    @Override
+    PolicyValue<V> resolve(List<PolicyValue<V>> adminPolicies) {
         if (adminPolicies.isEmpty()) {
             return null;
         }
         for (PolicyValue<V> value : mMostToLeastRestrictive) {
-            if (adminPolicies.containsValue(value)) {
+            if (adminPolicies.contains(value)) {
                 return value;
             }
         }
         // Return first set policy if none can be found in known values
-        Map.Entry<EnforcingAdmin, PolicyValue<V>> policy =
-                adminPolicies.entrySet().stream().findFirst().get();
-        return policy.getValue();
+        return adminPolicies.get(0);
     }
 
     @Override
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
index 7912cbc..3f9605a 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java
@@ -636,6 +636,33 @@
         }
     }
 
+    boolean isRequiredPasswordComplexityMigrated() {
+        synchronized (mData) {
+            return mData.mRequiredPasswordComplexityMigrated;
+        }
+    }
+
+    void markRequiredPasswordComplexityMigrated() {
+        synchronized (mData) {
+            mData.mRequiredPasswordComplexityMigrated = true;
+            mData.writeDeviceOwner();
+        }
+
+    }
+
+    boolean isSuspendedPackagesMigrated() {
+        synchronized (mData) {
+            return mData.mSuspendedPackagesMigrated;
+        }
+    }
+
+    void markSuspendedPackagesMigrated() {
+        synchronized (mData) {
+            mData.mSuspendedPackagesMigrated = true;
+            mData.writeDeviceOwner();
+        }
+    }
+
     boolean isMigratedPostUpdate() {
         synchronized (mData) {
             return mData.mPoliciesMigratedPostUpdate;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
index d02cfee..2ea5f16 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/OwnersData.java
@@ -88,7 +88,9 @@
 
     private static final String ATTR_MIGRATED_TO_POLICY_ENGINE = "migratedToPolicyEngine";
     private static final String ATTR_SECURITY_LOG_MIGRATED = "securityLogMigrated";
-
+    private static final String ATTR_REQUIRED_PASSWORD_COMPLEXITY_MIGRATED =
+            "passwordComplexityMigrated";
+    private static final String ATTR_SUSPENDED_PACKAGES_MIGRATED = "suspendedPackagesMigrated";
     private static final String ATTR_MIGRATED_POST_UPGRADE = "migratedPostUpgrade";
 
     // Internal state for the device owner package.
@@ -118,6 +120,8 @@
 
     boolean mMigratedToPolicyEngine = false;
     boolean mSecurityLoggingMigrated = false;
+    boolean mRequiredPasswordComplexityMigrated = false;
+    boolean mSuspendedPackagesMigrated = false;
 
     boolean mPoliciesMigratedPostUpdate = false;
 
@@ -409,6 +413,14 @@
             if (Flags.securityLogV2Enabled()) {
                 out.attributeBoolean(null, ATTR_SECURITY_LOG_MIGRATED, mSecurityLoggingMigrated);
             }
+            if (Flags.unmanagedModeMigration()) {
+                out.attributeBoolean(null, ATTR_REQUIRED_PASSWORD_COMPLEXITY_MIGRATED,
+                        mRequiredPasswordComplexityMigrated);
+                out.attributeBoolean(null, ATTR_SUSPENDED_PACKAGES_MIGRATED,
+                        mSuspendedPackagesMigrated);
+
+            }
+
             out.endTag(null, TAG_POLICY_ENGINE_MIGRATION);
 
         }
@@ -473,6 +485,12 @@
                             null, ATTR_MIGRATED_POST_UPGRADE, false);
                     mSecurityLoggingMigrated = Flags.securityLogV2Enabled()
                             && parser.getAttributeBoolean(null, ATTR_SECURITY_LOG_MIGRATED, false);
+                    mRequiredPasswordComplexityMigrated = Flags.unmanagedModeMigration()
+                            && parser.getAttributeBoolean(null,
+                                    ATTR_REQUIRED_PASSWORD_COMPLEXITY_MIGRATED, false);
+                    mSuspendedPackagesMigrated = Flags.unmanagedModeMigration()
+                            && parser.getAttributeBoolean(null,
+                                    ATTR_SUSPENDED_PACKAGES_MIGRATED, false);
 
                     break;
                 default:
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PackageSuspender.java b/services/devicepolicy/java/com/android/server/devicepolicy/PackageSuspender.java
new file mode 100644
index 0000000..40cf0e9
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PackageSuspender.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicepolicy;
+
+import static com.android.server.devicepolicy.DevicePolicyManagerService.LOG_TAG;
+
+import android.annotation.Nullable;
+import android.content.pm.PackageManagerInternal;
+import android.util.ArraySet;
+
+import com.android.server.utils.Slogf;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Helper class for calling into PackageManagerInternal.setPackagesSuspendedByAdmin.
+ * Two main things this class encapsulates:
+ *    1. Handling of the DPM internal suspension exemption list
+ *    2. Calculating the failed packages result in the context of coexistence
+ *
+ * 1 is handled by the two internal methods {@link #suspendWithExemption(Set)} and
+ * {@link #unsuspendWithExemption(Set)} where the exemption list is taken into consideration
+ * before and after calling {@link PackageManagerInternal#setPackagesSuspendedByAdmin}.
+ * In order to compute 2, the resolved package suspension state before and after suspension is
+ * needed as multiple admins can both suspend the same packages under coexistence.
+ */
+public class PackageSuspender {
+
+    private final Set<String> mSuspendedPackageBefore;
+    private final Set<String> mSuspendedPackageAfter;
+    private final List<String> mExemptedPackages;
+    private final PackageManagerInternal mPackageManager;
+    private final int mUserId;
+
+    public PackageSuspender(@Nullable Set<String> suspendedPackageBefore,
+            @Nullable Set<String> suspendedPackageAfter, List<String> exemptedPackages,
+            PackageManagerInternal pmi, int userId) {
+        mSuspendedPackageBefore =
+                suspendedPackageBefore != null ? suspendedPackageBefore : Collections.emptySet();
+        mSuspendedPackageAfter =
+                suspendedPackageAfter != null ? suspendedPackageAfter : Collections.emptySet();
+        mExemptedPackages = exemptedPackages;
+        mPackageManager = pmi;
+        mUserId = userId;
+    }
+
+    /**
+     * Suspend packages that are requested by a single admin
+     *
+     * @return a list of packages that the admin has requested to suspend but could not be
+     * suspended, due to DPM and PackageManager exemption list.
+     *
+     */
+    public String[] suspend(Set<String> packages) {
+        // When suspending, call PM with the list of packages admin has requested, even if some
+        // of these packages are already in suspension (some other admin might have already
+        // suspended them). We do it this way so that we can simply return the failed list from
+        // PackageManager to the caller as the accurate list of unsuspended packages.
+        // This is different from the unsuspend() logic, please see below.
+        //
+        // For example admin A already suspended package 1, 2 and 3, but package 3 is
+        // PackageManager-exempted. Now admin B wants to suspend package 2, 3 and 4 (2 and 4 are
+        // suspendable). We need to return package 3 as the unsuspended package here, and we ask
+        // PackageManager to suspend package 2, 3 and 4 here (who will return package 3 in the
+        // failed list, and package 2 is already suspended).
+        Set<String> result = suspendWithExemption(packages);
+        return result.toArray(String[]::new);
+    }
+
+    /**
+     * Suspend packages considering the exemption list.
+     *
+     * @return the list of packages that couldn't be suspended, either due to the exemption list,
+     * or due to failures from PackageManagerInternal itself.
+     */
+    private Set<String> suspendWithExemption(Set<String> packages) {
+        Set<String> packagesToSuspend = new ArraySet<>(packages);
+        // Any original packages that are also in the exempted list will not be suspended and hence
+        // will appear in the final result.
+        Set<String> result = new ArraySet<>(mExemptedPackages);
+        result.retainAll(packagesToSuspend);
+        // Remove exempted packages before calling PackageManager
+        packagesToSuspend.removeAll(mExemptedPackages);
+        String[] failedPackages = mPackageManager.setPackagesSuspendedByAdmin(
+                mUserId, packagesToSuspend.toArray(String[]::new), true);
+        if (failedPackages == null) {
+            Slogf.w(LOG_TAG, "PM failed to suspend packages (%s)", packages);
+            return packages;
+        } else {
+            result.addAll(Arrays.asList(failedPackages));
+            return result;
+        }
+    }
+
+    /**
+     * Unsuspend packages that are requested by a single admin
+     *
+     * @return a list of packages that the admin has requested to unsuspend but could not be
+     * unsuspended, due to other amdin's policy or PackageManager restriction.
+     *
+     */
+    public String[] unsuspend(Set<String> packages) {
+        // Unlike suspend(), when unsuspending, call PackageManager with the delta of resolved
+        // suspended packages list and not what the admin has requested. This is because some
+        // packages might still be subject to another admin's suspension request.
+        Set<String> packagesToUnsuspend = new ArraySet<>(mSuspendedPackageBefore);
+        packagesToUnsuspend.removeAll(mSuspendedPackageAfter);
+
+        // To calculate the result (which packages are not unsuspended), start with packages that
+        // are still subject to another admin's suspension policy. This is calculated by
+        // intersecting the packages argument with mSuspendedPackageAfter.
+        Set<String> result = new ArraySet<>(packages);
+        result.retainAll(mSuspendedPackageAfter);
+        // Remove mExemptedPackages since they can't be suspended to start with.
+        result.removeAll(mExemptedPackages);
+        // Finally make the unsuspend() request and add packages that PackageManager can't unsuspend
+        // to the result.
+        result.addAll(unsuspendWithExemption(packagesToUnsuspend));
+        return result.toArray(String[]::new);
+    }
+
+    /**
+     * Unsuspend packages considering the exemption list.
+     *
+     * @return the list of packages that couldn't be unsuspended, either due to the exemption list,
+     * or due to failures from PackageManagerInternal itself.
+     */
+    private Set<String> unsuspendWithExemption(Set<String> packages) {
+        // when unsuspending, no need to consider exemption list since by definition they can't
+        // be suspended to begin with.
+        String[] failedPackages = mPackageManager.setPackagesSuspendedByAdmin(
+                mUserId, packages.toArray(String[]::new), false);
+        if (failedPackages == null) {
+            Slogf.w(LOG_TAG, "PM failed to unsuspend packages (%s)", packages);
+        }
+        return new ArraySet<>(failedPackages);
+    }
+}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index 8bec384..a0ea4e9 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -240,7 +240,7 @@
                     POLICY_FLAG_LOCAL_ONLY_POLICY | POLICY_FLAG_INHERITABLE
                             | POLICY_FLAG_NON_COEXISTABLE_POLICY
                             | POLICY_FLAG_SKIP_ENFORCEMENT_IF_UNCHANGED,
-                    PolicyEnforcerCallbacks::setApplicationRestrictions,
+                    PolicyEnforcerCallbacks::noOp,
                     new BundlePolicySerializer());
 
     /**
@@ -263,7 +263,7 @@
             new MostRecent<>(),
             POLICY_FLAG_LOCAL_ONLY_POLICY | POLICY_FLAG_NON_COEXISTABLE_POLICY,
             // DevicePolicyManagerService handles the enforcement, this just takes care of storage
-            (Long value, Context context, Integer userId, PolicyKey policyKey) -> true,
+            PolicyEnforcerCallbacks::noOp,
             new LongPolicySerializer());
 
     static PolicyDefinition<Integer> KEYGUARD_DISABLED_FEATURES = new PolicyDefinition<>(
@@ -271,7 +271,7 @@
             new FlagUnion(),
             POLICY_FLAG_LOCAL_ONLY_POLICY,
             // Nothing is enforced for keyguard features, we just need to store it
-            (Integer value, Context context, Integer userId, PolicyKey policyKey) -> true,
+            PolicyEnforcerCallbacks::noOp,
             new IntegerPolicySerializer());
 
     // This is saved in the static map sPolicyDefinitions so that we're able to reconstruct the
@@ -312,7 +312,7 @@
                     TRUE_MORE_RESTRICTIVE,
                     POLICY_FLAG_LOCAL_ONLY_POLICY | POLICY_FLAG_INHERITABLE,
                     // Nothing is enforced, we just need to store it
-                    (Boolean value, Context context, Integer userId, PolicyKey policyKey) -> true,
+                    PolicyEnforcerCallbacks::noOp,
                     new BooleanPolicySerializer());
 
     /**
@@ -332,7 +332,7 @@
             new NoArgsPolicyKey(DevicePolicyIdentifiers.PERMITTED_INPUT_METHODS_POLICY),
             new MostRecent<>(),
             POLICY_FLAG_LOCAL_ONLY_POLICY | POLICY_FLAG_INHERITABLE,
-            (Set<String> value, Context context, Integer userId, PolicyKey policyKey) -> true,
+            PolicyEnforcerCallbacks::noOp,
             new PackageSetPolicySerializer());
 
 
@@ -366,6 +366,30 @@
             PolicyEnforcerCallbacks::setContentProtectionPolicy,
             new IntegerPolicySerializer());
 
+    static PolicyDefinition<Integer> PASSWORD_COMPLEXITY = new PolicyDefinition<>(
+            new NoArgsPolicyKey(DevicePolicyIdentifiers.PASSWORD_COMPLEXITY_POLICY),
+            new MostRestrictive<>(
+                    List.of(
+                            new IntegerPolicyValue(
+                                    DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH),
+                            new IntegerPolicyValue(
+                                    DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM),
+                            new IntegerPolicyValue(
+                                    DevicePolicyManager.PASSWORD_COMPLEXITY_LOW),
+                            new IntegerPolicyValue(
+                                    DevicePolicyManager.PASSWORD_COMPLEXITY_NONE))),
+            POLICY_FLAG_LOCAL_ONLY_POLICY,
+            PolicyEnforcerCallbacks::noOp,
+            new IntegerPolicySerializer());
+
+    static PolicyDefinition<Set<String>> PACKAGES_SUSPENDED =
+            new PolicyDefinition<>(
+                    new NoArgsPolicyKey(
+                            DevicePolicyIdentifiers.PACKAGES_SUSPENDED_POLICY),
+                    new PackageSetUnion(),
+                    PolicyEnforcerCallbacks::noOp,
+                    new PackageSetPolicySerializer());
+
     private static final Map<String, PolicyDefinition<?>> POLICY_DEFINITIONS = new HashMap<>();
     private static Map<String, Integer> USER_RESTRICTION_FLAGS = new HashMap<>();
 
@@ -405,6 +429,13 @@
                 USB_DATA_SIGNALING);
         POLICY_DEFINITIONS.put(DevicePolicyIdentifiers.CONTENT_PROTECTION_POLICY,
                 CONTENT_PROTECTION);
+        // Intentionally not flagged since if the flag is flipped off on a device already
+        // having PASSWORD_COMPLEXITY policy in the on-device XML, it will cause the
+        // deserialization logic to break due to seeing an unknown tag.
+        POLICY_DEFINITIONS.put(DevicePolicyIdentifiers.PASSWORD_COMPLEXITY_POLICY,
+                PASSWORD_COMPLEXITY);
+        POLICY_DEFINITIONS.put(DevicePolicyIdentifiers.PACKAGES_SUSPENDED_POLICY,
+                PACKAGES_SUSPENDED);
 
         // User Restriction Policies
         USER_RESTRICTION_FLAGS.put(UserManager.DISALLOW_MODIFY_ACCOUNTS, /* flags= */ 0);
@@ -523,7 +554,7 @@
     private final PolicyKey mPolicyKey;
     private final ResolutionMechanism<V> mResolutionMechanism;
     private final int mPolicyFlags;
-    // A function that accepts  policy to apple, context, userId, callback arguments, and returns
+    // A function that accepts  policy to apply, context, userId, callback arguments, and returns
     // true if the policy has been enforced successfully.
     private final QuadFunction<V, Context, Integer, PolicyKey, Boolean> mPolicyEnforcerCallback;
     private final PolicySerializer<V> mPolicySerializer;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
index 04d277e..e1cb37d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
@@ -73,6 +73,10 @@
 
     private static final String LOG_TAG = "PolicyEnforcerCallbacks";
 
+    static <T> boolean noOp(T value, Context context, Integer userId, PolicyKey policyKey) {
+        return true;
+    }
+
     static boolean setAutoTimezoneEnabled(@Nullable Boolean enabled, @NonNull Context context) {
         if (!DevicePolicyManagerService.isUnicornFlagEnabled()) {
             Slogf.w(LOG_TAG, "Trying to enforce setAutoTimezoneEnabled while flag is off.");
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
index c544ebc..245c438 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
@@ -200,7 +200,7 @@
         pw.println(mPolicyDefinition.getPolicyKey());
         pw.increaseIndent();
 
-        pw.println("Per-admin Policy");
+        pw.println("Per-admin Policy:");
         pw.increaseIndent();
         if (mPoliciesSetByAdmins.size() == 0) {
             pw.println("null");
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ResolutionMechanism.java b/services/devicepolicy/java/com/android/server/devicepolicy/ResolutionMechanism.java
index c321aa1..ad7ac2b 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/ResolutionMechanism.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/ResolutionMechanism.java
@@ -20,9 +20,24 @@
 import android.app.admin.PolicyValue;
 
 import java.util.LinkedHashMap;
+import java.util.List;
 
 abstract class ResolutionMechanism<V> {
+    /**
+     * The most generic resolution logic where we know both the policy value and the admin who
+     * sets it.
+     */
     @Nullable
     abstract PolicyValue<V> resolve(LinkedHashMap<EnforcingAdmin, PolicyValue<V>> adminPolicies);
+
+    /**
+     * A special resolution logic that does not care about admins who set them. Only applicable to
+     * a subset of ResolutionMechanism.
+     */
+    @Nullable
+    PolicyValue<V> resolve(List<PolicyValue<V>> adminPolicies) {
+        throw new UnsupportedOperationException();
+    }
+
     abstract android.app.admin.ResolutionMechanism<V> getParcelableResolutionMechanism();
 }
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index cfe4e17..927df8b 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -761,6 +761,9 @@
         }
     }
 
+    private static final long BINDER_CALLBACK_THROTTLE_MS = 10_100L;
+    private long mBinderCallbackLast = -1;
+
     private void run() {
         TimingsTraceAndSlog t = new TimingsTraceAndSlog();
         try {
@@ -965,6 +968,14 @@
         Binder.setTransactionCallback(new IBinderCallback() {
             @Override
             public void onTransactionError(int pid, int code, int flags, int err) {
+
+                final long now = SystemClock.uptimeMillis();
+                if (now < mBinderCallbackLast + BINDER_CALLBACK_THROTTLE_MS) {
+                    Slog.d(TAG, "Too many transaction errors, throttling freezer binder callback.");
+                    return;
+                }
+                mBinderCallbackLast = now;
+                Slog.wtfStack(TAG, "Binder Transaction Error");
                 mActivityManagerService.frozenBinderTransactionDetected(pid, code, flags, err);
             }
         });
diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index 3628a57..d3efcb6 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -16,7 +16,6 @@
 
 package com.android.server.display;
 
-import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
@@ -44,6 +43,7 @@
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
 import android.os.PowerManager;
+import android.os.SystemClock;
 import android.os.test.TestLooper;
 import android.util.SparseArray;
 import android.view.Display;
@@ -54,6 +54,7 @@
 
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.config.HysteresisLevels;
+import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.testutils.OffsettableClock;
 
 import org.junit.After;
@@ -68,6 +69,8 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class AutomaticBrightnessControllerTest {
+    private static final int ANDROID_SLEEP_TIME = 1000;
+    private static final int NANO_SECONDS_MULTIPLIER = 1000000;
     private static final float BRIGHTNESS_MIN_FLOAT = 0.0f;
     private static final float BRIGHTNESS_MAX_FLOAT = 1.0f;
     private static final int LIGHT_SENSOR_RATE = 20;
@@ -100,6 +103,8 @@
     @Mock BrightnessRangeController mBrightnessRangeController;
     @Mock
     BrightnessClamperController mBrightnessClamperController;
+    @Mock
+    DisplayManagerFlags mDisplayManagerFlags;
     @Mock BrightnessThrottler mBrightnessThrottler;
 
     @Before
@@ -148,8 +153,18 @@
                     }
 
                     @Override
-                    AutomaticBrightnessController.Clock createClock() {
-                        return mClock::now;
+                    AutomaticBrightnessController.Clock createClock(boolean isEnabled) {
+                        return new AutomaticBrightnessController.Clock() {
+                            @Override
+                            public long uptimeMillis() {
+                                return mClock.now();
+                            }
+
+                            @Override
+                            public long getSensorEventScaleTime() {
+                                return mClock.now() + ANDROID_SLEEP_TIME;
+                            }
+                        };
                     }
 
                 }, // pass in test looper instead, pass in offsettable clock
@@ -166,7 +181,7 @@
                 mContext, mBrightnessRangeController, mBrightnessThrottler,
                 useHorizon ? AMBIENT_LIGHT_HORIZON_SHORT : 1,
                 useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits,
-                mBrightnessClamperController
+                mBrightnessClamperController, mDisplayManagerFlags
         );
 
         when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn(
@@ -350,7 +365,7 @@
 
         when(mBrightnessMappingStrategy.getShortTermModelTimeout()).thenReturn(2000L);
 
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE, /* sendUpdate= */ true);
         when(mBrightnessMappingStrategy.shouldResetShortTermModel(
                 123f, 0.5f)).thenReturn(true);
 
@@ -360,7 +375,7 @@
                 mBrightnessMappingStrategy.getShortTermModelTimeout() + 1000);
         mTestLooper.dispatchAll();
 
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT, /* sendUpdate= */ true);
         mTestLooper.moveTimeForward(4000);
         mTestLooper.dispatchAll();
 
@@ -394,14 +409,14 @@
         when(mBrightnessMappingStrategy.getUserBrightness()).thenReturn(0.51f);
         when(mBrightnessMappingStrategy.getUserLux()).thenReturn(123.0f);
 
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE, /* sendUpdate= */ true);
 
         // Time does not move forward, since clock is doesn't increment naturally.
         mTestLooper.dispatchAll();
 
         // Sensor reads 100000 lux,
         listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 678910));
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT, /* sendUpdate= */ true);
 
         // Verify short term model is not reset.
         verify(mBrightnessMappingStrategy, never()).clearUserDataPoints();
@@ -432,7 +447,7 @@
         when(mBrightnessMappingStrategy.getUserBrightness()).thenReturn(0.5f);
         when(mBrightnessMappingStrategy.getUserLux()).thenReturn(123f);
 
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE, /* sendUpdate= */ true);
         when(mIdleBrightnessMappingStrategy.getUserBrightness()).thenReturn(
                 PowerManager.BRIGHTNESS_INVALID_FLOAT);
         when(mIdleBrightnessMappingStrategy.getUserLux()).thenReturn(
@@ -446,7 +461,7 @@
                 mBrightnessMappingStrategy.getShortTermModelTimeout() + 1000);
         mTestLooper.dispatchAll();
 
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT, /* sendUpdate= */ true);
         mTestLooper.moveTimeForward(4000);
         mTestLooper.dispatchAll();
 
@@ -479,7 +494,7 @@
         when(mBrightnessMappingStrategy.getUserBrightness()).thenReturn(0.5f);
         when(mBrightnessMappingStrategy.getUserLux()).thenReturn(123f);
 
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE, /* sendUpdate= */ true);
         when(mIdleBrightnessMappingStrategy.getUserBrightness()).thenReturn(
                 PowerManager.BRIGHTNESS_INVALID_FLOAT);
         when(mIdleBrightnessMappingStrategy.getUserLux()).thenReturn(
@@ -493,7 +508,7 @@
         // Do not fast-forward time.
         mTestLooper.dispatchAll();
 
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT, /* sendUpdate= */ true);
         // Do not fast-forward time
         mTestLooper.dispatchAll();
 
@@ -523,7 +538,7 @@
 
         // No user brightness interaction.
 
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE, /* sendUpdate= */ true);
         when(mIdleBrightnessMappingStrategy.getUserBrightness()).thenReturn(
                 PowerManager.BRIGHTNESS_INVALID_FLOAT);
         when(mIdleBrightnessMappingStrategy.getUserLux()).thenReturn(
@@ -534,7 +549,7 @@
         // Do not fast-forward time.
         mTestLooper.dispatchAll();
 
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT, /* sendUpdate= */ true);
         // Do not fast-forward time
         mTestLooper.dispatchAll();
 
@@ -568,7 +583,7 @@
         verify(mBrightnessMappingStrategy, times(3)).getBrightness(anyFloat(), any(), anyInt());
 
         // Now let's do the same for idle mode
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE, /* sendUpdate= */ true);
         // Called once when switching,
         // setAmbientLux() is called twice and once in updateAutoBrightness(),
         // nextAmbientLightBrighteningTransition() and nextAmbientLightDarkeningTransition() are
@@ -800,6 +815,43 @@
     }
 
     @Test
+    public void testAmbientLuxBuffers_prunedBeyondLongHorizonExceptLatestValue() throws Exception {
+        when(mDisplayManagerFlags.offloadControlsDozeAutoBrightness()).thenReturn(true);
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // Choose values such that the ring buffer's capacity is extended and the buffer is pruned
+        int increment = 11;
+        int lux = 5000;
+        for (int i = 0; i < 1000; i++) {
+            lux += increment;
+            mClock.fastForward(increment);
+            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux,
+                    (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER));
+        }
+        mClock.fastForward(AMBIENT_LIGHT_HORIZON_LONG + 10);
+        int newLux = 2000;
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, newLux,
+                (mClock.now() + ANDROID_SLEEP_TIME) * NANO_SECONDS_MULTIPLIER));
+
+        float[] sensorValues = mController.getLastSensorValues();
+        long[] sensorTimestamps = mController.getLastSensorTimestamps();
+        // Only the values within the horizon should be kept
+        assertEquals(2, sensorValues.length);
+        assertEquals(2, sensorTimestamps.length);
+
+        assertEquals(lux, sensorValues[0], EPSILON);
+        assertEquals(newLux, sensorValues[1], EPSILON);
+        assertEquals(mClock.now() + ANDROID_SLEEP_TIME - AMBIENT_LIGHT_HORIZON_LONG,
+                sensorTimestamps[0]);
+        assertEquals(mClock.now() + ANDROID_SLEEP_TIME,
+                sensorTimestamps[1]);
+    }
+
+    @Test
     public void testGetSensorReadingsFullBuffer() throws Exception {
         ArgumentCaptor<SensorEventListener> listenerCaptor =
                 ArgumentCaptor.forClass(SensorEventListener.class);
@@ -966,7 +1018,7 @@
                 BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
                 /* useHorizon= */ false);
 
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE, /* sendUpdate= */ true);
 
         ArgumentCaptor<SensorEventListener> listenerCaptor =
                 ArgumentCaptor.forClass(SensorEventListener.class);
@@ -1003,7 +1055,7 @@
                 BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
                 /* useHorizon= */ false);
 
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE, /* sendUpdate= */ true);
 
         ArgumentCaptor<SensorEventListener> listenerCaptor =
                 ArgumentCaptor.forClass(SensorEventListener.class);
@@ -1030,35 +1082,6 @@
     }
 
     @Test
-    public void testBrightnessBasedOnLastUsedLux() throws Exception {
-        ArgumentCaptor<SensorEventListener> listenerCaptor =
-                ArgumentCaptor.forClass(SensorEventListener.class);
-        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
-                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
-        SensorEventListener listener = listenerCaptor.getValue();
-
-        // Set up system to return 0.3f as a brightness value
-        float lux = 100.0f;
-        // Brightness as float (from 0.0f to 1.0f)
-        float normalizedBrightness = 0.3f;
-        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
-        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
-        when(mBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
-                /* category= */ anyInt())).thenReturn(normalizedBrightness);
-        when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
-
-        // Send a new sensor value, disable the sensor and verify
-        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
-        mController.configure(AUTO_BRIGHTNESS_DISABLED, /* configuration= */ null,
-                /* brightness= */ 0, /* userChangedBrightness= */ false, /* adjustment= */ 0,
-                /* userChanged= */ false, DisplayPowerRequest.POLICY_BRIGHT, Display.STATE_ON,
-                /* shouldResetShortTermModel= */ true);
-        assertEquals(normalizedBrightness,
-                mController.getAutomaticScreenBrightnessBasedOnLastUsedLux(
-                        /* brightnessEvent= */ null), EPSILON);
-    }
-
-    @Test
     public void testAutoBrightnessInDoze() throws Exception {
         ArgumentCaptor<SensorEventListener> listenerCaptor =
                 ArgumentCaptor.forClass(SensorEventListener.class);
@@ -1089,9 +1112,6 @@
         assertEquals(normalizedBrightness * DOZE_SCALE_FACTOR,
                 mController.getAutomaticScreenBrightness(
                         /* brightnessEvent= */ null), EPSILON);
-        assertEquals(normalizedBrightness * DOZE_SCALE_FACTOR,
-                mController.getAutomaticScreenBrightnessBasedOnLastUsedLux(
-                        /* brightnessEvent= */ null), EPSILON);
     }
 
     @Test
@@ -1113,7 +1133,7 @@
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
 
         // Switch mode to DOZE
-        mController.switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DOZE, /* sendUpdate= */ false);
 
         // Set policy to DOZE
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
@@ -1127,9 +1147,6 @@
         // The brightness should not be scaled by the doze factor
         assertEquals(normalizedBrightness,
                 mController.getAutomaticScreenBrightness(/* brightnessEvent= */ null), EPSILON);
-        assertEquals(normalizedBrightness,
-                mController.getAutomaticScreenBrightnessBasedOnLastUsedLux(
-                        /* brightnessEvent= */ null), EPSILON);
     }
 
     @Test
@@ -1162,8 +1179,63 @@
         // The brightness should not be scaled by the doze factor
         assertEquals(normalizedBrightness,
                 mController.getAutomaticScreenBrightness(/* brightnessEvent= */ null), EPSILON);
+    }
+
+    @Test
+    public void testSwitchMode_UpdateBrightnessImmediately() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // Set up system to return 0.3f as a brightness value
+        float lux = 100.0f;
+        // Brightness as float (from 0.0f to 1.0f)
+        float normalizedBrightness = 0.3f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
+        when(mDozeBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
+                /* category= */ anyInt())).thenReturn(normalizedBrightness);
+        when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
+
+        // Send a new sensor value
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
+
+        // Switch mode to DOZE
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DOZE, /* sendUpdate= */ false);
+
         assertEquals(normalizedBrightness,
-                mController.getAutomaticScreenBrightnessBasedOnLastUsedLux(
-                        /* brightnessEvent= */ null), EPSILON);
+                mController.getAutomaticScreenBrightness(/* brightnessEvent= */ null), EPSILON);
+    }
+
+    @Test
+    public void testSwitchMode_UpdateBrightnessInBackground() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // Set up system to return 0.3f as a brightness value
+        float lux = 100.0f;
+        // Brightness as float (from 0.0f to 1.0f)
+        float normalizedBrightness = 0.3f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
+        when(mDozeBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
+                /* category= */ anyInt())).thenReturn(normalizedBrightness);
+        when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
+
+        // Send a new sensor value
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
+
+        // Switch mode to DOZE
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_DOZE, /* sendUpdate= */ true);
+        mClock.fastForward(SystemClock.uptimeMillis());
+        mTestLooper.dispatchAll();
+
+        assertEquals(normalizedBrightness,
+                mController.getAutomaticScreenBrightness(/* brightnessEvent= */ null), EPSILON);
     }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 8844e6c..d0eb83a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -2513,9 +2513,8 @@
         LogicalDisplay display =
                 logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
         assertThat(display.isEnabledLocked()).isFalse();
-        // TODO(b/332711269) make sure only one DISPLAY_GROUP_EVENT_ADDED sent.
         assertThat(callback.receivedEvents()).containsExactly(DISPLAY_GROUP_EVENT_ADDED,
-                DISPLAY_GROUP_EVENT_ADDED, EVENT_DISPLAY_CONNECTED).inOrder();
+                EVENT_DISPLAY_CONNECTED).inOrder();
     }
 
     @Test
@@ -3359,8 +3358,11 @@
         }
         displayDeviceInfo.address = new TestUtils.TestDisplayAddress();
         displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
-        displayManager.getDisplayDeviceRepository()
-                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
+
+        displayManager.getDisplayHandler().runWithScissors(() -> {
+            displayManager.getDisplayDeviceRepository()
+                    .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
+        }, 0 /* now */);
         return displayDevice;
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index e5685c7..98f572d 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1021,6 +1021,36 @@
     }
 
     @Test
+    public void testAutoBrightnessEnabled_DisplayIsInDoze_OffloadAllows() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true);
+        when(mDisplayManagerFlagsMock.offloadControlsDozeAutoBrightness()).thenReturn(true);
+        when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(true);
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_DOZE,
+                Display.STATE_DOZE, /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController)
+                .setAutoBrightnessEnabled(AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED);
+    }
+
+    @Test
     public void testAutoBrightnessDisabled_ManualBrightnessMode() {
         Settings.System.putInt(mContext.getContentResolver(),
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
@@ -1067,7 +1097,7 @@
     }
 
     @Test
-    public void testAutoBrightnessDisabled_DisplayIsInDoze() {
+    public void testAutoBrightnessDisabled_DisplayIsInDoze_ConfigDoesNotAllow() {
         Settings.System.putInt(mContext.getContentResolver(),
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
                 Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
@@ -1093,6 +1123,36 @@
     }
 
     @Test
+    public void testAutoBrightnessDisabled_DisplayIsInDoze_OffloadDoesNotAllow() {
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true);
+        when(mDisplayManagerFlagsMock.offloadControlsDozeAutoBrightness()).thenReturn(true);
+        when(mDisplayOffloadSession.allowAutoBrightnessInDoze()).thenReturn(false);
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.automaticBrightnessController).configure(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                /* configuration= */ null, PowerManager.BRIGHTNESS_INVALID_FLOAT,
+                /* userChangedBrightness= */ false, /* adjustment= */ 0,
+                /* userChangedAutoBrightnessAdjustment= */ false, DisplayPowerRequest.POLICY_DOZE,
+                Display.STATE_DOZE, /* shouldResetShortTermModel= */ false
+        );
+        verify(mHolder.hbmController).setAutoBrightnessEnabled(
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE);
+    }
+
+    @Test
     public void testAutoBrightnessDisabled_FollowerDisplay() {
         Settings.System.putInt(mContext.getContentResolver(),
                 Settings.System.SCREEN_BRIGHTNESS_MODE,
@@ -1191,7 +1251,8 @@
                 /* ambientLightHorizonLong= */ anyInt(),
                 eq(lux),
                 eq(nits),
-                any(BrightnessClamperController.class)
+                any(BrightnessClamperController.class),
+                any(DisplayManagerFlags.class)
         );
     }
 
@@ -1668,7 +1729,8 @@
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.automaticBrightnessController).switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
+        verify(mHolder.automaticBrightnessController)
+                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE, /* sendUpdate= */ false);
 
         // Back to default mode
         when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_ON);
@@ -1676,7 +1738,8 @@
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mHolder.automaticBrightnessController).switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
+        verify(mHolder.automaticBrightnessController)
+                .switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT, /* sendUpdate= */ false);
     }
 
     @Test
@@ -1690,7 +1753,7 @@
         advanceTime(1); // Run updatePowerState
 
         verify(mHolder.automaticBrightnessController, never())
-                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
+                .switchMode(eq(AUTO_BRIGHTNESS_MODE_DOZE), /* sendUpdate= */ anyBoolean());
     }
 
     @Test
@@ -1703,7 +1766,7 @@
         advanceTime(1); // Run updatePowerState
 
         verify(mHolder.automaticBrightnessController, never())
-                .switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
+                .switchMode(eq(AUTO_BRIGHTNESS_MODE_DOZE), /* sendUpdate= */ anyBoolean());
     }
 
     @Test
@@ -1764,37 +1827,7 @@
     }
 
     @Test
-    public void testInitialDozeBrightness_AutoBrightnessEnabled() {
-        when(mDisplayManagerFlagsMock.areAutoBrightnessModesEnabled()).thenReturn(true);
-        when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true);
-        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, false);
-        float brightness = 0.277f;
-        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController
-                .getAutomaticScreenBrightnessBasedOnLastUsedLux(any(BrightnessEvent.class)))
-                .thenReturn(brightness);
-        when(mHolder.hbmController.getCurrentBrightnessMax())
-                .thenReturn(PowerManager.BRIGHTNESS_MAX);
-        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
-
-        DisplayPowerRequest dpr = new DisplayPowerRequest();
-        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
-        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
-
-        verify(mHolder.animator).animateTo(eq(brightness),
-                /* linearSecondTarget= */ anyFloat(), /* rate= */ anyFloat(),
-                /* ignoreAnimationLimits= */ anyBoolean());
-        verify(mHolder.brightnessSetting).setBrightness(brightness);
-    }
-
-    @Test
-    public void testInitialDozeBrightness_AutoBrightnessDisabled() {
+    public void testDozeManualBrightness() {
         when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true);
         mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
         Settings.System.putInt(mContext.getContentResolver(),
@@ -1827,22 +1860,17 @@
     }
 
     @Test
-    public void testInitialDozeBrightness_AbcIsNull() {
-        when(mDisplayManagerFlagsMock.areAutoBrightnessModesEnabled()).thenReturn(true);
+    public void testDozeManualBrightness_AbcIsNull() {
         when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true);
-        Settings.System.putInt(mContext.getContentResolver(),
-                Settings.System.SCREEN_BRIGHTNESS_MODE,
-                Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
-        mContext.getOrCreateTestableResources().addOverride(
-                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, false);
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID, /* isEnabled= */ true,
                 /* isAutoBrightnessAvailable= */ false);
         mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+        Settings.System.putInt(mContext.getContentResolver(),
+                Settings.System.SCREEN_BRIGHTNESS_MODE,
+                Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
         float brightness = 0.277f;
         when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
-        when(mHolder.automaticBrightnessController
-                .getAutomaticScreenBrightnessBasedOnLastUsedLux(any(BrightnessEvent.class)))
-                .thenReturn(brightness);
+        when(mHolder.brightnessSetting.getBrightness()).thenReturn(brightness);
         when(mHolder.hbmController.getCurrentBrightnessMax())
                 .thenReturn(PowerManager.BRIGHTNESS_MAX);
         when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
@@ -1850,13 +1878,20 @@
         DisplayPowerRequest dpr = new DisplayPowerRequest();
         dpr.policy = DisplayPowerRequest.POLICY_DOZE;
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
-        advanceTime(1); // Run updatePowerState
+        advanceTime(1); // Run updatePowerState, initialize
 
-        // Automatic Brightness Controller is null so no initial doze brightness should be set and
-        // we should not crash
-        verify(mHolder.animator, never()).animateTo(eq(brightness),
+        ArgumentCaptor<BrightnessSetting.BrightnessSettingListener> listenerCaptor =
+                ArgumentCaptor.forClass(BrightnessSetting.BrightnessSettingListener.class);
+        verify(mHolder.brightnessSetting).registerListener(listenerCaptor.capture());
+        BrightnessSetting.BrightnessSettingListener listener = listenerCaptor.getValue();
+        listener.onBrightnessChanged(brightness);
+        advanceTime(1); // Send messages, run updatePowerState
+
+        verify(mHolder.animator).animateTo(eq(brightness * DOZE_SCALE_FACTOR),
                 /* linearSecondTarget= */ anyFloat(), /* rate= */ anyFloat(),
                 /* ignoreAnimationLimits= */ anyBoolean());
+        assertEquals(brightness * DOZE_SCALE_FACTOR, mHolder.dpc.getDozeBrightnessForOffload(),
+                /* delta= */ 0);
     }
 
     @Test
@@ -1864,6 +1899,8 @@
         float brightness = 0.121f;
         when(mPowerManagerMock.getBrightnessConstraint(
                 PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE)).thenReturn(brightness);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, false);
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
         when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
         when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
@@ -1881,6 +1918,31 @@
                 eq(BRIGHTNESS_RAMP_RATE_FAST_INCREASE), eq(false));
     }
 
+    @Test
+    public void testDefaultDozeBrightness_ShouldNotBeUsedIfAutoBrightnessAllowedInDoze() {
+        float brightness = 0.121f;
+        when(mPowerManagerMock.getBrightnessConstraint(
+                PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DOZE)).thenReturn(brightness);
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.bool.config_allowAutoBrightnessWhileDozing, true);
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f);
+        when(mHolder.automaticBrightnessController.getAutomaticScreenBrightness(
+                any(BrightnessEvent.class))).thenReturn(PowerManager.BRIGHTNESS_INVALID_FLOAT);
+        when(mHolder.hbmController.getCurrentBrightnessMax())
+                .thenReturn(PowerManager.BRIGHTNESS_MAX);
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(Display.STATE_DOZE);
+
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_DOZE;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        verify(mHolder.animator, never()).animateTo(eq(brightness),
+                /* linearSecondTarget= */ anyFloat(), /* rate= */ anyFloat(),
+                /* ignoreAnimationLimits= */ anyBoolean());
+    }
+
     /**
      * Creates a mock and registers it to {@link LocalServices}.
      */
@@ -2186,7 +2248,8 @@
                 BrightnessRangeController brightnessRangeController,
                 BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
                 int ambientLightHorizonLong, float userLux, float userNits,
-                BrightnessClamperController brightnessClamperController) {
+                BrightnessClamperController brightnessClamperController,
+                DisplayManagerFlags displayManagerFlags) {
             return mAutomaticBrightnessController;
         }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
index ea08be4..82acaf8 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
@@ -317,7 +317,7 @@
                 mDisplayEventCaptor.capture());
         assertThat(mLogicalDisplayCaptor.getValue()).isEqualTo(mMockedLogicalDisplay);
         assertThat(mDisplayEventCaptor.getValue()).isEqualTo(EVENT_DISPLAY_CONNECTED);
-        verify(mMockedLogicalDisplayMapper).setDisplayEnabledLocked(eq(mMockedLogicalDisplay),
+        verify(mMockedLogicalDisplayMapper).setEnabledLocked(eq(mMockedLogicalDisplay),
                 eq(false));
         clearInvocations(mMockedLogicalDisplayMapper);
         clearInvocations(mMockedLogicalDisplay);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 7fd96c5..12050e1 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -1246,6 +1246,11 @@
 
             @Override
             public void onBlockingScreenOn(Runnable unblocker) {}
+
+            @Override
+            public boolean allowAutoBrightnessInDoze() {
+                return true;
+            }
         });
 
         mDisplayOffloadSession = new DisplayOffloadSessionImpl(mDisplayOffloader,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/TestUtils.java b/services/tests/displayservicetests/src/com/android/server/display/TestUtils.java
index 8b45145..18dfcc1 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/TestUtils.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/TestUtils.java
@@ -30,13 +30,21 @@
 public final class TestUtils {
 
     public static SensorEvent createSensorEvent(Sensor sensor, int value) throws Exception {
+        return createSensorEvent(sensor, value, SystemClock.elapsedRealtimeNanos());
+    }
+
+    /**
+     * Creates a light sensor event
+     */
+    public static SensorEvent createSensorEvent(Sensor sensor, int value, long timestamp)
+            throws Exception {
         final Constructor<SensorEvent> constructor =
                 SensorEvent.class.getDeclaredConstructor(int.class);
         constructor.setAccessible(true);
         final SensorEvent event = constructor.newInstance(1);
         event.sensor = sensor;
         event.values[0] = value;
-        event.timestamp = SystemClock.elapsedRealtimeNanos();
+        event.timestamp = timestamp;
         return event;
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
index 09f5bb6..498bffd 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
@@ -343,17 +343,11 @@
                 AutomaticBrightnessController.class);
         when(automaticBrightnessController.getAutomaticScreenBrightness(any(BrightnessEvent.class)))
                 .thenReturn(automaticScreenBrightness);
-        when(automaticBrightnessController.getAutomaticScreenBrightnessBasedOnLastUsedLux(
-                any(BrightnessEvent.class)))
-                .thenReturn(automaticScreenBrightness);
         mAutomaticBrightnessStrategy.setAutomaticBrightnessController(
                 automaticBrightnessController);
         assertEquals(automaticScreenBrightness,
                 mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(
                         new BrightnessEvent(DISPLAY_ID)), 0.0f);
-        assertEquals(automaticScreenBrightness,
-                mAutomaticBrightnessStrategy.getAutomaticScreenBrightnessBasedOnLastUsedLux(
-                        new BrightnessEvent(DISPLAY_ID)), 0.0f);
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
index d19f479..afb5a5c 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
@@ -18,6 +18,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.mock;
@@ -273,7 +274,8 @@
         mAutomaticBrightnessStrategy.setAutoBrightnessState(Display.STATE_ON,
                 allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
                 userSetBrightnessChanged);
-        verify(mAutomaticBrightnessController, never()).switchMode(anyInt());
+        verify(mAutomaticBrightnessController, never())
+                .switchMode(anyInt(), /* sendUpdate= */ anyBoolean());
 
         // Validate interaction when automaticBrightnessController is in non-idle mode, and display
         // state is ON
@@ -282,7 +284,8 @@
                 allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
                 userSetBrightnessChanged);
         verify(mAutomaticBrightnessController).switchMode(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT);
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT,
+                /* sendUpdate= */ false);
 
         // Validate interaction when automaticBrightnessController is in non-idle mode, and display
         // state is DOZE
@@ -290,7 +293,8 @@
                 allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
                 userSetBrightnessChanged);
         verify(mAutomaticBrightnessController).switchMode(
-                AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE);
+                AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE,
+                /* sendUpdate= */ false);
     }
 
     @Test
@@ -388,17 +392,11 @@
                 AutomaticBrightnessController.class);
         when(automaticBrightnessController.getAutomaticScreenBrightness(any(BrightnessEvent.class)))
                 .thenReturn(automaticScreenBrightness);
-        when(automaticBrightnessController.getAutomaticScreenBrightnessBasedOnLastUsedLux(
-                any(BrightnessEvent.class)))
-                .thenReturn(automaticScreenBrightness);
         mAutomaticBrightnessStrategy.setAutomaticBrightnessController(
                 automaticBrightnessController);
         assertEquals(automaticScreenBrightness,
                 mAutomaticBrightnessStrategy.getAutomaticScreenBrightness(
                         new BrightnessEvent(DISPLAY_ID), false), 0.0f);
-        assertEquals(automaticScreenBrightness,
-                mAutomaticBrightnessStrategy.getAutomaticScreenBrightnessBasedOnLastUsedLux(
-                        new BrightnessEvent(DISPLAY_ID)), 0.0f);
     }
 
     @Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java
index 3399565..396f4da 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java
@@ -25,6 +25,7 @@
 import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_SYSTEM_APP_NO_AGENT;
 import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSIONS_MATCH;
 import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER;
+import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_V_TO_U_RESTORE_PKG_ELIGIBLE;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -42,17 +43,23 @@
 import android.content.pm.Signature;
 import android.content.pm.SigningDetails;
 import android.content.pm.SigningInfo;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Process;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+import android.testing.TestableContext;
 
 import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.frameworks.mockingservicestests.R;
 import com.android.server.backup.FileMetadata;
+import com.android.server.backup.Flags;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.restore.PerformAdbRestoreTask;
 import com.android.server.backup.restore.RestorePolicy;
@@ -61,6 +68,7 @@
 import com.google.common.hash.Hashing;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -86,6 +94,8 @@
     @Mock private BytesReadListener mBytesReadListenerMock;
     @Mock private IBackupManagerMonitor mBackupManagerMonitorMock;
     @Mock private PackageManagerInternal mMockPackageManagerInternal;
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 
     private final PackageManagerStub mPackageManagerStub = new PackageManagerStub();
     private Context mContext;
@@ -95,7 +105,7 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        mContext = InstrumentationRegistry.getContext();
+        mContext = new TestableContext(ApplicationProvider.getApplicationContext());
         mUserId = UserHandle.USER_SYSTEM;
     }
 
@@ -515,6 +525,107 @@
 
     @Test
     public void
+    chooseRestorePolicy_flagOnNotRestoreAnyVersionVToURestoreAndInAllowlist_returnsIgnore()
+            throws Exception {
+
+        mSetFlagsRule.enableFlags(
+                Flags.FLAG_ENABLE_V_TO_U_RESTORE_FOR_SYSTEM_COMPONENTS_IN_ALLOWLIST);
+
+        TarBackupReader tarBackupReader = createTarBackupReader();
+
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.V_TO_U_RESTORE_ALLOWLIST, "test");
+
+        Signature[] signatures = new Signature[]{FAKE_SIGNATURE_1};
+        FileMetadata info = new FileMetadata();
+        info.version = Build.VERSION_CODES.UPSIDE_DOWN_CAKE + 1;
+
+        PackageInfo packageInfo = createNonRestoreAnyVersionUPackage();
+        PackageManagerStub.sPackageInfo = packageInfo;
+
+        doReturn(true).when(mMockPackageManagerInternal).isDataRestoreSafe(FAKE_SIGNATURE_1,
+                packageInfo.packageName);
+        RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
+                false /* allowApks */, info, signatures, mMockPackageManagerInternal,
+                mUserId, mContext);
+
+        assertThat(policy).isEqualTo(RestorePolicy.ACCEPT);
+        ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mBackupManagerMonitorMock).onEvent(bundleCaptor.capture());
+        assertThat(bundleCaptor.getValue().get(EXTRA_LOG_EVENT_ID)).isEqualTo(
+                LOG_EVENT_ID_V_TO_U_RESTORE_PKG_ELIGIBLE);
+    }
+
+
+    @Test
+    public void
+    chooseRestorePolicy_flagOffNotRestoreAnyVersionVToURestoreAndInAllowlist_returnsAccept()
+            throws Exception {
+
+        mSetFlagsRule.disableFlags(
+                Flags.FLAG_ENABLE_V_TO_U_RESTORE_FOR_SYSTEM_COMPONENTS_IN_ALLOWLIST);
+
+        TarBackupReader tarBackupReader = createTarBackupReader();
+
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.V_TO_U_RESTORE_ALLOWLIST, "test");
+
+        Signature[] signatures = new Signature[]{FAKE_SIGNATURE_1};
+        FileMetadata info = new FileMetadata();
+        info.version = Build.VERSION_CODES.UPSIDE_DOWN_CAKE + 1;
+
+        PackageInfo packageInfo = createNonRestoreAnyVersionUPackage();
+        PackageManagerStub.sPackageInfo = packageInfo;
+
+        doReturn(true).when(mMockPackageManagerInternal).isDataRestoreSafe(FAKE_SIGNATURE_1,
+                packageInfo.packageName);
+        RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
+                false /* allowApks */, info, signatures, mMockPackageManagerInternal,
+                mUserId, mContext);
+
+        assertThat(policy).isEqualTo(RestorePolicy.IGNORE);
+        ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mBackupManagerMonitorMock).onEvent(bundleCaptor.capture());
+        assertThat(bundleCaptor.getValue().get(EXTRA_LOG_EVENT_ID)).isEqualTo(
+                LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER);
+
+    }
+
+    @Test
+    public void
+    chooseRestorePolicy_flagOnNotRestoreAnyVersionVToURestoreAndNotInAllowlist_returnsIgnore()
+            throws Exception {
+
+        mSetFlagsRule.enableFlags(
+                Flags.FLAG_ENABLE_V_TO_U_RESTORE_FOR_SYSTEM_COMPONENTS_IN_ALLOWLIST);
+
+        TarBackupReader tarBackupReader = createTarBackupReader();
+
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.V_TO_U_RESTORE_ALLOWLIST, "pkg");
+
+        Signature[] signatures = new Signature[]{FAKE_SIGNATURE_1};
+        FileMetadata info = new FileMetadata();
+        info.version = Build.VERSION_CODES.UPSIDE_DOWN_CAKE + 1;
+
+        PackageInfo packageInfo = createNonRestoreAnyVersionUPackage();
+        PackageManagerStub.sPackageInfo = packageInfo;
+
+        doReturn(true).when(mMockPackageManagerInternal).isDataRestoreSafe(FAKE_SIGNATURE_1,
+                packageInfo.packageName);
+        RestorePolicy policy = tarBackupReader.chooseRestorePolicy(mPackageManagerStub,
+                false /* allowApks */, info, signatures, mMockPackageManagerInternal,
+                mUserId, mContext);
+
+        assertThat(policy).isEqualTo(RestorePolicy.IGNORE);
+        ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+        verify(mBackupManagerMonitorMock).onEvent(bundleCaptor.capture());
+        assertThat(bundleCaptor.getValue().get(EXTRA_LOG_EVENT_ID)).isEqualTo(
+                LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER);
+    }
+
+    @Test
+    public void
     chooseRestorePolicy_notRestoreAnyVersionAndVersionMismatchButAllowApksAndHasApk_returnsAcceptIfApk()
             throws Exception {
         InputStream inputStream = mContext.getResources().openRawResource(
@@ -523,6 +634,10 @@
                 inputStream, null);
         TarBackupReader tarBackupReader = new TarBackupReader(tarInputStream,
                 mBytesReadListenerMock, mBackupManagerMonitorMock);
+
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.V_TO_U_RESTORE_ALLOWLIST, "pkg");
+
         Signature[] signatures = new Signature[]{FAKE_SIGNATURE_1};
         FileMetadata info = new FileMetadata();
         info.version = 2;
@@ -564,6 +679,10 @@
                 inputStream, null);
         TarBackupReader tarBackupReader = new TarBackupReader(tarInputStream,
                 mBytesReadListenerMock, mBackupManagerMonitorMock);
+
+        Settings.Secure.putString(mContext.getContentResolver(),
+                Settings.Secure.V_TO_U_RESTORE_ALLOWLIST, "pkg");
+
         Signature[] signatures = new Signature[]{FAKE_SIGNATURE_1};
         FileMetadata info = new FileMetadata();
         info.version = 2;
@@ -596,5 +715,33 @@
         assertThat(bundleCaptor.getValue().get(EXTRA_LOG_EVENT_ID)).isEqualTo(
                 LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER);
     }
+
+    private TarBackupReader createTarBackupReader() throws Exception {
+        InputStream inputStream = mContext.getResources().openRawResource(
+                R.raw.backup_telephony_no_password);
+        InputStream tarInputStream = PerformAdbRestoreTask.parseBackupFileHeaderAndReturnTarStream(
+                inputStream, null);
+        TarBackupReader tarBackupReader = new TarBackupReader(tarInputStream,
+                mBytesReadListenerMock, mBackupManagerMonitorMock);
+        return tarBackupReader;
+    }
+
+    private PackageInfo createNonRestoreAnyVersionUPackage(){
+        PackageInfo packageInfo = new PackageInfo();
+        packageInfo.packageName = "test";
+        packageInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.applicationInfo.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP;
+        packageInfo.applicationInfo.flags &= ~ApplicationInfo.FLAG_RESTORE_ANY_VERSION;
+        packageInfo.applicationInfo.uid = Process.FIRST_APPLICATION_UID;
+        packageInfo.applicationInfo.backupAgentName = null;
+        packageInfo.signingInfo = new SigningInfo(
+                new SigningDetails(
+                        new Signature[]{FAKE_SIGNATURE_1},
+                        SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3,
+                        null,
+                        null));
+        packageInfo.versionCode = Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
+        return packageInfo;
+    }
 }
 
diff --git a/services/tests/mockingservicestests/src/com/android/server/location/gnss/hal/GnssNativeTest.java b/services/tests/mockingservicestests/src/com/android/server/location/gnss/hal/GnssNativeTest.java
new file mode 100644
index 0000000..a14bcaf
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/location/gnss/hal/GnssNativeTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.location.gnss.hal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.location.gnss.GnssConfiguration;
+import com.android.server.location.gnss.GnssPowerStats;
+import com.android.server.location.injector.Injector;
+import com.android.server.location.injector.TestInjector;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class GnssNativeTest {
+
+    private @Mock Context mContext;
+    private @Mock GnssConfiguration mMockConfiguration;
+    private FakeGnssHal mFakeGnssHal;
+    private GnssNative mGnssNative;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mFakeGnssHal = new FakeGnssHal();
+        GnssNative.setGnssHalForTest(mFakeGnssHal);
+        Injector injector = new TestInjector(mContext);
+        mGnssNative = spy(Objects.requireNonNull(GnssNative.create(injector, mMockConfiguration)));
+        mGnssNative.register();
+    }
+
+    @Test
+    public void testRequestPowerStats_onNull_executesCallbackWithNull() {
+        mFakeGnssHal.setPowerStats(null);
+        Executor executor = spy(Runnable::run);
+        GnssNative.PowerStatsCallback callback = spy(stats -> {});
+
+        mGnssNative.requestPowerStats(executor, callback);
+
+        verify(executor).execute(any());
+        verify(callback).onReportPowerStats(null);
+    }
+
+    @Test
+    public void testRequestPowerStats_onPowerStats_executesCallbackWithStats() {
+        GnssPowerStats powerStats = new GnssPowerStats(1, 2, 3, 4, 5, 6, 7, 8, new double[]{9, 10});
+        mFakeGnssHal.setPowerStats(powerStats);
+        Executor executor = spy(Runnable::run);
+        GnssNative.PowerStatsCallback callback = spy(stats -> {});
+
+        mGnssNative.requestPowerStats(executor, callback);
+
+        verify(executor).execute(any());
+        verify(callback).onReportPowerStats(powerStats);
+    }
+
+    @Test
+    public void testRequestPowerStatsBlocking_onNull_returnsNull() {
+        mFakeGnssHal.setPowerStats(null);
+
+        assertThat(mGnssNative.requestPowerStatsBlocking()).isNull();
+    }
+
+    @Test
+    public void testRequestPowerStatsBlocking_onPowerStats_returnsStats() {
+        GnssPowerStats powerStats = new GnssPowerStats(1, 2, 3, 4, 5, 6, 7, 8, new double[]{9, 10});
+        mFakeGnssHal.setPowerStats(powerStats);
+
+        assertThat(mGnssNative.requestPowerStatsBlocking()).isEqualTo(powerStats);
+    }
+
+    @Test
+    public void testGetLastKnownPowerStats_onNull_preservesLastKnownPowerStats() {
+        GnssPowerStats powerStats = new GnssPowerStats(1, 2, 3, 4, 5, 6, 7, 8, new double[]{9, 10});
+
+        mGnssNative.reportGnssPowerStats(powerStats);
+        assertThat(mGnssNative.getLastKnownPowerStats()).isEqualTo(powerStats);
+
+        mGnssNative.reportGnssPowerStats(null);
+        assertThat(mGnssNative.getLastKnownPowerStats()).isEqualTo(powerStats);
+    }
+
+    @Test
+    public void testGetLastKnownPowerStats_onPowerStats_updatesLastKnownPowerStats() {
+        GnssPowerStats powerStats1 = new GnssPowerStats(1, 2, 3, 4, 5, 6, 7, 8, new double[]{9, 0});
+        GnssPowerStats powerStats2 = new GnssPowerStats(2, 3, 4, 5, 6, 7, 8, 9, new double[]{0, 9});
+
+        mGnssNative.reportGnssPowerStats(powerStats1);
+        assertThat(mGnssNative.getLastKnownPowerStats()).isEqualTo(powerStats1);
+
+        mGnssNative.reportGnssPowerStats(powerStats2);
+        assertThat(mGnssNative.getLastKnownPowerStats()).isEqualTo(powerStats2);
+    }
+
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BinaryStatePowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BinaryStatePowerStatsProcessorTest.java
new file mode 100644
index 0000000..be1c121
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BinaryStatePowerStatsProcessorTest.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.power.stats;
+
+import static android.os.BatteryConsumer.PROCESS_STATE_BACKGROUND;
+import static android.os.BatteryConsumer.PROCESS_STATE_CACHED;
+import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND;
+import static android.os.BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE;
+
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.POWER_STATE_OTHER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_ON;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.SCREEN_STATE_OTHER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_POWER;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_PROCESS_STATE;
+import static com.android.server.power.stats.AggregatedPowerStatsConfig.STATE_SCREEN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.BatteryConsumer;
+import android.os.BatteryStats;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.os.MonotonicClock;
+import com.android.internal.os.PowerStats;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+public class BinaryStatePowerStatsProcessorTest {
+    @Rule(order = 0)
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProvideMainThread(true)
+            .build();
+
+    private static final double PRECISION = 0.00001;
+    private static final int APP_UID1 = Process.FIRST_APPLICATION_UID + 42;
+    private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 101;
+    private static final int POWER_COMPONENT = BatteryConsumer.POWER_COMPONENT_AUDIO;
+    private static final int TEST_STATE_FLAG = 0x1;
+
+    private final MockClock mClock = new MockClock();
+    private final MonotonicClock mMonotonicClock = new MonotonicClock(0, mClock);
+    private final PowerStatsUidResolver mUidResolver = new PowerStatsUidResolver();
+
+    private static class TestBinaryStatePowerStatsProcessor extends BinaryStatePowerStatsProcessor {
+        TestBinaryStatePowerStatsProcessor(int powerComponentId,
+                double averagePowerMilliAmp, PowerStatsUidResolver uidResolver) {
+            super(powerComponentId, uidResolver, averagePowerMilliAmp);
+        }
+
+        @Override
+        protected int getBinaryState(BatteryStats.HistoryItem item) {
+            return (item.states & TEST_STATE_FLAG) != 0 ? STATE_ON : STATE_OFF;
+        }
+    }
+
+    @Test
+    public void powerProfileModel() {
+        TestBinaryStatePowerStatsProcessor processor = new TestBinaryStatePowerStatsProcessor(
+                POWER_COMPONENT,  /* averagePowerMilliAmp */ 100, mUidResolver);
+
+        BinaryStatePowerStatsLayout statsLayout = new BinaryStatePowerStatsLayout();
+
+        PowerComponentAggregatedPowerStats stats = createAggregatedPowerStats(processor);
+
+        processor.noteStateChange(stats, buildHistoryItem(0, true, APP_UID1));
+
+        // Turn the screen off after 2.5 seconds
+        stats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500);
+        stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500);
+        stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, 5000);
+
+        processor.noteStateChange(stats, buildHistoryItem(6000, false, APP_UID1));
+
+        processor.noteStateChange(stats, buildHistoryItem(7000, true, APP_UID2));
+
+        processor.finish(stats, 11000);
+
+        // Total usage duration is 10000
+        // Total estimated power = 10000 * 100 = 1000000 mA-ms = 0.277777 mAh
+        // Screen-on  - 25%
+        // Screen-off - 75%
+        double expectedPower = 0.277778;
+        long[] deviceStats = new long[stats.getPowerStatsDescriptor().statsArrayLength];
+        stats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_ON));
+        assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+                .isWithin(PRECISION).of(expectedPower * 0.25);
+
+        stats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_OTHER));
+        assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+                .isWithin(PRECISION).of(expectedPower * 0.75);
+
+        // UID1 =
+        //     6000 * 100 = 600000 mA-ms = 0.166666 mAh
+        //     split between three different states
+        double expectedPower1 = 0.166666;
+        long[] uidStats = new long[stats.getPowerStatsDescriptor().uidStatsArrayLength];
+        stats.getUidStats(uidStats, APP_UID1,
+                states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower1 * 2500 / 6000);
+
+        stats.getUidStats(uidStats, APP_UID1,
+                states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_BACKGROUND));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower1 * 2500 / 6000);
+
+        stats.getUidStats(uidStats, APP_UID1,
+                states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower1 * 1000 / 6000);
+
+        // UID2 =
+        //     4000 * 100 = 400000 mA-ms = 0.111111 mAh
+        //     all in the same state
+        double expectedPower2 = 0.111111;
+        stats.getUidStats(uidStats, APP_UID2,
+                states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower2);
+
+        stats.getUidStats(uidStats, APP_UID2,
+                states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(0);
+    }
+
+    @Test
+    public void energyConsumerModel() {
+        TestBinaryStatePowerStatsProcessor processor = new TestBinaryStatePowerStatsProcessor(
+                POWER_COMPONENT,  /* averagePowerMilliAmp */ 100, mUidResolver);
+
+        BinaryStatePowerStatsLayout statsLayout = new BinaryStatePowerStatsLayout();
+        PersistableBundle extras = new PersistableBundle();
+        statsLayout.toExtras(extras);
+        PowerStats.Descriptor descriptor = new PowerStats.Descriptor(POWER_COMPONENT,
+                statsLayout.getDeviceStatsArrayLength(), null, 0,
+                statsLayout.getUidStatsArrayLength(), extras);
+        PowerStats powerStats = new PowerStats(descriptor);
+        powerStats.stats = new long[descriptor.statsArrayLength];
+
+        PowerComponentAggregatedPowerStats stats = createAggregatedPowerStats(processor);
+
+        // Establish a baseline
+        processor.addPowerStats(stats, powerStats, mMonotonicClock.monotonicTime());
+
+        processor.noteStateChange(stats, buildHistoryItem(0, true, APP_UID1));
+
+        // Turn the screen off after 2.5 seconds
+        stats.setState(STATE_SCREEN, SCREEN_STATE_OTHER, 2500);
+        stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_BACKGROUND, 2500);
+        stats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND_SERVICE, 5000);
+
+        processor.noteStateChange(stats, buildHistoryItem(6000, false, APP_UID1));
+
+        statsLayout.setConsumedEnergy(powerStats.stats, 0, 2_160_000);
+        processor.addPowerStats(stats, powerStats, mMonotonicClock.monotonicTime());
+
+        processor.noteStateChange(stats, buildHistoryItem(7000, true, APP_UID2));
+
+        mClock.realtime = 11000;
+        statsLayout.setConsumedEnergy(powerStats.stats, 0, 1_440_000);
+        processor.addPowerStats(stats, powerStats, mMonotonicClock.monotonicTime());
+
+        processor.finish(stats, 11000);
+
+        // Total estimated power = 3,600,000 uC = 1.0 mAh
+        // of which 3,000,000 is distributed:
+        //     Screen-on  - 2500/6000 * 2160000 = 900000 uC = 0.25 mAh
+        //     Screen-off - 3500/6000 * 2160000 = 1260000 uC = 0.35 mAh
+        // and 600,000 was fully with screen off:
+        //     Screen-off - 1440000 uC = 0.4 mAh
+        long[] deviceStats = new long[descriptor.statsArrayLength];
+        stats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_ON));
+        assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+                .isWithin(PRECISION).of(0.25);
+
+        stats.getDeviceStats(deviceStats, states(POWER_STATE_OTHER, SCREEN_STATE_OTHER));
+        assertThat(statsLayout.getDevicePowerEstimate(deviceStats))
+                .isWithin(PRECISION).of(0.35 + 0.4);
+
+        // UID1 =
+        //     2,160,000 uC = 0.6 mAh
+        //     split between three different states
+        //          fg screen-on: 2500/6000
+        //          bg screen-off: 2500/6000
+        //          fgs screen-off: 1000/6000
+        double expectedPower1 = 0.6;
+        long[] uidStats = new long[descriptor.uidStatsArrayLength];
+        stats.getUidStats(uidStats, APP_UID1,
+                states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_FOREGROUND));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower1 * 2500 / 6000);
+
+        stats.getUidStats(uidStats, APP_UID1,
+                states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_BACKGROUND));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower1 * 2500 / 6000);
+
+        stats.getUidStats(uidStats, APP_UID1,
+                states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_FOREGROUND_SERVICE));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower1 * 1000 / 6000);
+
+        // UID2 =
+        //     1440000 mA-ms = 0.4 mAh
+        //     all in the same state
+        double expectedPower2 = 0.4;
+        stats.getUidStats(uidStats, APP_UID2,
+                states(POWER_STATE_OTHER, SCREEN_STATE_OTHER, PROCESS_STATE_CACHED));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(expectedPower2);
+
+        stats.getUidStats(uidStats, APP_UID2,
+                states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED));
+        assertThat(statsLayout.getUidPowerEstimate(uidStats))
+                .isWithin(PRECISION).of(0);
+    }
+
+
+    @NonNull
+    private BatteryStats.HistoryItem buildHistoryItem(int elapsedRealtime, boolean stateOn,
+            int uid) {
+        mClock.realtime = elapsedRealtime;
+        BatteryStats.HistoryItem historyItem = new BatteryStats.HistoryItem();
+        historyItem.time = mMonotonicClock.monotonicTime();
+        historyItem.states = stateOn ? TEST_STATE_FLAG : 0;
+        if (stateOn) {
+            historyItem.eventCode = BatteryStats.HistoryItem.EVENT_STATE_CHANGE
+                    | BatteryStats.HistoryItem.EVENT_FLAG_START;
+        } else {
+            historyItem.eventCode = BatteryStats.HistoryItem.EVENT_STATE_CHANGE
+                    | BatteryStats.HistoryItem.EVENT_FLAG_FINISH;
+        }
+        historyItem.eventTag = historyItem.localEventTag;
+        historyItem.eventTag.uid = uid;
+        historyItem.eventTag.string = "test";
+        return historyItem;
+    }
+
+    private int[] states(int... states) {
+        return states;
+    }
+
+    private static PowerComponentAggregatedPowerStats createAggregatedPowerStats(
+            BinaryStatePowerStatsProcessor processor) {
+        AggregatedPowerStatsConfig config = new AggregatedPowerStatsConfig();
+        config.trackPowerComponent(POWER_COMPONENT)
+                .trackDeviceStates(STATE_POWER, STATE_SCREEN)
+                .trackUidStates(STATE_POWER, STATE_SCREEN, STATE_PROCESS_STATE)
+                .setProcessor(processor);
+
+        AggregatedPowerStats aggregatedPowerStats = new AggregatedPowerStats(config);
+        PowerComponentAggregatedPowerStats powerComponentStats =
+                aggregatedPowerStats.getPowerComponentStats(POWER_COMPONENT);
+        processor.start(powerComponentStats, 0);
+
+        powerComponentStats.setState(STATE_POWER, POWER_STATE_OTHER, 0);
+        powerComponentStats.setState(STATE_SCREEN, SCREEN_STATE_ON, 0);
+        powerComponentStats.setUidState(APP_UID1, STATE_PROCESS_STATE, PROCESS_STATE_FOREGROUND, 0);
+        powerComponentStats.setUidState(APP_UID2, STATE_PROCESS_STATE, PROCESS_STATE_CACHED, 0);
+
+        return powerComponentStats;
+    }
+}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsProcessorTest.java
index 752bc27..c88f0a9 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerStatsProcessorTest.java
@@ -200,7 +200,7 @@
 
         aggregatedStats.addPowerStats(collector.collectStats(), 10_000);
 
-        processor.finish(aggregatedStats);
+        processor.finish(aggregatedStats, 10_000);
 
         BluetoothPowerStatsLayout statsLayout =
                 new BluetoothPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor());
@@ -301,7 +301,7 @@
 
         aggregatedStats.addPowerStats(collector.collectStats(), 10_000);
 
-        processor.finish(aggregatedStats);
+        processor.finish(aggregatedStats, 10_000);
 
         BluetoothPowerStatsLayout statsLayout =
                 new BluetoothPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor());
@@ -408,7 +408,7 @@
 
         aggregatedStats.addPowerStats(collector.collectStats(), 10_000);
 
-        processor.finish(aggregatedStats);
+        processor.finish(aggregatedStats, 10_000);
 
         BluetoothPowerStatsLayout statsLayout =
                 new BluetoothPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor());
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsProcessorTest.java
index 6b5da81..b6b759e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsProcessorTest.java
@@ -128,7 +128,7 @@
                 states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED),
                 values(1500, 2000, 1000), 1.252578);
 
-        mProcessor.finish(mStats);
+        mProcessor.finish(mStats, 10_000);
 
         mStats.verifyPowerEstimates();
     }
@@ -173,7 +173,7 @@
                 states(POWER_STATE_OTHER, SCREEN_STATE_ON, PROCESS_STATE_CACHED),
                 values(1500, 2000, 1000), 0.80773);
 
-        mProcessor.finish(mStats);
+        mProcessor.finish(mStats, 10_000);
 
         mStats.verifyPowerEstimates();
     }
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsProcessorTest.java
index 29ef3b6..137c2a6 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MobileRadioPowerStatsProcessorTest.java
@@ -228,7 +228,7 @@
 
         aggregatedStats.addPowerStats(powerStats, 10_000);
 
-        processor.finish(aggregatedStats);
+        processor.finish(aggregatedStats, 10_000);
 
         MobileRadioPowerStatsLayout statsLayout =
                 new MobileRadioPowerStatsLayout(
@@ -475,7 +475,7 @@
 
         aggregatedStats.addPowerStats(powerStats, 10_000);
 
-        processor.finish(aggregatedStats);
+        processor.finish(aggregatedStats, 10_000);
         return aggregatedStats;
     }
 
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PhoneCallPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PhoneCallPowerStatsProcessorTest.java
index 69d655b..548d54c 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/PhoneCallPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PhoneCallPowerStatsProcessorTest.java
@@ -203,11 +203,11 @@
 
         aggregatedPowerStats.addPowerStats(collector.collectStats(), 10_000);
 
-        mobileStatsProcessor.finish(mobileRadioStats);
+        mobileStatsProcessor.finish(mobileRadioStats, 10_000);
 
         PowerComponentAggregatedPowerStats stats =
                 aggregatedPowerStats.getPowerComponentStats(BatteryConsumer.POWER_COMPONENT_PHONE);
-        phoneStatsProcessor.finish(stats);
+        phoneStatsProcessor.finish(stats, 10_000);
 
         PowerStatsLayout statsLayout =
                 new PowerStatsLayout(stats.getPowerStatsDescriptor());
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsProcessorTest.java
index 3ceaf35..ff56691 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerStatsProcessorTest.java
@@ -234,7 +234,7 @@
 
         aggregatedStats.addPowerStats(collector.collectStats(), 10_000);
 
-        processor.finish(aggregatedStats);
+        processor.finish(aggregatedStats, 10_000);
 
         WifiPowerStatsLayout statsLayout =
                 new WifiPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor());
@@ -355,7 +355,7 @@
 
         aggregatedStats.addPowerStats(collector.collectStats(), 10_000);
 
-        processor.finish(aggregatedStats);
+        processor.finish(aggregatedStats, 10_000);
 
         WifiPowerStatsLayout statsLayout =
                 new WifiPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor());
@@ -454,7 +454,7 @@
 
         aggregatedStats.addPowerStats(collector.collectStats(), 10_000);
 
-        processor.finish(aggregatedStats);
+        processor.finish(aggregatedStats, 10_000);
 
         WifiPowerStatsLayout statsLayout =
                 new WifiPowerStatsLayout(aggregatedStats.getPowerStatsDescriptor());
diff --git a/services/tests/servicestests/assets/AppOpsPersistenceTest/recent_accesses.xml b/services/tests/servicestests/assets/AppOpsPersistenceTest/recent_accesses.xml
new file mode 100644
index 0000000..5aceea3
--- /dev/null
+++ b/services/tests/servicestests/assets/AppOpsPersistenceTest/recent_accesses.xml
@@ -0,0 +1,11 @@
+<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
+<app-ops v="1">
+    <pkg n="com.android.servicestests.apps.testapp">
+        <uid n="10001">
+            <op n="26">
+                <st id="attribution.tag.test.1" n="429496729601" t="1710799464518" d="2963" />
+                <st n="1073741824008" dv="companion:1" t="1712610342977" d="7596" pp="com.android.servicestests.apps.proxy" pc="com.android.servicestests.apps.proxy.attrtag" pu="10002" pdv="companion:2" />
+            </op>
+        </uid>
+    </pkg>
+</app-ops>
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsRecentAccessPersistenceTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsRecentAccessPersistenceTest.java
new file mode 100644
index 0000000..c4b3c149
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsRecentAccessPersistenceTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appop;
+
+import static android.app.AppOpsManager.OP_FLAGS_ALL;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.companion.virtual.VirtualDeviceManager;
+import android.content.Context;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.LocalServices;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+@RunWith(AndroidJUnit4.class)
+public class AppOpsRecentAccessPersistenceTest {
+    private static final String TAG = AppOpsRecentAccessPersistenceTest.class.getSimpleName();
+    private static final String TEST_XML = "AppOpsPersistenceTest/recent_accesses.xml";
+
+    private final Context mContext =
+            InstrumentationRegistry.getInstrumentation().getTargetContext();
+    private File mMockDataDirectory;
+    private File mRecentAccessFile;
+    private AppOpsService mAppOpsService;
+
+    @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+    @Mock private AppOpsServiceTestingShim mAppOpCheckingService;
+
+    @Before
+    public void setUp() {
+        when(mAppOpCheckingService.addAppOpsModeChangedListener(any())).thenReturn(true);
+        LocalServices.addService(AppOpsCheckingServiceInterface.class, mAppOpCheckingService);
+
+        mMockDataDirectory = mContext.getDir("mock_data", Context.MODE_PRIVATE);
+        mRecentAccessFile = new File(mMockDataDirectory, "test_accesses.xml");
+
+        HandlerThread handlerThread = new HandlerThread(TAG);
+        handlerThread.start();
+        Handler handler = new Handler(handlerThread.getLooper());
+        mAppOpsService = new AppOpsService(mRecentAccessFile, mRecentAccessFile, handler, mContext);
+    }
+
+    @After
+    public void cleanUp() {
+        FileUtils.deleteContents(mMockDataDirectory);
+    }
+
+    @Test
+    public void readAndWriteRecentAccesses() throws Exception {
+        copyRecentAccessFromAsset(mContext, TEST_XML, mRecentAccessFile);
+        SparseArray<AppOpsService.UidState> uidStates = new SparseArray<>();
+
+        AtomicFile recentAccessFile = new AtomicFile(mRecentAccessFile);
+        AppOpsRecentAccessPersistence persistence =
+                new AppOpsRecentAccessPersistence(recentAccessFile, mAppOpsService);
+
+        persistence.readRecentAccesses(uidStates);
+        validateUidStates(uidStates);
+
+        // Now we clear the xml file and write uidStates to it, then read again to verify data
+        // written to the xml is correct.
+        recentAccessFile.delete();
+        persistence.writeRecentAccesses(uidStates);
+
+        SparseArray<AppOpsService.UidState> newUidStates = new SparseArray<>();
+        persistence.readRecentAccesses(newUidStates);
+        validateUidStates(newUidStates);
+    }
+
+    // We compare data loaded into uidStates with original data in recent_accesses.xml
+    private void validateUidStates(SparseArray<AppOpsService.UidState> uidStates) {
+        assertThat(uidStates.size()).isEqualTo(1);
+
+        AppOpsService.UidState uidState = uidStates.get(10001);
+        assertThat(uidState.uid).isEqualTo(10001);
+
+        ArrayMap<String, AppOpsService.Ops> packageOps = uidState.pkgOps;
+        assertThat(packageOps.size()).isEqualTo(1);
+
+        AppOpsService.Ops ops = packageOps.get("com.android.servicestests.apps.testapp");
+        assertThat(ops.size()).isEqualTo(1);
+
+        AppOpsService.Op op = ops.get(26);
+        assertThat(op.mDeviceAttributedOps.size()).isEqualTo(2);
+
+        // Test AppOp access for the default device
+        AttributedOp attributedOp =
+                op.mDeviceAttributedOps
+                        .get(VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)
+                        .get("attribution.tag.test.1");
+        assertThat(attributedOp.persistentDeviceId)
+                .isEqualTo(VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
+        assertThat(attributedOp.tag).isEqualTo("attribution.tag.test.1");
+
+        AppOpsManager.AttributedOpEntry attributedOpEntry =
+                attributedOp.createAttributedOpEntryLocked();
+
+        assertThat(attributedOpEntry.getLastAccessTime(OP_FLAGS_ALL)).isEqualTo(1710799464518L);
+        assertThat(attributedOpEntry.getLastDuration(OP_FLAGS_ALL)).isEqualTo(2963);
+
+        // Test AppOp access for an external device
+        AttributedOp attributedOpForDevice = op.mDeviceAttributedOps.get("companion:1").get(null);
+        assertThat(attributedOpForDevice.persistentDeviceId).isEqualTo("companion:1");
+
+        AppOpsManager.AttributedOpEntry attributedOpEntryForDevice =
+                attributedOpForDevice.createAttributedOpEntryLocked();
+        assertThat(attributedOpEntryForDevice.getLastAccessTime(OP_FLAGS_ALL))
+                .isEqualTo(1712610342977L);
+        assertThat(attributedOpEntryForDevice.getLastDuration(OP_FLAGS_ALL)).isEqualTo(7596);
+
+        AppOpsManager.OpEventProxyInfo proxyInfo =
+                attributedOpEntryForDevice.getLastProxyInfo(OP_FLAGS_ALL);
+        assertThat(proxyInfo.getUid()).isEqualTo(10002);
+        assertThat(proxyInfo.getPackageName()).isEqualTo("com.android.servicestests.apps.proxy");
+        assertThat(proxyInfo.getAttributionTag())
+                .isEqualTo("com.android.servicestests.apps.proxy.attrtag");
+        assertThat(proxyInfo.getDeviceId()).isEqualTo("companion:2");
+    }
+
+    private static void copyRecentAccessFromAsset(Context context, String xmlAsset, File outFile)
+            throws IOException {
+        writeToFile(outFile, readAsset(context, xmlAsset));
+    }
+
+    private static String readAsset(Context context, String assetPath) throws IOException {
+        final StringBuilder sb = new StringBuilder();
+        try (BufferedReader br =
+                new BufferedReader(
+                        new InputStreamReader(
+                                context.getResources().getAssets().open(assetPath)))) {
+            String line;
+            while ((line = br.readLine()) != null) {
+                sb.append(line);
+                sb.append(System.lineSeparator());
+            }
+        }
+        return sb.toString();
+    }
+
+    private static void writeToFile(File path, String content) throws IOException {
+        path.getParentFile().mkdirs();
+
+        try (FileWriter writer = new FileWriter(path)) {
+            writer.write(content);
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 2470403..e72d9e7 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -5590,6 +5590,12 @@
         mContext.binder.callingUid = managedProfileAdminUid;
         addManagedProfile(admin1, managedProfileAdminUid, admin1, VERSION_CODES.R);
 
+        // Profile has a unified challenge
+        doReturn(false).when(getServices().lockPatternUtils)
+                .isSeparateProfileChallengeEnabled(managedProfileUserId);
+        doReturn(true).when(getServices().lockPatternUtils)
+                .isProfileWithUnifiedChallenge(managedProfileUserId);
+
         dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH);
         parentDpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_LOW);
 
@@ -5610,6 +5616,12 @@
         mContext.binder.callingUid = managedProfileAdminUid;
         addManagedProfile(admin1, managedProfileAdminUid, admin1, VERSION_CODES.R);
 
+        // Profile has a unified challenge
+        doReturn(false).when(getServices().lockPatternUtils)
+                .isSeparateProfileChallengeEnabled(managedProfileUserId);
+        doReturn(true).when(getServices().lockPatternUtils)
+                .isProfileWithUnifiedChallenge(managedProfileUserId);
+
         dpm.setPasswordQuality(admin1, DevicePolicyManager.PASSWORD_QUALITY_COMPLEX);
         dpm.setPasswordMinimumLength(admin1, 8);
         dpm.setPasswordMinimumLetters(admin1, 1);
@@ -5870,6 +5882,8 @@
                 .thenReturn(new UserInfo(UserHandle.USER_SYSTEM, "user system", 0));
         doReturn(separateChallenge).when(getServices().lockPatternUtils)
                 .isSeparateProfileChallengeEnabled(userId);
+        doReturn(!separateChallenge).when(getServices().lockPatternUtils)
+                .isProfileWithUnifiedChallenge(userId);
         when(getServices().userManager.getCredentialOwnerProfile(userId))
                 .thenReturn(separateChallenge ? userId : UserHandle.USER_SYSTEM);
         when(getServices().lockSettingsInternal.getUserPasswordMetrics(userId))
@@ -7631,6 +7645,7 @@
 
         addManagedProfile(admin1, managedProfileAdminUid, admin1);
         mContext.binder.callingUid = managedProfileAdminUid;
+        when(getServices().userManager.isManagedProfile()).thenReturn(true);
 
         final Set<Integer> allowedModes = Set.of(PASSWORD_COMPLEXITY_NONE, PASSWORD_COMPLEXITY_LOW,
                 PASSWORD_COMPLEXITY_MEDIUM, PASSWORD_COMPLEXITY_HIGH);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
index c1ae852..6d86301 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
@@ -86,6 +86,8 @@
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(mContextSpy));
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
         mNativeWrapper = new FakeNativeWrapper();
+        mPhysicalAddress = 0x2000;
+        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper(
                 this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(hdmiCecController);
@@ -94,8 +96,6 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mPhysicalAddress = 0x2000;
-        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         mTestLooper.dispatchAll();
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
index e669e7c..2296911 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
@@ -104,6 +104,8 @@
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(mContextSpy));
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
         mNativeWrapper = new FakeNativeWrapper();
+        mPhysicalAddress = 0x2000;
+        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper(
                 this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(hdmiCecController);
@@ -112,8 +114,6 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mPhysicalAddress = 0x2000;
-        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         mTestLooper.dispatchAll();
         mPlaybackDevice = mHdmiControlService.playback();
         mDevicePowerStatusAction = DevicePowerStatusAction.create(mPlaybackDevice, ADDR_TV,
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
index 29d20a6..47cfa42 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
@@ -137,6 +137,7 @@
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
         mNativeWrapper = new FakeNativeWrapper();
+        mNativeWrapper.setPhysicalAddress(0x0000);
         mHdmiCecController = HdmiCecController.createWithNativeWrapper(
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
@@ -150,7 +151,7 @@
 
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
-        mNativeWrapper.setPhysicalAddress(0x0000);
+
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mTestLooper.dispatchAll();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
index d32b75b..eb4a628 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
@@ -146,6 +146,7 @@
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
         mNativeWrapper = new FakeNativeWrapper();
+        mNativeWrapper.setPhysicalAddress(0x0000);
         mHdmiCecController = HdmiCecController.createWithNativeWrapper(
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
@@ -168,7 +169,6 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mNativeWrapper.setPhysicalAddress(0x0000);
         mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
         mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_PLAYBACK_1);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java b/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java
index cb19029..bfe435c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/FakeNativeWrapper.java
@@ -62,6 +62,7 @@
     private HdmiCecController.HdmiCecCallback mCallback = null;
     private int mCecVersion = HdmiControlManager.HDMI_CEC_VERSION_2_0;
     private boolean mIsCecControlEnabled = true;
+    private boolean mGetPhysicalAddressCalled = false;
 
     @Override
     public String nativeInit() {
@@ -96,6 +97,7 @@
 
     @Override
     public int nativeGetPhysicalAddress() {
+        mGetPhysicalAddressCalled = true;
         return mMyPhysicalAddress;
     }
 
@@ -161,6 +163,10 @@
         return mIsCecControlEnabled;
     }
 
+    public boolean getPhysicalAddressCalled() {
+        return mGetPhysicalAddressCalled;
+    }
+
     public void setCecVersion(@HdmiControlManager.HdmiCecVersion int cecVersion) {
         mCecVersion = cecVersion;
     }
@@ -200,6 +206,10 @@
         mResultMessages.clear();
     }
 
+    public void clearGetPhysicalAddressCallHistory() {
+        mGetPhysicalAddressCalled = false;
+    }
+
     public void setPollAddressResponse(int logicalAddress, int response) {
         mPollAddressResponse[logicalAddress] = response;
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 5502de8..0244164 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -472,6 +472,7 @@
         HdmiCecMessage message =
                 HdmiCecMessageBuilder.buildRequestArcInitiation(ADDR_TV, ADDR_AUDIO_SYSTEM);
         mNativeWrapper.setPhysicalAddress(0x1100);
+        mHdmiControlService.onHotplug(0x1100, true);
 
         assertThat(mHdmiCecLocalDeviceAudioSystem.handleRequestArcInitiate(message))
                 .isEqualTo(Constants.ABORT_NOT_IN_CORRECT_MODE);
@@ -482,6 +483,8 @@
         HdmiCecMessage message =
                 HdmiCecMessageBuilder.buildRequestArcInitiation(ADDR_TV, ADDR_AUDIO_SYSTEM);
         mNativeWrapper.setPhysicalAddress(0x1000);
+        mHdmiControlService.onHotplug(0x1000, true);
+
         mHdmiCecLocalDeviceAudioSystem.removeAction(ArcInitiationActionFromAvr.class);
 
         assertThat(mHdmiCecLocalDeviceAudioSystem.handleRequestArcInitiate(message))
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index 8df7d54..95a7f4b 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -167,6 +167,7 @@
             }
         };
         mHdmiCecLocalDevicePlayback.init();
+        mPlaybackPhysicalAddress = 0x2000;
         HdmiPortInfo[] hdmiPortInfos = new HdmiPortInfo[1];
         hdmiPortInfos[0] =
                 new HdmiPortInfo.Builder(1, HdmiPortInfo.PORT_OUTPUT, 0x0000)
@@ -176,13 +177,12 @@
                         .build();
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
+        mNativeWrapper.setPhysicalAddress(mPlaybackPhysicalAddress);
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.setPowerManagerInternal(mPowerManagerInternal);
-        mPlaybackPhysicalAddress = 0x2000;
-        mNativeWrapper.setPhysicalAddress(mPlaybackPhysicalAddress);
         mTestLooper.dispatchAll();
         mLocalDevices.add(mHdmiCecLocalDevicePlayback);
         mHdmiControlService.getHdmiCecNetwork().addCecDevice(INFO_TV);
@@ -416,6 +416,7 @@
         int newPlaybackPhysicalAddress = 0x2100;
         int switchPhysicalAddress = 0x2000;
         mNativeWrapper.setPhysicalAddress(newPlaybackPhysicalAddress);
+        mHdmiControlService.onHotplug(newPlaybackPhysicalAddress, true);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
 
         mHdmiCecLocalDevicePlayback.mService.getHdmiCecConfig().setStringValue(
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
index 192be2a..004c6c6 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTest.java
@@ -181,6 +181,7 @@
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
         mNativeWrapper = new FakeNativeWrapper();
+        mNativeWrapper.setPhysicalAddress(0x2000);
         mHdmiCecController = HdmiCecController.createWithNativeWrapper(
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
@@ -199,7 +200,6 @@
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_ENABLE_CEC);
-        mNativeWrapper.setPhysicalAddress(0x2000);
         mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
     }
@@ -237,6 +237,7 @@
     @Test
     public void handleGivePhysicalAddress_success() {
         mNativeWrapper.setPhysicalAddress(0x0);
+        mHdmiControlService.onHotplug(0x0, true);
         HdmiCecMessage expectedMessage =
                 HdmiCecMessageBuilder.buildReportPhysicalAddressCommand(ADDR_TV, 0, DEVICE_TV);
         @Constants.HandleMessageResult
@@ -252,6 +253,7 @@
     @Test
     public void handleGivePhysicalAddress_failure() {
         mNativeWrapper.setPhysicalAddress(Constants.INVALID_PHYSICAL_ADDRESS);
+        mHdmiControlService.onHotplug(Constants.INVALID_PHYSICAL_ADDRESS, true);
         HdmiCecMessage expectedMessage =
                 HdmiCecMessageBuilder.buildFeatureAbortCommand(
                         ADDR_TV,
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index b50684b..2a4b797 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -221,13 +221,13 @@
                         .setEarcSupported(true)
                         .build();
         mNativeWrapper.setPortInfo(hdmiPortInfos);
+        mTvPhysicalAddress = 0x0000;
+        mNativeWrapper.setPhysicalAddress(mTvPhysicalAddress);
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mTvPhysicalAddress = 0x0000;
         mEarcBlocksArc = false;
-        mNativeWrapper.setPhysicalAddress(mTvPhysicalAddress);
         mHdmiControlService.setEarcEnabled(HdmiControlManager.EARC_FEATURE_DISABLED);
         mTestLooper.dispatchAll();
         mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
index 1ad9ce0..10f4308 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
@@ -660,4 +660,18 @@
         assertThat(mHdmiCecNetwork.getLocalDeviceList().get(0)).isInstanceOf(
                 HdmiCecLocalDeviceTv.class);
     }
+
+    @Test
+    public void portInfoInitiated_getPhysicalAddressCalled_readsFromHalOnFirstCallOnly() {
+        mNativeWrapper.clearGetPhysicalAddressCallHistory();
+        mNativeWrapper.setPhysicalAddress(0x0000);
+        mHdmiCecNetwork.initPortInfo();
+
+        assertThat(mHdmiCecNetwork.getPhysicalAddress()).isEqualTo(0x0000);
+        assertThat(mNativeWrapper.getPhysicalAddressCalled()).isTrue();
+
+        mNativeWrapper.clearGetPhysicalAddressCallHistory();
+        assertThat(mHdmiCecNetwork.getPhysicalAddress()).isEqualTo(0x0000);
+        assertThat(mNativeWrapper.getPhysicalAddressCalled()).isFalse();
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
index 9412ee0..3361e7f3 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
@@ -115,12 +115,12 @@
                         .build();
         mNativeWrapper.setPortInfo(hdmiPortInfos);
         mNativeWrapper.setPortConnectionStatus(1, true);
+        mNativeWrapper.setPhysicalAddress(0x2000);
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(contextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
         mHdmiControlService.getHdmiCecNetwork().initPortInfo();
-        mNativeWrapper.setPhysicalAddress(0x2000);
         mTestLooper.dispatchAll();
         mHdmiCecLocalDevicePlayback = mHdmiControlService.playback();
         mHdmiCecPowerStatusController = new HdmiCecPowerStatusController(mHdmiControlService);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
index 298ff46..2f4a660 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/OneTouchPlayActionTest.java
@@ -118,6 +118,8 @@
         mHdmiControlService.setHdmiCecConfig(mHdmiCecConfig);
         setHdmiControlEnabled(hdmiControlEnabled);
         mNativeWrapper = new FakeNativeWrapper();
+        mPhysicalAddress = 0x2000;
+        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         mHdmiCecController = HdmiCecController.createWithNativeWrapper(
                 this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
@@ -127,8 +129,6 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mPhysicalAddress = 0x2000;
-        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         mTestLooper.dispatchAll();
         mNativeWrapper.clearResultMessages();
     }
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
index 1d4a72f..974f64d 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
@@ -111,12 +111,12 @@
                         .setArcSupported(false)
                         .build();
         mNativeWrapper.setPortInfo(hdmiPortInfo);
+        mPhysicalAddress = 0x0000;
+        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         mHdmiControlService.initService();
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mPhysicalAddress = 0x0000;
-        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         mTestLooper.dispatchAll();
         mTvDevice = mHdmiControlService.tv();
         mNativeWrapper.clearResultMessages();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index cafe1e7..f8e465c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -128,6 +128,7 @@
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
         mNativeWrapper = new FakeNativeWrapper();
+        mNativeWrapper.setPhysicalAddress(0x0000);
         mHdmiCecController = HdmiCecController.createWithNativeWrapper(
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(mHdmiCecController);
@@ -136,7 +137,6 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mNativeWrapper.setPhysicalAddress(0x0000);
         mTestLooper.dispatchAll();
         mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mTvLogicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java
index 864a182..67a3f2a 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java
@@ -87,6 +87,8 @@
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
         mNativeWrapper = new FakeNativeWrapper();
+        mPhysicalAddress = 0x2000;
+        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper(
                 this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(hdmiCecController);
@@ -95,8 +97,6 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mPhysicalAddress = 0x2000;
-        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         mTestLooper.dispatchAll();
         mPlaybackDevice = mHdmiControlService.playback();
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java
index 06709cd..047a04c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java
@@ -89,6 +89,8 @@
         mHdmiControlService.setHdmiCecConfig(new FakeHdmiCecConfig(context));
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
         mNativeWrapper = new FakeNativeWrapper();
+        mPhysicalAddress = 0x0000;
+        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper(
                 this.mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setCecController(hdmiCecController);
@@ -97,8 +99,6 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mPhysicalAddress = 0x0000;
-        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         mTestLooper.dispatchAll();
         mTvDevice = mHdmiControlService.tv();
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
index 5163e29..1019db4 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
@@ -188,6 +188,7 @@
 
         mHdmiControlService.setIoLooper(mMyLooper);
         mNativeWrapper = new FakeNativeWrapper();
+        mNativeWrapper.setPhysicalAddress(0x0000);
         mHdmiCecController = HdmiCecController.createWithNativeWrapper(
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
@@ -205,7 +206,6 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(context);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mNativeWrapper.setPhysicalAddress(0x0000);
         mTestLooper.dispatchAll();
         mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mNativeWrapper.clearResultMessages();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
index 4dcc6a4..effea5a 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
@@ -92,6 +92,8 @@
 
         mHdmiControlService.setIoLooper(myLooper);
         mNativeWrapper = new FakeNativeWrapper();
+        mPhysicalAddress = 0x0000;
+        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         HdmiCecController hdmiCecController = HdmiCecController.createWithNativeWrapper(
                 mHdmiControlService, mNativeWrapper, mHdmiControlService.getAtomWriter());
         mHdmiControlService.setDeviceConfig(new FakeDeviceConfigWrapper());
@@ -115,8 +117,6 @@
         mHdmiControlService.onBootPhase(PHASE_SYSTEM_SERVICES_READY);
         mPowerManager = new FakePowerManagerWrapper(mContextSpy);
         mHdmiControlService.setPowerManager(mPowerManager);
-        mPhysicalAddress = 0x0000;
-        mNativeWrapper.setPhysicalAddress(mPhysicalAddress);
         mTestLooper.dispatchAll();
         mHdmiCecLocalDeviceTv = mHdmiControlService.tv();
         mPhysicalAddress = mHdmiCecLocalDeviceTv.getDeviceInfo().getLogicalAddress();
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index abd3abe..e64397d 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -372,8 +372,8 @@
         doReturn(true)
                 .when(mWindowManagerInternal)
                 .setContentRecordingSession(any(ContentRecordingSession.class));
-        ContentRecordingSession taskSession =
-                createTaskSession(mock(IBinder.class), targetUid);
+        ContentRecordingSession taskSession = createTaskSession(mock(IBinder.class));
+        taskSession.setTargetUid(targetUid);
         service.setContentRecordingSession(taskSession);
 
         projection.stop();
@@ -708,8 +708,8 @@
         mService =
                 new MediaProjectionManagerService(mContext, mMediaProjectionMetricsLoggerInjector);
 
-        ContentRecordingSession taskSession =
-                createTaskSession(mock(IBinder.class), targetUid);
+        ContentRecordingSession taskSession = createTaskSession(mock(IBinder.class));
+        taskSession.setTargetUid(targetUid);
         mService.setContentRecordingSession(taskSession);
 
         MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
@@ -915,8 +915,8 @@
                 .setContentRecordingSession(any(ContentRecordingSession.class));
         int targetUid = 123455;
 
-        ContentRecordingSession taskSession =
-                createTaskSession(mock(IBinder.class), targetUid);
+        ContentRecordingSession taskSession = createTaskSession(mock(IBinder.class));
+        taskSession.setTargetUid(targetUid);
         service.setContentRecordingSession(taskSession);
 
         verify(mMediaProjectionMetricsLogger).logInProgress(projection.uid, targetUid);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index 70a0038..3da8031 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -2363,8 +2363,20 @@
         mAttentionHelper.buzzBeepBlinkLocked(r4, DEFAULT_SIGNALS);
         verifyBeepVolume(0.5f);
 
-        verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt());
-        assertNotEquals(-1, r4.getLastAudiblyAlertedMs());
+        // Set important conversation
+        mChannel.setImportantConversation(true);
+        NotificationRecord r5 = getConversationNotificationRecord(mId, false /* insistent */,
+                false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true,
+                true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg",
+                "shortcut");
+
+        // important conversation should beep at 100% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r5, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+
+        verify(mAccessibilityService, times(5)).sendAccessibilityEvent(any(), anyInt());
+        assertNotEquals(-1, r5.getLastAudiblyAlertedMs());
     }
 
     @Test
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 e564ba6..15c9bfb 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -10696,6 +10696,7 @@
     }
 
     @Test
+    @DisableFlags(android.app.Flags.FLAG_REMOVE_REMOTE_VIEWS)
     public void testRemoveLargeRemoteViews() throws Exception {
         int removeSize = mContext.getResources().getInteger(
                 com.android.internal.R.integer.config_notificationStripRemoteViewSizeBytes);
@@ -10758,6 +10759,46 @@
     }
 
     @Test
+    @EnableFlags(android.app.Flags.FLAG_REMOVE_REMOTE_VIEWS)
+    public void testRemoveRemoteViews() throws Exception {
+        Notification np = new Notification.Builder(mContext, "test")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setContentText("test")
+                .setCustomContentView(mock(RemoteViews.class))
+                .setCustomBigContentView(mock(RemoteViews.class))
+                .setCustomHeadsUpContentView(mock(RemoteViews.class))
+                .build();
+        Notification n = new Notification.Builder(mContext, "test")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setContentText("test")
+                .setCustomContentView(mock(RemoteViews.class))
+                .setCustomBigContentView(mock(RemoteViews.class))
+                .setCustomHeadsUpContentView(mock(RemoteViews.class))
+                .setPublicVersion(np)
+                .build();
+
+        assertNotNull(n.contentView);
+        assertNotNull(n.bigContentView);
+        assertNotNull(n.headsUpContentView);
+
+        assertTrue(np.extras.containsKey(Notification.EXTRA_CONTAINS_CUSTOM_VIEW));
+        assertNotNull(np.contentView);
+        assertNotNull(np.bigContentView);
+        assertNotNull(np.headsUpContentView);
+
+        mService.fixNotification(n, mPkg, "tag", 9, 0, mUid, NOT_FOREGROUND_SERVICE, true);
+
+        assertNull(n.contentView);
+        assertNull(n.bigContentView);
+        assertNull(n.headsUpContentView);
+        assertNull(n.publicVersion.contentView);
+        assertNull(n.publicVersion.bigContentView);
+        assertNull(n.publicVersion.headsUpContentView);
+
+        verify(mUsageStats, times(1)).registerImageRemoved(mPkg);
+    }
+
+    @Test
     public void testNotificationBubbles_flagAutoExpandForeground_fails_notForeground()
             throws Exception {
         setUpPrefsForBubbles(mPkg, mUid,
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
index fd69217..2e571bb 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
@@ -20,7 +20,7 @@
 import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_ALL_APPS;
 import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_ASSIST;
 import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_NOTIFICATION_PANEL;
-import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_SETTINGS_NOTIFICATION_PANEL;
+import static com.android.server.policy.PhoneWindowManager.SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL;
 
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.annotations.RequiresFlagsEnabled;
@@ -288,12 +288,12 @@
     }
 
     @Keep
-    private static Object[][] shortPressOnSettingsTestArguments() {
-        // testName, testKeys, shortPressOnSettingsBehavior, expectedLogEvent, expectedKey,
+    private static Object[][] settingsKeyTestArguments() {
+        // testName, testKeys, settingsKeyBehavior, expectedLogEvent, expectedKey,
         // expectedModifierState
         return new Object[][]{
                 {"SETTINGS key -> Toggle Notification panel", new int[]{KeyEvent.KEYCODE_SETTINGS},
-                        SHORT_PRESS_SETTINGS_NOTIFICATION_PANEL,
+                        SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL,
                         KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_SETTINGS, 0}};
     }
 
@@ -303,7 +303,7 @@
         mPhoneWindowManager.overrideKeyEventSource(VENDOR_ID, PRODUCT_ID, DEVICE_BUS);
         mPhoneWindowManager.overrideLaunchHome();
         mPhoneWindowManager.overrideSearchKeyBehavior(
-                PhoneWindowManager.SEARCH_BEHAVIOR_TARGET_ACTIVITY);
+                PhoneWindowManager.SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY);
         mPhoneWindowManager.overrideEnableBugReportTrigger(true);
         mPhoneWindowManager.overrideStatusBarManagerInternal();
         mPhoneWindowManager.overrideStartActivity();
@@ -349,11 +349,11 @@
     }
 
     @Test
-    @Parameters(method = "shortPressOnSettingsTestArguments")
-    public void testShortPressOnSettings(String testName, int[] testKeys,
-            int shortPressOnSettingsBehavior, KeyboardLogEvent expectedLogEvent, int expectedKey,
+    @Parameters(method = "settingsKeyTestArguments")
+    public void testSettingsKey(String testName, int[] testKeys,
+            int settingsKeyBehavior, KeyboardLogEvent expectedLogEvent, int expectedKey,
             int expectedModifierState) {
-        mPhoneWindowManager.overrideShortPressOnSettingsBehavior(shortPressOnSettingsBehavior);
+        mPhoneWindowManager.overrideSettingsKeyBehavior(settingsKeyBehavior);
         sendKeyCombination(testKeys, 0 /* duration */);
         mPhoneWindowManager.assertShortcutLogged(VENDOR_ID, PRODUCT_ID, expectedLogEvent,
                 expectedKey, expectedModifierState, DEVICE_BUS,
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index dd9d05a..fdb57d1 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -496,8 +496,8 @@
         mPhoneWindowManager.mDoubleTapOnHomeBehavior = behavior;
     }
 
-    void overrideShortPressOnSettingsBehavior(int behavior) {
-        mPhoneWindowManager.mShortPressOnSettingsBehavior = behavior;
+    void overrideSettingsKeyBehavior(int behavior) {
+        mPhoneWindowManager.mSettingsKeyBehavior = behavior;
     }
 
     void overrideCanStartDreaming(boolean canDream) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index 12c1377..a4bec64 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -57,7 +57,8 @@
 @SmallTest
 @Presubmit
 @RunWith(WindowTestRunner.class)
-public class DesktopModeLaunchParamsModifierTests extends LaunchParamsModifierTestsBase {
+public class DesktopModeLaunchParamsModifierTests extends
+        LaunchParamsModifierTestsBase<DesktopModeLaunchParamsModifier> {
     @Before
     public void setUp() throws Exception {
         mActivity = new ActivityBuilder(mAtm).build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsModifierTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsModifierTestsBase.java
index 55f5df1..87671f2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsModifierTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsModifierTestsBase.java
@@ -33,7 +33,7 @@
 import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
 
 /** Common base class for launch param modifier unit test classes. */
-public class LaunchParamsModifierTestsBase extends WindowTestsBase{
+public class LaunchParamsModifierTestsBase<T extends LaunchParamsModifier> extends WindowTestsBase {
 
     static final Rect DISPLAY_BOUNDS = new Rect(/* left */ 0, /* top */ 0,
             /* right */ 1920, /* bottom */ 1080);
@@ -42,7 +42,7 @@
 
     ActivityRecord mActivity;
 
-    LaunchParamsModifier mTarget;
+    T mTarget;
 
     LaunchParams mCurrent;
     LaunchParams mResult;
diff --git a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
index 402b704..78509db 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PhysicalDisplaySwitchTransitionLauncherTest.java
@@ -273,7 +273,7 @@
         if (enabled) {
             mTransitionController.registerTransitionPlayer(mPlayer, null /* proc */);
         } else {
-            mTransitionController.detachPlayer();
+            mTransitionController.unregisterTransitionPlayer(mPlayer);
         }
     }
 
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 6b605ec..96ddfe8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -108,6 +108,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsDisabled;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.Properties;
 import android.view.InsetsFrameProvider;
@@ -188,6 +189,7 @@
     private void setUpApp(DisplayContent display) {
         mTask = new TaskBuilder(mSupervisor).setDisplay(display).setCreateActivity(true).build();
         mActivity = mTask.getTopNonFinishingActivity();
+        doReturn(false).when(mActivity).isImmersiveMode(any());
     }
 
     private void setUpDisplaySizeWithApp(int dw, int dh) {
@@ -396,6 +398,55 @@
         verify(translucentActivity.mLetterboxUiController).updateInheritedLetterbox();
     }
 
+    // TODO(b/333663877): Enable test after fix
+    @Test
+    @RequiresFlagsDisabled({Flags.FLAG_INSETS_DECOUPLED_CONFIGURATION})
+    public void testRepositionLandscapeImmersiveAppWithDisplayCutout() {
+        final int dw = 2100;
+        final int dh = 2000;
+        final int cutoutHeight = 150;
+        final TestDisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh)
+                .setCanRotate(false)
+                .setNotch(cutoutHeight)
+                .build();
+        setUpApp(display);
+        display.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mWm.mLetterboxConfiguration.setLetterboxVerticalPositionMultiplier(0.5f);
+        mWm.mLetterboxConfiguration.setIsVerticalReachabilityEnabled(true);
+
+        doReturn(true).when(mActivity).isImmersiveMode(any());
+        prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
+                SCREEN_ORIENTATION_LANDSCAPE);
+        addWindowToActivity(mActivity);
+        mActivity.mRootWindowContainer.performSurfacePlacement();
+
+        final Function<ActivityRecord, Rect> innerBoundsOf =
+                (ActivityRecord a) -> {
+                    final Rect bounds = new Rect();
+                    a.mLetterboxUiController.getLetterboxInnerBounds(bounds);
+                    return bounds;
+                };
+
+        final Consumer<Integer> doubleClick =
+                (Integer y) -> {
+                    mActivity.mLetterboxUiController.handleVerticalDoubleTap(y);
+                    mActivity.mRootWindowContainer.performSurfacePlacement();
+                };
+
+        final Rect bounds = mActivity.getBounds();
+        assertTrue(bounds.top > cutoutHeight && bounds.bottom < dh);
+        assertEquals(dw, bounds.width());
+
+        // Double click bottom.
+        doubleClick.accept(dh - 10);
+        assertEquals(dh, innerBoundsOf.apply(mActivity).bottom);
+
+        // Double click top.
+        doubleClick.accept(10);
+        doubleClick.accept(10);
+        assertEquals(cutoutHeight, innerBoundsOf.apply(mActivity).top);
+    }
+
     @Test
     public void testResetOpaqueReferenceWhenOpaqueIsDestroyed() {
         mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true);
@@ -4034,8 +4085,7 @@
 
         // Prepare unresizable landscape activity
         prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
-        final DisplayPolicy displayPolicy = mActivity.mDisplayContent.getDisplayPolicy();
-        doReturn(immersive).when(displayPolicy).isImmersiveMode();
+        doReturn(immersive).when(mActivity).isImmersiveMode(any());
 
         mActivity.mRootWindowContainer.performSurfacePlacement();
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
index 2ce21ba..3c921c6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
@@ -70,7 +70,8 @@
 @SmallTest
 @Presubmit
 @RunWith(WindowTestRunner.class)
-public class TaskLaunchParamsModifierTests extends LaunchParamsModifierTestsBase {
+public class TaskLaunchParamsModifierTests extends
+        LaunchParamsModifierTestsBase<TaskLaunchParamsModifier> {
     private static final Rect SMALL_DISPLAY_BOUNDS = new Rect(/* left */ 0, /* top */ 0,
             /* right */ 1000, /* bottom */ 500);
     private static final Rect SMALL_DISPLAY_STABLE_BOUNDS = new Rect(/* left */ 100,
@@ -1898,8 +1899,7 @@
         }
         Rect startingBounds = new Rect(0, 0, 20, 20);
         Rect adjustedBounds = new Rect(startingBounds);
-        ((TaskLaunchParamsModifier) mTarget).adjustBoundsToAvoidConflict(displayBounds,
-                existingTaskBounds, adjustedBounds);
+        mTarget.adjustBoundsToAvoidConflict(displayBounds, existingTaskBounds, adjustedBounds);
         assertEquals(startingBounds, adjustedBounds);
     }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 80c066d..698afaa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1549,8 +1549,10 @@
         });
         assertTrue(activity1.isVisible());
         doReturn(false).when(task1).isTranslucent(null);
+        doReturn(false).when(task1).isTranslucentForTransition();
         assertTrue(controller.canApplyDim(task1));
         doReturn(true).when(task1).isTranslucent(null);
+        doReturn(true).when(task1).isTranslucentForTransition();
         assertFalse(controller.canApplyDim(task1));
 
         controller.finishTransition(closeTransition);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index 7e6301f..24ebad6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -795,39 +795,55 @@
     }
 
     @Test
-    public void testGetTaskWindowContainerTokenForLaunchCookie_nullCookie() {
-        WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForLaunchCookie(null);
-        assertThat(wci).isNull();
-    }
-
-    @Test
-    public void testGetTaskWindowContainerTokenForLaunchCookie_invalidCookie() {
+    public void testGetTaskWindowContainerTokenForRecordingSession_invalidCookie() {
         Binder cookie = new Binder("test cookie");
-        WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForLaunchCookie(cookie);
+        WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForRecordingSession(
+                ContentRecordingSession.createTaskSession(cookie));
         assertThat(wci).isNull();
 
         final ActivityRecord testActivity = new ActivityBuilder(mAtm)
                 .setCreateTask(true)
                 .build();
 
-        wci = mWm.getTaskWindowContainerInfoForLaunchCookie(cookie);
+        wci = mWm.getTaskWindowContainerInfoForRecordingSession(
+                ContentRecordingSession.createTaskSession(cookie));
         assertThat(wci).isNull();
     }
 
     @Test
-    public void testGetTaskWindowContainerTokenForLaunchCookie_validCookie() {
+    public void testGetTaskWindowContainerTokenForRecordingSession_validCookie() {
         final Binder cookie = new Binder("ginger cookie");
         final WindowContainerToken launchRootTask = mock(WindowContainerToken.class);
         final int uid = 123;
         setupActivityWithLaunchCookie(cookie, launchRootTask, uid);
 
-        WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForLaunchCookie(cookie);
+        WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForRecordingSession(
+                ContentRecordingSession.createTaskSession(cookie));
         mExpect.that(wci.getToken()).isEqualTo(launchRootTask);
         mExpect.that(wci.getUid()).isEqualTo(uid);
     }
 
     @Test
-    public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies() {
+    public void testGetTaskWindowContainerTokenForRecordingSession_validTaskId() {
+        final WindowContainerToken launchRootTask = mock(WindowContainerToken.class);
+        final WindowContainer.RemoteToken remoteToken = mock(WindowContainer.RemoteToken.class);
+        when(remoteToken.toWindowContainerToken()).thenReturn(launchRootTask);
+
+        final int uid = 123;
+        final ActivityRecord testActivity =
+                new ActivityBuilder(mAtm).setCreateTask(true).setUid(uid).build();
+        testActivity.mLaunchCookie = null;
+        testActivity.getTask().mRemoteToken = remoteToken;
+
+        WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForRecordingSession(
+                ContentRecordingSession.createTaskSession(
+                        new Binder("cookie"), testActivity.getTask().mTaskId));
+        mExpect.that(wci.getToken()).isEqualTo(launchRootTask);
+        mExpect.that(wci.getUid()).isEqualTo(uid);
+    }
+
+    @Test
+    public void testGetTaskWindowContainerTokenForRecordingSession_multipleCookies() {
         final Binder cookie1 = new Binder("ginger cookie");
         final WindowContainerToken launchRootTask1 = mock(WindowContainerToken.class);
         final int uid1 = 123;
@@ -839,13 +855,14 @@
         setupActivityWithLaunchCookie(new Binder("peanut butter cookie"),
                 mock(WindowContainerToken.class), /* uid= */ 789);
 
-        WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForLaunchCookie(cookie1);
+        WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForRecordingSession(
+                ContentRecordingSession.createTaskSession(cookie1));
         mExpect.that(wci.getToken()).isEqualTo(launchRootTask1);
         mExpect.that(wci.getUid()).isEqualTo(uid1);
     }
 
     @Test
-    public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies_noneValid() {
+    public void testGetTaskWindowContainerTokenForRecordingSession_multipleCookies_noneValid() {
         setupActivityWithLaunchCookie(new Binder("ginger cookie"),
                 mock(WindowContainerToken.class), /* uid= */ 123);
 
@@ -855,8 +872,8 @@
         setupActivityWithLaunchCookie(new Binder("peanut butter cookie"),
                 mock(WindowContainerToken.class), /* uid= */ 789);
 
-        WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForLaunchCookie(
-                new Binder("some other cookie"));
+        WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForRecordingSession(
+                ContentRecordingSession.createTaskSession(new Binder("some other cookie")));
         assertThat(wci).isNull();
     }
 
@@ -895,6 +912,7 @@
     public void setContentRecordingSession_sessionContentTask_matchingTask_returnsTrue() {
         WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
         ActivityRecord activityRecord = createActivityRecord(createTask(mDefaultDisplay));
+        activityRecord.mLaunchCookie = new Binder();
         ContentRecordingSession session = ContentRecordingSession.createTaskSession(
                 activityRecord.mLaunchCookie);
 
@@ -908,6 +926,7 @@
         WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class);
         Task task = createTask(mDefaultDisplay);
         ActivityRecord activityRecord = createActivityRecord(task);
+        activityRecord.mLaunchCookie = new Binder();
         ContentRecordingSession session =
                 ContentRecordingSession.createTaskSession(activityRecord.mLaunchCookie);
 
@@ -915,7 +934,7 @@
 
         mExpect.that(session.getTokenToRecord())
                 .isEqualTo(task.mRemoteToken.toWindowContainerToken().asBinder());
-        mExpect.that(session.getTargetUid()).isEqualTo(activityRecord.getUid());
+        mExpect.that(session.getTargetUid()).isEqualTo(task.effectiveUid);
     }
 
     @Test
diff --git a/telephony/java/android/telephony/Annotation.java b/telephony/java/android/telephony/Annotation.java
index 2435243..8fe107c 100644
--- a/telephony/java/android/telephony/Annotation.java
+++ b/telephony/java/android/telephony/Annotation.java
@@ -716,6 +716,31 @@
     })
     public @interface NetCapability { }
 
+
+    /**
+     * Representing the transport type.  Apps should generally not care about transport.  A
+     * request for a fast internet connection could be satisfied by a number of different
+     * transports.  If any are specified here it will be satisfied a Network that matches
+     * any of them.  If a caller doesn't care about the transport it should not specify any.
+     * Must update here when new capabilities are added in {@link NetworkCapabilities}.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "TRANSPORT_" }, value = {
+            NetworkCapabilities.TRANSPORT_CELLULAR,
+            NetworkCapabilities.TRANSPORT_WIFI,
+            NetworkCapabilities.TRANSPORT_BLUETOOTH,
+            NetworkCapabilities.TRANSPORT_ETHERNET,
+            NetworkCapabilities.TRANSPORT_VPN,
+            NetworkCapabilities.TRANSPORT_WIFI_AWARE,
+            NetworkCapabilities.TRANSPORT_LOWPAN,
+            NetworkCapabilities.TRANSPORT_TEST,
+            NetworkCapabilities.TRANSPORT_USB,
+            NetworkCapabilities.TRANSPORT_THREAD,
+            NetworkCapabilities.TRANSPORT_SATELLITE,
+    })
+    public @interface ConnectivityTransport { }
+
+
     /**
      * Per Android API guideline 8.15, annotation can't be public APIs. So duplicate
      * android.net.NetworkAgent.ValidationStatus here. Must update here when new validation status
diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
index 379b45c..a23f211 100644
--- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
+++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/splitscreen/EnterSystemSplitTest.kt
@@ -24,6 +24,7 @@
 import android.tools.flicker.legacy.LegacyFlickerTest
 import android.tools.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.traces.parsers.toFlickerComponent
+import androidx.test.filters.FlakyTest
 import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase
 import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
 import com.android.server.wm.flicker.testapp.ActivityOptions
@@ -174,6 +175,12 @@
         }
     }
 
+    @FlakyTest(bugId = 342596801)
+    @Test
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
+        super.visibleWindowsShownMoreThanOneConsecutiveEntry()
+    }
+
     @Ignore("Not applicable to this CUJ.")
     override fun visibleLayersShownMoreThanOneConsecutiveEntry() {}
 
diff --git a/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/OpenShowWhenLockedSeamlessAppRotationTest.kt b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/OpenShowWhenLockedSeamlessAppRotationTest.kt
new file mode 100644
index 0000000..bf569bc
--- /dev/null
+++ b/tests/FlickerTests/Rotation/src/com/android/server/wm/flicker/rotation/OpenShowWhenLockedSeamlessAppRotationTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.flicker.rotation
+
+import android.platform.test.annotations.Presubmit
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.flicker.assertions.FlickerTest
+import android.tools.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.flicker.legacy.FlickerBuilder
+import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.flicker.rules.ChangeDisplayOrientationRule
+import android.tools.traces.component.ComponentNameMatcher
+import com.android.server.wm.flicker.BaseTest
+import com.android.server.wm.flicker.helpers.SeamlessRotationAppHelper
+import org.junit.Assume
+import org.junit.FixMethodOrder
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test opening an app over lockscreen with rotation change using seamless rotations.
+ */
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class OpenShowWhenLockedSeamlessAppRotationTest(flicker: LegacyFlickerTest) : BaseTest(flicker) {
+    val testApp = SeamlessRotationAppHelper(instrumentation)
+
+    override val transition: FlickerBuilder.() -> Unit
+        get() = {
+            setup {
+                device.sleep()
+                wmHelper.StateSyncBuilder().withoutTopVisibleAppWindows().waitForAndVerify()
+                device.wakeUp()
+                val originalRotation = device.displayRotation
+                ChangeDisplayOrientationRule.setRotation(Rotation.ROTATION_90)
+                Assume.assumeTrue("Assume that lockscreen uses fixed orientation",
+                        originalRotation == device.displayRotation)
+            }
+            transitions {
+                // The activity is show-when-locked, so the requested orientation will be changed
+                // from NOSENSOR(keyguard) to UNSPECIFIED(activity). Then the fixed-user-rotation
+                // (by setRotation) will take effect to rotate the display.
+                testApp.launchViaIntent(wmHelper)
+            }
+            teardown { testApp.exit(wmHelper) }
+        }
+
+    @Presubmit
+    @Test
+    fun notContainsRotationAnimation() {
+        flicker.assertLayers {
+            // Verifies that com.android.wm.shell.transition.ScreenRotationAnimation is not used.
+            notContains(ComponentNameMatcher("", "Animation leash of screenshot rotation"))
+        }
+    }
+
+    // Ignore the assertions which are included in SeamlessAppRotationTest.
+    @Test
+    @Ignore("Uninterested")
+    override fun statusBarLayerPositionAtStartAndEnd() {}
+
+    @Test
+    @Ignore("Uninterested")
+    override fun statusBarLayerIsVisibleAtStartAndEnd() {}
+
+    @Test
+    @Ignore("Uninterested")
+    override fun statusBarWindowIsAlwaysVisible() {}
+
+    @Test
+    @Ignore("Uninterested")
+    override fun navBarLayerPositionAtStartAndEnd() {}
+
+    @Test
+    @Ignore("Uninterested")
+    override fun navBarLayerIsVisibleAtStartAndEnd() {}
+
+    @Test
+    @Ignore("Uninterested")
+    override fun navBarWindowIsVisibleAtStartAndEnd() {}
+
+    @Test
+    @Ignore("Uninterested")
+    override fun navBarWindowIsAlwaysVisible() {}
+
+    @Test
+    @Ignore("Uninterested")
+    override fun visibleLayersShownMoreThanOneConsecutiveEntry() {}
+
+    @Test
+    @Ignore("Uninterested")
+    override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {}
+
+    companion object {
+        @Parameterized.Parameters(name = "{0}")
+        @JvmStatic
+        fun getParams(): Collection<FlickerTest> {
+            // The rotation will be controlled by the setup of test.
+            return LegacyFlickerTestFactory.nonRotationTests(
+                supportedRotations = listOf(Rotation.ROTATION_0),
+                supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+            )
+        }
+    }
+}
diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
index 3e500d9..45260bd 100644
--- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml
@@ -74,6 +74,7 @@
                   android:taskAffinity="com.android.server.wm.flicker.testapp.SeamlessRotationActivity"
                   android:theme="@style/CutoutShortEdges"
                   android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
+                  android:showWhenLocked="true"
                   android:label="SeamlessActivity"
                   android:exported="true">
             <intent-filter>
diff --git a/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt b/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt
index 4c1fa6e..4995eeb 100644
--- a/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt
+++ b/tools/processors/intdef_mappings/src/android/processor/IntDefProcessor.kt
@@ -30,7 +30,7 @@
 import javax.lang.model.element.AnnotationValue
 import javax.lang.model.element.TypeElement
 import javax.tools.Diagnostic.Kind
-import javax.tools.StandardLocation.CLASS_OUTPUT
+import javax.tools.StandardLocation.SOURCE_OUTPUT
 import kotlin.collections.set
 
 /**
@@ -126,7 +126,7 @@
     @Throws(IOException::class)
     private fun outputToFile(annotationTypeToIntDefMapping: Map<String, IntDefMapping>) {
         val resource = processingEnv.filer.createResource(
-                CLASS_OUTPUT, "com.android.winscope", outputName)
+                SOURCE_OUTPUT, "com.android.winscope", outputName)
         val writer = resource.openWriter()
         serializeTo(annotationTypeToIntDefMapping, writer)
         writer.close()
diff --git a/tools/processors/intdef_mappings/test/android/processor/IntDefProcessorTest.kt b/tools/processors/intdef_mappings/test/android/processor/IntDefProcessorTest.kt
index c0c159c..d87b6df 100644
--- a/tools/processors/intdef_mappings/test/android/processor/IntDefProcessorTest.kt
+++ b/tools/processors/intdef_mappings/test/android/processor/IntDefProcessorTest.kt
@@ -24,7 +24,7 @@
 import org.junit.Test
 import java.io.StringWriter
 import javax.tools.JavaFileObject
-import javax.tools.StandardLocation.CLASS_OUTPUT
+import javax.tools.StandardLocation.SOURCE_OUTPUT
 
 /**
  * Tests for [IntDefProcessor]
@@ -112,7 +112,7 @@
                 .compile(filesToCompile.toMutableList())
 
         assertThat(compilation).succeeded()
-        assertThat(compilation).generatedFile(CLASS_OUTPUT, "com.android.winscope",
+        assertThat(compilation).generatedFile(SOURCE_OUTPUT, "com.android.winscope",
                 "intDefMapping.json").contentsAsUtf8String().isEqualTo(expectedFile)
     }